From 4838319af5f76e3732bd3ba38c1fc2e47d545f1e Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 16 Jul 2024 08:02:45 +1000 Subject: [PATCH] make.tcl updates support for modules_tclX where X is tcl major version --- src/bootsupport/modules/dictutils-0.2.1.tm | 290 +- .../{ => modules}/include_modules.config | 7 +- src/bootsupport/modules/mime-1.7.0.tm | 3942 ---------- src/bootsupport/modules/oolib-0.1.2.tm | 402 +- src/bootsupport/modules/oolib-0.1.tm | 195 - src/bootsupport/modules/overtype-1.6.1.tm | 3399 -------- src/bootsupport/modules/overtype-1.6.2.tm | 3415 -------- .../{overtype-1.6.4.tm => overtype-1.6.5.tm} | 75 +- src/bootsupport/modules/punk/ansi-0.1.1.tm | 171 +- src/bootsupport/modules/punk/args-0.1.0.tm | 111 +- src/bootsupport/modules/punk/char-0.1.0.tm | 6 +- src/bootsupport/modules/punk/console-0.1.1.tm | 13 +- .../modules/punk/fileline-0.1.0.tm | 19 +- src/bootsupport/modules/punk/lib-0.1.1.tm | 986 ++- src/bootsupport/modules/punk/mix/base-0.1.tm | 24 +- .../modules/punk/mix/cli-0.3.1.tm} | 468 +- src/bootsupport/modules/punk/mix/cli-0.3.tm | 464 +- .../punk/mix/commandset/debug-0.1.0.tm | 4 +- .../punk/mix/commandset/module-0.1.0.tm | 54 +- .../punk/mix/commandset/project-0.1.0.tm | 26 +- .../templates/layouts/project/src/make.tcl | 102 +- .../modpod-loadscript.tcl | 53 + .../modpod-module-version/z | 2 + .../modpod/template_modpod-0.0.1/test.zip | Bin 0 -> 1275 bytes .../modules/modulename_buildversion.txt | 6 +- .../modules/modulename_description.txt | 20 +- .../mix/templates/utility/a b/tcltest.bat | 7 - .../mix/templates/utility/tclbatheader.txt | 6 +- .../punk/mix/templates/utility/tclbattest.bat | 16 +- .../mix/templates/utility/tclbattest2.bat | 38 +- src/bootsupport/modules/punk/ns-0.1.0.tm | 37 +- src/bootsupport/modules/punk/path-0.1.0.tm | 3 +- src/bootsupport/modules/punk/repo-0.1.1.tm | 11 +- src/bootsupport/modules/punkcheck-0.1.0.tm | 6 +- src/bootsupport/modules/textblock-0.1.1.tm | 690 +- .../modules_tcl8/include_modules.config | 9 + src/make.tcl | 419 +- .../#modpod-loadscript.tcl | 53 + .../#modpod-modpodtest-999999.0a1.0/#z | 2 + .../modpodtest-999999.0a1.0.tm | 181 + .../#modpod-zipper-0.11/zipper-0.11.tm | 120 + src/modules/#modpod-zipper-0.11/zipper.README | 28 + src/modules/modpodtest-buildversion.txt | 3 + src/modules/oolib-0.1.2.tm | 402 +- src/modules/punk-0.1.tm | 439 +- src/modules/punk/aliascore-999999.0a1.0.tm | 20 +- src/modules/punk/ansi-999999.0a1.0.tm | 140 +- src/modules/punk/args-999999.0a1.0.tm | 31 +- src/modules/punk/char-999999.0a1.0.tm | 6 +- src/modules/punk/config-0.1.tm | 199 +- src/modules/punk/console-999999.0a1.0.tm | 12 +- src/modules/punk/fileline-999999.0a1.0.tm | 12 +- src/modules/punk/lib-999999.0a1.0.tm | 646 +- src/modules/punk/mix/base-0.1.tm | 24 +- src/modules/punk/mix/cli-999999.0a1.0.tm | 1119 +++ src/modules/punk/mix/cli-buildversion.txt | 3 + .../mix/commandset/loadedlib-999999.0a1.0.tm | 5 +- .../mix/commandset/module-999999.0a1.0.tm | 54 +- .../mix/commandset/project-999999.0a1.0.tm | 26 +- .../templates/layouts/project/src/make.tcl | 102 +- .../modpod-loadscript.tcl | 53 + .../modpod-module-version/z | 2 + .../modpod/template_modpod-0.0.1/test.zip | Bin 0 -> 1275 bytes src/modules/punk/path-999999.0a1.0.tm | 3 +- src/modules/punk/repl-0.1.tm | 12 +- .../punk/repl/codethread-999999.0a1.0.tm | 4 +- src/modules/punk/repo-999999.0a1.0.tm | 11 +- src/modules/punk/zip-999999.0a1.0.tm | 632 ++ src/modules/punk/zip-buildversion.txt | 3 + src/modules/punkcheck-0.1.0.tm | 6 +- src/modules/shellfilter-0.1.9.tm | 9 +- src/modules/shellrun-0.1.1.tm | 36 + src/modules/textblock-999999.0a1.0.tm | 93 +- src/punk86.vfs/boot.tcl | 130 + src/punk86.vfs/config.tcl | 1 + src/punk86.vfs/lib/app-punk/repl.tcl | 24 +- .../lib/gridplus2.11/LICENSE.GRIDPLUS | 36 - src/punk86.vfs/lib/gridplus2.11/gridplus.tcl | 6871 ----------------- src/punk86.vfs/lib/gridplus2.11/pkgIndex.tcl | 1 - src/runtime/mapvfs.config | 3 +- src/vendormodules/Thread-2.8.9.tm | Bin 0 -> 7203 bytes .../Thread/platform/win32_x86_64-2.8.9.tm | Bin 0 -> 77919 bytes src/vendormodules/dictutils-0.2.1.tm | 290 +- src/vendormodules/dictutils-0.2.tm | 143 - src/vendormodules/gridplus-2.11.tm | Bin 0 -> 50052 bytes src/vendormodules/include_modules.config | 17 + src/vendormodules/md5-2.0.8.tm | 2 +- src/vendormodules/modpod-0.1.0.tm | 700 ++ .../overtype-1.6.5.tm} | 715 +- src/vendormodules/packageTest-0.1.0.tm | Bin 0 -> 8506 bytes src/vendormodules/tablelist-6.22.tm | Bin 0 -> 286547 bytes src/vendormodules/tablelist_tile-6.22.tm | 1 + src/vendormodules/textutil/wcswidth-35.2.tm | 2 +- src/vendormodules_tcl8/include_modules.config | 11 + src/vendormodules_tcl9/Thread-3.0b3.tm | Bin 0 -> 7175 bytes .../platform/win32_x86_64_tcl9-3.0b3.tm | Bin 0 -> 209406 bytes src/vendormodules_tcl9/include_modules.config | 11 + 97 files changed, 8343 insertions(+), 20602 deletions(-) rename src/bootsupport/{ => modules}/include_modules.config (87%) delete mode 100644 src/bootsupport/modules/mime-1.7.0.tm delete mode 100644 src/bootsupport/modules/oolib-0.1.tm delete mode 100644 src/bootsupport/modules/overtype-1.6.1.tm delete mode 100644 src/bootsupport/modules/overtype-1.6.2.tm rename src/bootsupport/modules/{overtype-1.6.4.tm => overtype-1.6.5.tm} (98%) rename src/{modules/punk/mix/cli-0.3.tm => bootsupport/modules/punk/mix/cli-0.3.1.tm} (62%) create mode 100644 src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl create mode 100644 src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z create mode 100644 src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip delete mode 100644 src/bootsupport/modules/punk/mix/templates/utility/a b/tcltest.bat create mode 100644 src/bootsupport/modules_tcl8/include_modules.config create mode 100644 src/modules/#modpod-modpodtest-999999.0a1.0/#modpod-loadscript.tcl create mode 100644 src/modules/#modpod-modpodtest-999999.0a1.0/#z create mode 100644 src/modules/#modpod-modpodtest-999999.0a1.0/modpodtest-999999.0a1.0.tm create mode 100644 src/modules/#modpod-zipper-0.11/zipper-0.11.tm create mode 100644 src/modules/#modpod-zipper-0.11/zipper.README create mode 100644 src/modules/modpodtest-buildversion.txt create mode 100644 src/modules/punk/mix/cli-999999.0a1.0.tm create mode 100644 src/modules/punk/mix/cli-buildversion.txt create mode 100644 src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl create mode 100644 src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z create mode 100644 src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip create mode 100644 src/modules/punk/zip-999999.0a1.0.tm create mode 100644 src/modules/punk/zip-buildversion.txt create mode 100644 src/punk86.vfs/boot.tcl create mode 100644 src/punk86.vfs/config.tcl delete mode 100644 src/punk86.vfs/lib/gridplus2.11/LICENSE.GRIDPLUS delete mode 100644 src/punk86.vfs/lib/gridplus2.11/gridplus.tcl delete mode 100644 src/punk86.vfs/lib/gridplus2.11/pkgIndex.tcl create mode 100644 src/vendormodules/Thread-2.8.9.tm create mode 100644 src/vendormodules/Thread/platform/win32_x86_64-2.8.9.tm delete mode 100644 src/vendormodules/dictutils-0.2.tm create mode 100644 src/vendormodules/gridplus-2.11.tm create mode 100644 src/vendormodules/include_modules.config create mode 100644 src/vendormodules/modpod-0.1.0.tm rename src/{bootsupport/modules/overtype-1.6.3.tm => vendormodules/overtype-1.6.5.tm} (87%) create mode 100644 src/vendormodules/packageTest-0.1.0.tm create mode 100644 src/vendormodules/tablelist-6.22.tm create mode 100644 src/vendormodules/tablelist_tile-6.22.tm create mode 100644 src/vendormodules_tcl8/include_modules.config create mode 100644 src/vendormodules_tcl9/Thread-3.0b3.tm create mode 100644 src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm create mode 100644 src/vendormodules_tcl9/include_modules.config diff --git a/src/bootsupport/modules/dictutils-0.2.1.tm b/src/bootsupport/modules/dictutils-0.2.1.tm index cd6b4e58..12ca495b 100644 --- a/src/bootsupport/modules/dictutils-0.2.1.tm +++ b/src/bootsupport/modules/dictutils-0.2.1.tm @@ -1,145 +1,145 @@ -# dictutils.tcl -- - # - # Various dictionary utilities. - # - # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). - # - # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). - # - - #2023 0.2.1 - changed "package require Tcl 8.6" to "package require Tcl 8.6-" - - package require Tcl 8.6- - package provide dictutils 0.2.1 - - namespace eval dictutils { - namespace export equal apply capture witharray nlappend - namespace ensemble create - - # dictutils witharray dictVar arrayVar script -- - # - # Unpacks the elements of the dictionary in dictVar into the array - # variable arrayVar and then evaluates the script. If the script - # completes with an ok, return or continue status, then the result is copied - # back into the dictionary variable, otherwise it is discarded. A - # [break] can be used to explicitly abort the transaction. - # - proc witharray {dictVar arrayVar script} { - upvar 1 $dictVar dict $arrayVar array - array set array $dict - try { uplevel 1 $script - } on break {} { # Discard the result - } on continue result - on ok result { - set dict [array get array] ;# commit changes - return $result - } on return {result opts} { - set dict [array get array] ;# commit changes - dict incr opts -level ;# remove this proc from level - return -options $opts $result - } - # All other cases will discard the changes and propagage - } - - # dictutils equal equalp d1 d2 -- - # - # Compare two dictionaries for equality. Two dictionaries are equal - # if they (a) have the same keys, (b) the corresponding values for - # each key in the two dictionaries are equal when compared using the - # equality predicate, equalp (passed as an argument). The equality - # predicate is invoked with the key and the two values from each - # dictionary as arguments. - # - proc equal {equalp d1 d2} { - if {[dict size $d1] != [dict size $d2]} { return 0 } - dict for {k v} $d1 { - if {![dict exists $d2 $k]} { return 0 } - if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } - } - return 1 - } - - # apply dictVar lambdaExpr ?arg1 arg2 ...? -- - # - # A combination of *dict with* and *apply*, this procedure creates a - # new procedure scope populated with the values in the dictionary - # variable. It then applies the lambdaTerm (anonymous procedure) in - # this new scope. If the procedure completes normally, then any - # changes made to variables in the dictionary are reflected back to - # the dictionary variable, otherwise they are ignored. This provides - # a transaction-style semantics whereby atomic updates to a - # dictionary can be performed. This procedure can also be useful for - # implementing a variety of control constructs, such as mutable - # closures. - # - proc apply {dictVar lambdaExpr args} { - upvar 1 $dictVar dict - set env $dict ;# copy - lassign $lambdaExpr params body ns - if {$ns eq ""} { set ns "::" } - set body [format { - upvar 1 env __env__ - dict with __env__ %s - } [list $body]] - set lambdaExpr [list $params $body $ns] - set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] - if {$rc == 0} { - # Copy back any updates - set dict $env - } - return -options $opts $ret - } - - # capture ?level? ?exclude? ?include? -- - # - # Captures a snapshot of the current (scalar) variable bindings at - # $level on the stack into a dictionary environment. This dictionary - # can later be used with *dictutils apply* to partially restore the - # scope, creating a first approximation of closures. The *level* - # argument should be of the forms accepted by *uplevel* and - # designates which level to capture. It defaults to 1 as in uplevel. - # The *exclude* argument specifies an optional list of literal - # variable names to avoid when performing the capture. No variables - # matching any item in this list will be captured. The *include* - # argument can be used to specify a list of glob patterns of - # variables to capture. Only variables matching one of these - # patterns are captured. The default is a single pattern "*", for - # capturing all visible variables (as determined by *info vars*). - # - proc capture {{level 1} {exclude {}} {include {*}}} { - if {[string is integer $level]} { incr level } - set env [dict create] - foreach pattern $include { - foreach name [uplevel $level [list info vars $pattern]] { - if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } - upvar $level $name value - catch { dict set env $name $value } ;# no arrays - } - } - return $env - } - - # nlappend dictVar keyList ?value ...? - # - # Append zero or more elements to the list value stored in the given - # dictionary at the path of keys specified in $keyList. If $keyList - # specifies a non-existent path of keys, nlappend will behave as if - # the path mapped to an empty list. - # - proc nlappend {dictvar keylist args} { - upvar 1 $dictvar dict - if {[info exists dict] && [dict exists $dict {*}$keylist]} { - set list [dict get $dict {*}$keylist] - } - lappend list {*}$args - dict set dict {*}$keylist $list - } - - # invoke cmd args... -- - # - # Helper procedure to invoke a callback command with arguments at - # the global scope. The helper ensures that proper quotation is - # used. The command is expected to be a list, e.g. {string equal}. - # - proc invoke {cmd args} { uplevel #0 $cmd $args } - - } +# dictutils.tcl -- + # + # Various dictionary utilities. + # + # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). + # + # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). + # + + #2023 0.2.1 - changed "package require Tcl 8.6" to "package require Tcl 8.6-" + + package require Tcl 8.6- + package provide dictutils 0.2.1 + + namespace eval dictutils { + namespace export equal apply capture witharray nlappend + namespace ensemble create + + # dictutils witharray dictVar arrayVar script -- + # + # Unpacks the elements of the dictionary in dictVar into the array + # variable arrayVar and then evaluates the script. If the script + # completes with an ok, return or continue status, then the result is copied + # back into the dictionary variable, otherwise it is discarded. A + # [break] can be used to explicitly abort the transaction. + # + proc witharray {dictVar arrayVar script} { + upvar 1 $dictVar dict $arrayVar array + array set array $dict + try { uplevel 1 $script + } on break {} { # Discard the result + } on continue result - on ok result { + set dict [array get array] ;# commit changes + return $result + } on return {result opts} { + set dict [array get array] ;# commit changes + dict incr opts -level ;# remove this proc from level + return -options $opts $result + } + # All other cases will discard the changes and propagage + } + + # dictutils equal equalp d1 d2 -- + # + # Compare two dictionaries for equality. Two dictionaries are equal + # if they (a) have the same keys, (b) the corresponding values for + # each key in the two dictionaries are equal when compared using the + # equality predicate, equalp (passed as an argument). The equality + # predicate is invoked with the key and the two values from each + # dictionary as arguments. + # + proc equal {equalp d1 d2} { + if {[dict size $d1] != [dict size $d2]} { return 0 } + dict for {k v} $d1 { + if {![dict exists $d2 $k]} { return 0 } + if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } + } + return 1 + } + + # apply dictVar lambdaExpr ?arg1 arg2 ...? -- + # + # A combination of *dict with* and *apply*, this procedure creates a + # new procedure scope populated with the values in the dictionary + # variable. It then applies the lambdaTerm (anonymous procedure) in + # this new scope. If the procedure completes normally, then any + # changes made to variables in the dictionary are reflected back to + # the dictionary variable, otherwise they are ignored. This provides + # a transaction-style semantics whereby atomic updates to a + # dictionary can be performed. This procedure can also be useful for + # implementing a variety of control constructs, such as mutable + # closures. + # + proc apply {dictVar lambdaExpr args} { + upvar 1 $dictVar dict + set env $dict ;# copy + lassign $lambdaExpr params body ns + if {$ns eq ""} { set ns "::" } + set body [format { + upvar 1 env __env__ + dict with __env__ %s + } [list $body]] + set lambdaExpr [list $params $body $ns] + set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] + if {$rc == 0} { + # Copy back any updates + set dict $env + } + return -options $opts $ret + } + + # capture ?level? ?exclude? ?include? -- + # + # Captures a snapshot of the current (scalar) variable bindings at + # $level on the stack into a dictionary environment. This dictionary + # can later be used with *dictutils apply* to partially restore the + # scope, creating a first approximation of closures. The *level* + # argument should be of the forms accepted by *uplevel* and + # designates which level to capture. It defaults to 1 as in uplevel. + # The *exclude* argument specifies an optional list of literal + # variable names to avoid when performing the capture. No variables + # matching any item in this list will be captured. The *include* + # argument can be used to specify a list of glob patterns of + # variables to capture. Only variables matching one of these + # patterns are captured. The default is a single pattern "*", for + # capturing all visible variables (as determined by *info vars*). + # + proc capture {{level 1} {exclude {}} {include {*}}} { + if {[string is integer $level]} { incr level } + set env [dict create] + foreach pattern $include { + foreach name [uplevel $level [list info vars $pattern]] { + if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } + upvar $level $name value + catch { dict set env $name $value } ;# no arrays + } + } + return $env + } + + # nlappend dictVar keyList ?value ...? + # + # Append zero or more elements to the list value stored in the given + # dictionary at the path of keys specified in $keyList. If $keyList + # specifies a non-existent path of keys, nlappend will behave as if + # the path mapped to an empty list. + # + proc nlappend {dictvar keylist args} { + upvar 1 $dictvar dict + if {[info exists dict] && [dict exists $dict {*}$keylist]} { + set list [dict get $dict {*}$keylist] + } + lappend list {*}$args + dict set dict {*}$keylist $list + } + + # invoke cmd args... -- + # + # Helper procedure to invoke a callback command with arguments at + # the global scope. The helper ensures that proper quotation is + # used. The command is expected to be a list, e.g. {string equal}. + # + proc invoke {cmd args} { uplevel #0 $cmd $args } + + } diff --git a/src/bootsupport/include_modules.config b/src/bootsupport/modules/include_modules.config similarity index 87% rename from src/bootsupport/include_modules.config rename to src/bootsupport/modules/include_modules.config index cd46f833..55093f48 100644 --- a/src/bootsupport/include_modules.config +++ b/src/bootsupport/modules/include_modules.config @@ -1,4 +1,7 @@ +#bootsupport modules can be pulled in from within other areas of src or from the built module folders of the project +#They must be already built, so generally shouldn't come directly from src/modules. + #each entry - base module set bootsupport_modules [list\ src/vendormodules cksum\ @@ -58,7 +61,3 @@ set bootsupport_modules [list\ modules oolib\ ] -#each entry - base subpath -set bootsupport_module_folders [list\ - modules punk/mix/templates -] diff --git a/src/bootsupport/modules/mime-1.7.0.tm b/src/bootsupport/modules/mime-1.7.0.tm deleted file mode 100644 index fa460769..00000000 --- a/src/bootsupport/modules/mime-1.7.0.tm +++ /dev/null @@ -1,3942 +0,0 @@ -# mime.tcl - MIME body parts -# -# (c) 1999-2000 Marshall T. Rose -# (c) 2000 Brent Welch -# (c) 2000 Sandeep Tamhankar -# (c) 2000 Dan Kuchler -# (c) 2000-2001 Eric Melski -# (c) 2001 Jeff Hobbs -# (c) 2001-2008 Andreas Kupries -# (c) 2002-2003 David Welton -# (c) 2003-2008 Pat Thoyts -# (c) 2005 Benjamin Riefenstahl -# (c) 2013-2021 Poor Yorick -# -# -# See the file "license.terms" for information on usage and redistribution -# of this file, and for a DISCLAIMER OF ALL WARRANTIES. -# -# Influenced by Borenstein's/Rose's safe-tcl (circa 1993) and Darren New's -# unpublished package of 1999. -# - -# new string features and inline scan are used, requiring 8.3. -package require Tcl 8.5 - -package provide mime 1.7.0 -package require tcl::chan::memchan - - -if {[catch {package require Trf 2.0}]} { - - # Fall-back to tcl-based procedures of base64 and quoted-printable - # encoders - ## - # Warning! - ## - # These are a fragile emulations of the more general calling - # sequence that appears to work with this code here. - ## - # The `__ignored__` arguments are expected to be `--` options on - # the caller's side. (See the uses in `copymessageaux`, - # `buildmessageaux`, `parsepart`, and `getbody`). - - package require base64 2.0 - set ::major [lindex [split [package require md5] .] 0] - - # Create these commands in the mime namespace so that they - # won't collide with things at the global namespace level - - namespace eval ::mime { - proc base64 {-mode what __ignored__ chunk} { - return [base64::$what $chunk] - } - proc quoted-printable {-mode what __ignored__ chunk} { - return [mime::qp_$what $chunk] - } - - if {$::major < 2} { - # md5 v1, result is hex string ready for use. - proc md5 {__ignored__ string} { - return [md5::md5 $string] - } - } else { - # md5 v2, need option to get hex string - proc md5 {__ignored__ string} { - return [md5::md5 -hex $string] - } - } - } - - unset ::major -} - -# -# state variables: -# -# canonicalP: input is in its canonical form -# content: type/subtype -# params: dictionary (keys are lower-case) -# encoding: transfer encoding -# version: MIME-version -# header: dictionary (keys are lower-case) -# lowerL: list of header keys, lower-case -# mixedL: list of header keys, mixed-case -# value: either "file", "parts", or "string" -# -# file: input file -# fd: cached file-descriptor, typically for root -# root: token for top-level part, for (distant) subordinates -# offset: number of octets from beginning of file/string -# count: length in octets of (encoded) content -# -# parts: list of bodies (tokens) -# -# string: input string -# -# cid: last child-id assigned -# - - -namespace eval ::mime { - variable mime - array set mime {uid 0 cid 0} - - # RFC 822 lexemes - variable addrtokenL - lappend addrtokenL \; , < > : . ( ) @ \" \[ ] \\ - variable addrlexemeL { - LX_SEMICOLON LX_COMMA - LX_LBRACKET LX_RBRACKET - LX_COLON LX_DOT - LX_LPAREN LX_RPAREN - LX_ATSIGN LX_QUOTE - LX_LSQUARE LX_RSQUARE - LX_QUOTE - } - - # RFC 2045 lexemes - variable typetokenL - lappend typetokenL \; , < > : ? ( ) @ \" \[ \] = / \\ - variable typelexemeL { - LX_SEMICOLON LX_COMMA - LX_LBRACKET LX_RBRACKET - LX_COLON LX_QUESTION - LX_LPAREN LX_RPAREN - LX_ATSIGN LX_QUOTE - LX_LSQUARE LX_RSQUARE - LX_EQUALS LX_SOLIDUS - LX_QUOTE - } - - variable encList { - ascii US-ASCII - big5 Big5 - cp1250 Windows-1250 - cp1251 Windows-1251 - cp1252 Windows-1252 - cp1253 Windows-1253 - cp1254 Windows-1254 - cp1255 Windows-1255 - cp1256 Windows-1256 - cp1257 Windows-1257 - cp1258 Windows-1258 - cp437 IBM437 - cp737 {} - cp775 IBM775 - cp850 IBM850 - cp852 IBM852 - cp855 IBM855 - cp857 IBM857 - cp860 IBM860 - cp861 IBM861 - cp862 IBM862 - cp863 IBM863 - cp864 IBM864 - cp865 IBM865 - cp866 IBM866 - cp869 IBM869 - cp874 {} - cp932 {} - cp936 GBK - cp949 {} - cp950 {} - dingbats {} - ebcdic {} - euc-cn EUC-CN - euc-jp EUC-JP - euc-kr EUC-KR - gb12345 GB12345 - gb1988 GB1988 - gb2312 GB2312 - iso2022 ISO-2022 - iso2022-jp ISO-2022-JP - iso2022-kr ISO-2022-KR - iso8859-1 ISO-8859-1 - iso8859-2 ISO-8859-2 - iso8859-3 ISO-8859-3 - iso8859-4 ISO-8859-4 - iso8859-5 ISO-8859-5 - iso8859-6 ISO-8859-6 - iso8859-7 ISO-8859-7 - iso8859-8 ISO-8859-8 - iso8859-9 ISO-8859-9 - iso8859-10 ISO-8859-10 - iso8859-13 ISO-8859-13 - iso8859-14 ISO-8859-14 - iso8859-15 ISO-8859-15 - iso8859-16 ISO-8859-16 - jis0201 JIS_X0201 - jis0208 JIS_C6226-1983 - jis0212 JIS_X0212-1990 - koi8-r KOI8-R - koi8-u KOI8-U - ksc5601 KS_C_5601-1987 - macCentEuro {} - macCroatian {} - macCyrillic {} - macDingbats {} - macGreek {} - macIceland {} - macJapan {} - macRoman {} - macRomania {} - macThai {} - macTurkish {} - macUkraine {} - shiftjis Shift_JIS - symbol {} - tis-620 TIS-620 - unicode {} - utf-8 UTF-8 - } - - variable encodings - array set encodings $encList - variable reversemap - # Initialized at the bottom of the file - - variable encAliasList { - ascii ANSI_X3.4-1968 - ascii iso-ir-6 - ascii ANSI_X3.4-1986 - ascii ISO_646.irv:1991 - ascii ASCII - ascii ISO646-US - ascii us - ascii IBM367 - ascii cp367 - cp437 cp437 - cp437 437 - cp775 cp775 - cp850 cp850 - cp850 850 - cp852 cp852 - cp852 852 - cp855 cp855 - cp855 855 - cp857 cp857 - cp857 857 - cp860 cp860 - cp860 860 - cp861 cp861 - cp861 861 - cp861 cp-is - cp862 cp862 - cp862 862 - cp863 cp863 - cp863 863 - cp864 cp864 - cp865 cp865 - cp865 865 - cp866 cp866 - cp866 866 - cp869 cp869 - cp869 869 - cp869 cp-gr - cp936 CP936 - cp936 MS936 - cp936 Windows-936 - iso8859-1 ISO_8859-1:1987 - iso8859-1 iso-ir-100 - iso8859-1 ISO_8859-1 - iso8859-1 latin1 - iso8859-1 l1 - iso8859-1 IBM819 - iso8859-1 CP819 - iso8859-2 ISO_8859-2:1987 - iso8859-2 iso-ir-101 - iso8859-2 ISO_8859-2 - iso8859-2 latin2 - iso8859-2 l2 - iso8859-3 ISO_8859-3:1988 - iso8859-3 iso-ir-109 - iso8859-3 ISO_8859-3 - iso8859-3 latin3 - iso8859-3 l3 - iso8859-4 ISO_8859-4:1988 - iso8859-4 iso-ir-110 - iso8859-4 ISO_8859-4 - iso8859-4 latin4 - iso8859-4 l4 - iso8859-5 ISO_8859-5:1988 - iso8859-5 iso-ir-144 - iso8859-5 ISO_8859-5 - iso8859-5 cyrillic - iso8859-6 ISO_8859-6:1987 - iso8859-6 iso-ir-127 - iso8859-6 ISO_8859-6 - iso8859-6 ECMA-114 - iso8859-6 ASMO-708 - iso8859-6 arabic - iso8859-7 ISO_8859-7:1987 - iso8859-7 iso-ir-126 - iso8859-7 ISO_8859-7 - iso8859-7 ELOT_928 - iso8859-7 ECMA-118 - iso8859-7 greek - iso8859-7 greek8 - iso8859-8 ISO_8859-8:1988 - iso8859-8 iso-ir-138 - iso8859-8 ISO_8859-8 - iso8859-8 hebrew - iso8859-9 ISO_8859-9:1989 - iso8859-9 iso-ir-148 - iso8859-9 ISO_8859-9 - iso8859-9 latin5 - iso8859-9 l5 - iso8859-10 iso-ir-157 - iso8859-10 l6 - iso8859-10 ISO_8859-10:1992 - iso8859-10 latin6 - iso8859-14 iso-ir-199 - iso8859-14 ISO_8859-14:1998 - iso8859-14 ISO_8859-14 - iso8859-14 latin8 - iso8859-14 iso-celtic - iso8859-14 l8 - iso8859-15 ISO_8859-15 - iso8859-15 Latin-9 - iso8859-16 iso-ir-226 - iso8859-16 ISO_8859-16:2001 - iso8859-16 ISO_8859-16 - iso8859-16 latin10 - iso8859-16 l10 - jis0201 X0201 - jis0208 iso-ir-87 - jis0208 x0208 - jis0208 JIS_X0208-1983 - jis0212 x0212 - jis0212 iso-ir-159 - ksc5601 iso-ir-149 - ksc5601 KS_C_5601-1989 - ksc5601 KSC5601 - ksc5601 korean - shiftjis MS_Kanji - utf-8 UTF8 - } - - namespace export {*}{ - copymessage finalize getbody getheader getproperty initialize - mapencoding parseaddress parsedatetime reversemapencoding setheader - uniqueID - } -} - -# ::mime::initialize -- -# -# Creates a MIME part, and returnes the MIME token for that part. -# -# Arguments: -# args Args can be any one of the following: -# ?-canonical type/subtype -# ?-param {key value}?... -# ?-encoding value? -# ?-header {key value}?... ? -# (-file name | -string value | -parts {token1 ... tokenN}) -# -# If the -canonical option is present, then the body is in -# canonical (raw) form and is found by consulting either the -file, -# -string, or -parts option. -# -# In addition, both the -param and -header options may occur zero -# or more times to specify "Content-Type" parameters (e.g., -# "charset") and header keyword/values (e.g., -# "Content-Disposition"), respectively. -# -# Also, -encoding, if present, specifies the -# "Content-Transfer-Encoding" when copying the body. -# -# If the -canonical option is not present, then the MIME part -# contained in either the -file or the -string option is parsed, -# dynamically generating subordinates as appropriate. -# -# Results: -# An initialized mime token. - -proc ::mime::initialize args { - global errorCode errorInfo - - variable mime - - set token [namespace current]::[incr mime(uid)] - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {[catch [list mime::initializeaux $token {*}$args] result eopts]} { - catch {mime::finalize $token -subordinates dynamic} - return -options $eopts $result - } - return $token -} - -# ::mime::initializeaux -- -# -# Configures the MIME token created in mime::initialize based on -# the arguments that mime::initialize supports. -# -# Arguments: -# token The MIME token to configure. -# args Args can be any one of the following: -# ?-canonical type/subtype -# ?-param {key value}?... -# ?-encoding value? -# ?-header {key value}?... ? -# (-file name | -string value | -parts {token1 ... tokenN}) -# -# Results: -# Either configures the mime token, or throws an error. - -proc ::mime::initializeaux {token args} { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - - array set params [set state(params) {}] - set state(encoding) {} - set state(version) 1.0 - - set state(header) {} - set state(lowerL) {} - set state(mixedL) {} - - set state(cid) 0 - - set userheader 0 - - set argc [llength $args] - for {set argx 0} {$argx < $argc} {incr argx} { - set option [lindex $args $argx] - if {[incr argx] >= $argc} { - error "missing argument to $option" - } - set value [lindex $args $argx] - - switch -- $option { - -canonical { - set state(content) [string tolower $value] - } - - -param { - if {[llength $value] != 2} { - error "-param expects a key and a value, not $value" - } - set lower [string tolower [set mixed [lindex $value 0]]] - if {[info exists params($lower)]} { - error "the $mixed parameter may be specified at most once" - } - - set params($lower) [lindex $value 1] - set state(params) [array get params] - } - - -encoding { - set value [string tolower $value[set value {}]] - - switch -- $value { - 7bit - 8bit - binary - quoted-printable - base64 { - } - - default { - error "unknown value for -encoding $state(encoding)" - } - } - set state(encoding) [string tolower $value] - } - - -header { - if {[llength $value] != 2} { - error "-header expects a key and a value, not $value" - } - set lower [string tolower [set mixed [lindex $value 0]]] - if {$lower eq {content-type}} { - error "use -canonical instead of -header $value" - } - if {$lower eq {content-transfer-encoding}} { - error "use -encoding instead of -header $value" - } - if {$lower in {content-md5 mime-version}} { - error {don't go there...} - } - if {$lower ni $state(lowerL)} { - lappend state(lowerL) $lower - lappend state(mixedL) $mixed - } - - set userheader 1 - - array set header $state(header) - lappend header($lower) [lindex $value 1] - set state(header) [array get header] - } - - -file { - set state(file) $value - } - - -parts { - set state(parts) $value - } - - -string { - set state(string) $value - - set state(lines) [split $value \n] - set state(lines.count) [llength $state(lines)] - set state(lines.current) 0 - } - - -root { - # the following are internal options - - set state(root) $value - } - - -offset { - set state(offset) $value - } - - -count { - set state(count) $value - } - - -lineslist { - set state(lines) $value - set state(lines.count) [llength $state(lines)] - set state(lines.current) 0 - #state(string) is needed, but will be built when required - set state(string) {} - } - - default { - error "unknown option $option" - } - } - } - - #We only want one of -file, -parts or -string: - set valueN 0 - foreach value {file parts string} { - if {[info exists state($value)]} { - set state(value) $value - incr valueN - } - } - if {$valueN != 1 && ![info exists state(lines)]} { - error {specify exactly one of -file, -parts, or -string} - } - - if {[set state(canonicalP) [info exists state(content)]]} { - switch -- $state(value) { - file { - set state(offset) 0 - } - - parts { - switch -glob -- $state(content) { - text/* - - - image/* - - - audio/* - - - video/* { - error "-canonical $state(content) and -parts do not mix" - } - - default { - if {$state(encoding) ne {}} { - error {-encoding and -parts do not mix} - } - } - } - } - default {# Go ahead} - } - - if {[lsearch -exact $state(lowerL) content-id] < 0} { - lappend state(lowerL) content-id - lappend state(mixedL) Content-ID - - array set header $state(header) - lappend header(content-id) [uniqueID] - set state(header) [array get header] - } - - set state(version) 1.0 - return - } - - if {$state(params) ne {}} { - error {-param requires -canonical} - } - if {$state(encoding) ne {}} { - error {-encoding requires -canonical} - } - if {$userheader} { - error {-header requires -canonical} - } - if {[info exists state(parts)]} { - error {-parts requires -canonical} - } - - if {[set fileP [info exists state(file)]]} { - if {[set openP [info exists state(root)]]} { - # FRINK: nocheck - variable $state(root) - upvar 0 $state(root) root - - set state(fd) $root(fd) - } else { - set state(root) $token - set state(fd) [open $state(file) RDONLY] - set state(offset) 0 - seek $state(fd) 0 end - set state(count) [tell $state(fd)] - - fconfigure $state(fd) -translation binary - } - } - - set code [catch {mime::parsepart $token} result] - set ecode $errorCode - set einfo $errorInfo - - if {$fileP} { - if {!$openP} { - unset state(root) - catch {close $state(fd)} - } - unset state(fd) - } - - return -code $code -errorinfo $einfo -errorcode $ecode $result -} - -# ::mime::parsepart -- -# -# Parses the MIME headers and attempts to break up the message -# into its various parts, creating a MIME token for each part. -# -# Arguments: -# token The MIME token to parse. -# -# Results: -# Throws an error if it has problems parsing the MIME token, -# otherwise it just sets up the appropriate variables. - -proc ::mime::parsepart {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {[set fileP [info exists state(file)]]} { - seek $state(fd) [set pos $state(offset)] start - set last [expr {$state(offset) + $state(count) - 1}] - } else { - set string $state(string) - } - - set vline {} - while 1 { - set blankP 0 - if {$fileP} { - if {($pos > $last) || ([set x [gets $state(fd) line]] <= 0)} { - set blankP 1 - } else { - incr pos [expr {$x + 1}] - } - } else { - if {$state(lines.current) >= $state(lines.count)} { - set blankP 1 - set line {} - } else { - set line [lindex $state(lines) $state(lines.current)] - incr state(lines.current) - set x [string length $line] - if {$x == 0} {set blankP 1} - } - } - - if {!$blankP && [string match *\r $line]} { - set line [string range $line 0 $x-2]] - if {$x == 1} { - set blankP 1 - } - } - - if {!$blankP && ( - [string first { } $line] == 0 - || - [string first \t $line] == 0 - )} { - append vline \n $line - continue - } - - if {$vline eq {}} { - if {$blankP} { - break - } - - set vline $line - continue - } - - if { - [set x [string first : $vline]] <= 0 - || - [set mixed [string trimright [ - string range $vline 0 [expr {$x - 1}]]]] eq {} - } { - error "improper line in header: $vline" - } - set value [string trim [string range $vline [expr {$x + 1}] end]] - switch -- [set lower [string tolower $mixed]] { - content-type { - if {[info exists state(content)]} { - error "multiple Content-Type fields starting with $vline" - } - - if {![catch {set x [parsetype $token $value]}]} { - set state(content) [lindex $x 0] - set state(params) [lindex $x 1] - } - } - - content-md5 { - } - - content-transfer-encoding { - if { - $state(encoding) ne {} - && - $state(encoding) ne [string tolower $value] - } { - error "multiple Content-Transfer-Encoding fields starting with $vline" - } - - set state(encoding) [string tolower $value] - } - - mime-version { - set state(version) $value - } - - default { - if {[lsearch -exact $state(lowerL) $lower] < 0} { - lappend state(lowerL) $lower - lappend state(mixedL) $mixed - } - - array set header $state(header) - lappend header($lower) $value - set state(header) [array get header] - } - } - - if {$blankP} { - break - } - set vline $line - } - - if {![info exists state(content)]} { - set state(content) text/plain - set state(params) [list charset us-ascii] - } - - if {![string match multipart/* $state(content)]} { - if {$fileP} { - set x [tell $state(fd)] - incr state(count) [expr {$state(offset) - $x}] - set state(offset) $x - } else { - # rebuild string, this is cheap and needed by other functions - set state(string) [join [ - lrange $state(lines) $state(lines.current) end] \n] - } - - if {[string match message/* $state(content)]} { - # FRINK: nocheck - variable [set child $token-[incr state(cid)]] - - set state(value) parts - set state(parts) $child - if {$fileP} { - mime::initializeaux $child \ - -file $state(file) -root $state(root) \ - -offset $state(offset) -count $state(count) - } else { - if {[info exists state(encoding)]} { - set strng [join [ - lrange $state(lines) $state(lines.current) end] \n] - switch -- $state(encoding) { - base64 - - quoted-printable { - set strng [$state(encoding) -mode decode -- $strng] - } - default {} - } - mime::initializeaux $child -string $strng - } else { - mime::initializeaux $child -lineslist [ - lrange $state(lines) $state(lines.current) end] - } - } - } - - return - } - - set state(value) parts - - set boundary {} - foreach {k v} $state(params) { - if {$k eq {boundary}} { - set boundary $v - break - } - } - if {$boundary eq {}} { - error "boundary parameter is missing in $state(content)" - } - if {[string trim $boundary] eq {}} { - error "boundary parameter is empty in $state(content)" - } - - if {$fileP} { - set pos [tell $state(fd)] - # This variable is like 'start', for the reasons laid out - # below, in the other branch of this conditional. - set initialpos $pos - } else { - # This variable is like 'start', a list of lines in the - # part. This record is made even before we find a starting - # boundary and used if we run into the terminating boundary - # before a starting boundary was found. In that case the lines - # before the terminator as recorded by tracelines are seen as - # the part, or at least we attempt to parse them as a - # part. See the forceoctet and nochild flags later. We cannot - # use 'start' as that records lines only after the starting - # boundary was found. - set tracelines [list] - } - - set inP 0 - set moreP 1 - set forceoctet 0 - while {$moreP} { - if {$fileP} { - if {$pos > $last} { - # We have run over the end of the part per the outer - # information without finding a terminating boundary. - # We now fake the boundary and force the parser to - # give any new part coming of this a mime-type of - # application/octet-stream regardless of header - # information. - set line "--$boundary--" - set x [string length $line] - set forceoctet 1 - } else { - if {[set x [gets $state(fd) line]] < 0} { - error "end-of-file encountered while parsing $state(content)" - } - } - incr pos [expr {$x + 1}] - } else { - if {$state(lines.current) >= $state(lines.count)} { - error "end-of-string encountered while parsing $state(content)" - } else { - set line [lindex $state(lines) $state(lines.current)] - incr state(lines.current) - set x [string length $line] - } - set x [string length $line] - } - if {[string last \r $line] == $x - 1} { - set line [string range $line 0 [expr {$x - 2}]] - set crlf 2 - } else { - set crlf 1 - } - - if {[string first --$boundary $line] != 0} { - if {$inP && !$fileP} { - lappend start $line - } - continue - } else { - lappend tracelines $line - } - - if {!$inP} { - # Haven't seen the starting boundary yet. Check if the - # current line contains this starting boundary. - - if {$line eq "--$boundary"} { - # Yes. Switch parser state to now search for the - # terminating boundary of the part and record where - # the part begins (or initialize the recorder for the - # lines in the part). - set inP 1 - if {$fileP} { - set start $pos - } else { - set start [list] - } - continue - } elseif {$line eq "--$boundary--"} { - # We just saw a terminating boundary before we ever - # saw the starting boundary of a part. This forces us - # to stop parsing, we do this by forcing the parser - # into an accepting state. We will try to create a - # child part based on faked start position or recorded - # lines, or, if that fails, let the current part have - # no children. - - # As an example note the test case mime-3.7 and the - # referenced file "badmail1.txt". - - set inP 1 - if {$fileP} { - set start $initialpos - } else { - set start $tracelines - } - set forceoctet 1 - # Fall through. This brings to the creation of the new - # part instead of searching further and possible - # running over the end. - } else { - continue - } - } - - # Looking for the end of the current part. We accept both a - # terminating boundary and the starting boundary of the next - # part as the end of the current part. - - if {[set moreP [string compare $line --$boundary--]] - && $line ne "--$boundary"} { - - # The current part has not ended, so we record the line - # if we are inside a part and doing string parsing. - if {$inP && !$fileP} { - lappend start $line - } - continue - } - - # The current part has ended. We now determine the exact - # boundaries, create a mime part object for it and recursively - # parse it deeper as part of that action. - - # FRINK: nocheck - variable [set child $token-[incr state(cid)]] - - lappend state(parts) $child - - set nochild 0 - if {$fileP} { - if {[set count [expr {$pos - ($start + $x + $crlf + 1)}]] < 0} { - set count 0 - } - if {$forceoctet} { - set ::errorInfo {} - if {[catch { - mime::initializeaux $child \ - -file $state(file) -root $state(root) \ - -offset $start -count $count - }]} { - set nochild 1 - set state(parts) [lrange $state(parts) 0 end-1] - } } else { - mime::initializeaux $child \ - -file $state(file) -root $state(root) \ - -offset $start -count $count - } - seek $state(fd) [set start $pos] start - } else { - if {$forceoctet} { - if {[catch { - mime::initializeaux $child -lineslist $start - }]} { - set nochild 1 - set state(parts) [lrange $state(parts) 0 end-1] - } - } else { - mime::initializeaux $child -lineslist $start - } - set start {} - } - if {$forceoctet && !$nochild} { - variable $child - upvar 0 $child childstate - set childstate(content) application/octet-stream - } - set forceoctet 0 - } -} - -# ::mime::parsetype -- -# -# Parses the string passed in and identifies the content-type and -# params strings. -# -# Arguments: -# token The MIME token to parse. -# string The content-type string that should be parsed. -# -# Results: -# Returns the content and params for the string as a two element -# tcl list. - -proc ::mime::parsetype {token string} { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - - variable typetokenL - variable typelexemeL - - set state(input) $string - set state(buffer) {} - set state(lastC) LX_END - set state(comment) {} - set state(tokenL) $typetokenL - set state(lexemeL) $typelexemeL - - set code [catch {mime::parsetypeaux $token $string} result] - set ecode $errorCode - set einfo $errorInfo - - unset {*}{ - state(input) - state(buffer) - state(lastC) - state(comment) - state(tokenL) - state(lexemeL) - } - - return -code $code -errorinfo $einfo -errorcode $ecode $result -} - -# ::mime::parsetypeaux -- -# -# A helper function for mime::parsetype. Parses the specified -# string looking for the content type and params. -# -# Arguments: -# token The MIME token to parse. -# string The content-type string that should be parsed. -# -# Results: -# Returns the content and params for the string as a two element -# tcl list. - -proc ::mime::parsetypeaux {token string} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {[parselexeme $token] ne {LX_ATOM}} { - error [format {expecting type (found %s)} $state(buffer)] - } - set type [string tolower $state(buffer)] - - switch -- [parselexeme $token] { - LX_SOLIDUS { - } - - LX_END { - if {$type ne {message}} { - error "expecting type/subtype (found $type)" - } - - return [list message/rfc822 {}] - } - - default { - error [format "expecting \"/\" (found %s)" $state(buffer)] - } - } - - if {[parselexeme $token] ne {LX_ATOM}} { - error [format "expecting subtype (found %s)" $state(buffer)] - } - append type [string tolower /$state(buffer)] - - array set params {} - while 1 { - switch -- [parselexeme $token] { - LX_END { - return [list $type [array get params]] - } - - LX_SEMICOLON { - } - - default { - error [format "expecting \";\" (found %s)" $state(buffer)] - } - } - - switch -- [parselexeme $token] { - LX_END { - return [list $type [array get params]] - } - - LX_ATOM { - } - - default { - error [format "expecting attribute (found %s)" $state(buffer)] - } - } - - set attribute [string tolower $state(buffer)] - - if {[parselexeme $token] ne {LX_EQUALS}} { - error [format {expecting "=" (found %s)} $state(buffer)] - } - - switch -- [parselexeme $token] { - LX_ATOM { - } - - LX_QSTRING { - set state(buffer) [ - string range $state(buffer) 1 [ - expr {[string length $state(buffer)] - 2}]] - } - - default { - error [format {expecting value (found %s)} $state(buffer)] - } - } - set params($attribute) $state(buffer) - } -} - -# ::mime::finalize -- -# -# mime::finalize destroys a MIME part. -# -# If the -subordinates option is present, it specifies which -# subordinates should also be destroyed. The default value is -# "dynamic". -# -# Arguments: -# token The MIME token to parse. -# args Args can be optionally be of the following form: -# ?-subordinates "all" | "dynamic" | "none"? -# -# Results: -# Returns an empty string. - -proc ::mime::finalize {token args} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - array set options [list -subordinates dynamic] - array set options $args - - switch -- $options(-subordinates) { - all { - #TODO: this code path is untested - if {$state(value) eq {parts}} { - foreach part $state(parts) { - eval [linsert $args 0 mime::finalize $part] - } - } - } - - dynamic { - for {set cid $state(cid)} {$cid > 0} {incr cid -1} { - eval [linsert $args 0 mime::finalize $token-$cid] - } - } - - none { - } - - default { - error "unknown value for -subordinates $options(-subordinates)" - } - } - - foreach name [array names state] { - unset state($name) - } - # FRINK: nocheck - unset $token -} - -# ::mime::getproperty -- -# -# mime::getproperty returns the properties of a MIME part. -# -# The properties are: -# -# property value -# ======== ===== -# content the type/subtype describing the content -# encoding the "Content-Transfer-Encoding" -# params a list of "Content-Type" parameters -# parts a list of tokens for the part's subordinates -# size the approximate size of the content (unencoded) -# -# The "parts" property is present only if the MIME part has -# subordinates. -# -# If mime::getproperty is invoked with the name of a specific -# property, then the corresponding value is returned; instead, if -# -names is specified, a list of all properties is returned; -# otherwise, a dictionary of properties is returned. -# -# Arguments: -# token The MIME token to parse. -# property One of 'content', 'encoding', 'params', 'parts', and -# 'size'. Defaults to returning a dictionary of -# properties. -# -# Results: -# Returns the properties of a MIME part - -proc ::mime::getproperty {token {property {}}} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - switch -- $property { - {} { - array set properties [list content $state(content) \ - encoding $state(encoding) \ - params $state(params) \ - size [getsize $token]] - if {[info exists state(parts)]} { - set properties(parts) $state(parts) - } - - return [array get properties] - } - - -names { - set names [list content encoding params] - if {[info exists state(parts)]} { - lappend names parts - } - - return $names - } - - content - - - encoding - - - params { - return $state($property) - } - - parts { - if {![info exists state(parts)]} { - error {MIME part is a leaf} - } - - return $state(parts) - } - - size { - return [getsize $token] - } - - default { - error "unknown property $property" - } - } -} - -# ::mime::getsize -- -# -# Determine the size (in bytes) of a MIME part/token -# -# Arguments: -# token The MIME token to parse. -# -# Results: -# Returns the size in bytes of the MIME token. - -proc ::mime::getsize {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - switch -- $state(value)/$state(canonicalP) { - file/0 { - set size $state(count) - } - - file/1 { - return [file size $state(file)] - } - - parts/0 - - - parts/1 { - set size 0 - foreach part $state(parts) { - incr size [getsize $part] - } - - return $size - } - - string/0 { - set size [string length $state(string)] - } - - string/1 { - return [string length $state(string)] - } - default { - error "Unknown combination \"$state(value)/$state(canonicalP)\"" - } - } - - if {$state(encoding) eq {base64}} { - set size [expr {($size * 3 + 2) / 4}] - } - - return $size -} - - -proc ::mime::getContentType token { - variable $token - upvar 0 $token state - set boundary {} - set res $state(content) - foreach {k v} $state(params) { - set boundary $v - append res ";\n $k=\"$v\"" - } - if {([string match multipart/* $state(content)]) \ - && ($boundary eq {})} { - # we're doing everything in one pass... - set key [clock seconds]$token[info hostname][array get state] - set seqno 8 - while {[incr seqno -1] >= 0} { - set key [md5 -- $key] - } - set boundary "----- =_[string trim [base64 -mode encode -- $key]]" - - append res ";\n boundary=\"$boundary\"" - } - return $res -} - -# ::mime::getheader -- -# -# mime::getheader returns the header of a MIME part. -# -# A header consists of zero or more key/value pairs. Each value is a -# list containing one or more strings. -# -# If mime::getheader is invoked with the name of a specific key, then -# a list containing the corresponding value(s) is returned; instead, -# if -names is specified, a list of all keys is returned; otherwise, a -# dictionary is returned. Note that when a -# key is specified (e.g., "Subject"), the list returned usually -# contains exactly one string; however, some keys (e.g., "Received") -# often occur more than once in the header, accordingly the list -# returned usually contains more than one string. -# -# Arguments: -# token The MIME token to parse. -# key Either a key or '-names'. If it is '-names' a list -# of all keys is returned. -# -# Results: -# Returns the header of a MIME part. - -proc ::mime::getheader {token {key {}}} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - array set header $state(header) - switch -- $key { - {} { - set result {} - lappend result MIME-Version $state(version) - foreach lower $state(lowerL) mixed $state(mixedL) { - foreach value $header($lower) { - lappend result $mixed $value - } - } - set tencoding [getTransferEncoding $token] - if {$tencoding ne {}} { - lappend result Content-Transfer-Encoding $tencoding - } - lappend result Content-Type [getContentType $token] - return $result - } - - -names { - return $state(mixedL) - } - - default { - set lower [string tolower $key] - - switch $lower { - content-transfer-encoding { - return [getTransferEncoding $token] - } - content-type { - return [list [getContentType $token]] - } - mime-version { - return [list $state(version)] - } - default { - if {![info exists header($lower)]} { - error "key $key not in header" - } - return $header($lower) - } - } - } - } -} - - -proc ::mime::getTransferEncoding token { - variable $token - upvar 0 $token state - set res {} - if {[set encoding $state(encoding)] eq {}} { - set encoding [encoding $token] - } - if {$encoding ne {}} { - set res $encoding - } - switch -- $encoding { - base64 - - - quoted-printable { - set converter $encoding - } - 7bit - 8bit - binary - {} { - # Bugfix for [#477088], also [#539952] - # Go ahead - } - default { - error "Can't handle content encoding \"$encoding\"" - } - } - return $res -} - -# ::mime::setheader -- -# -# mime::setheader writes, appends to, or deletes the value associated -# with a key in the header. -# -# The value for -mode is one of: -# -# write: the key/value is either created or overwritten (the -# default); -# -# append: a new value is appended for the key (creating it as -# necessary); or, -# -# delete: all values associated with the key are removed (the -# "value" parameter is ignored). -# -# Regardless, mime::setheader returns the previous value associated -# with the key. -# -# Arguments: -# token The MIME token to parse. -# key The name of the key whose value should be set. -# value The value for the header key to be set to. -# args An optional argument of the form: -# ?-mode "write" | "append" | "delete"? -# -# Results: -# Returns previous value associated with the specified key. - -proc ::mime::setheader {token key value args} { - # FRINK: nocheck - variable internal - variable $token - upvar 0 $token state - - array set options [list -mode write] - array set options $args - - set lower [string tolower $key] - array set header $state(header) - if {[set x [lsearch -exact $state(lowerL) $lower]] < 0} { - #TODO: this code path is not tested - if {$options(-mode) eq {delete}} { - error "key $key not in header" - } - - lappend state(lowerL) $lower - lappend state(mixedL) $key - - set result {} - } else { - set result $header($lower) - } - switch -- $options(-mode) { - append - write { - if {!$internal} { - switch -- $lower { - content-md5 - - - content-type - - - content-transfer-encoding - - - mime-version { - set values [getheader $token $lower] - if {$value ni $values} { - error "key $key may not be set" - } - } - default {# Skip key} - } - } - switch -- $options(-mode) { - append { - lappend header($lower) $value - } - write { - set header($lower) [list $value] - } - } - } - delete { - unset header($lower) - set state(lowerL) [lreplace $state(lowerL) $x $x] - set state(mixedL) [lreplace $state(mixedL) $x $x] - } - - default { - error "unknown value for -mode $options(-mode)" - } - } - - set state(header) [array get header] - return $result -} - -# ::mime::getbody -- -# -# mime::getbody returns the body of a leaf MIME part in canonical form. -# -# If the -command option is present, then it is repeatedly invoked -# with a fragment of the body as this: -# -# uplevel #0 $callback [list "data" $fragment] -# -# (The -blocksize option, if present, specifies the maximum size of -# each fragment passed to the callback.) -# When the end of the body is reached, the callback is invoked as: -# -# uplevel #0 $callback "end" -# -# Alternatively, if an error occurs, the callback is invoked as: -# -# uplevel #0 $callback [list "error" reason] -# -# Regardless, the return value of the final invocation of the callback -# is propagated upwards by mime::getbody. -# -# If the -command option is absent, then the return value of -# mime::getbody is a string containing the MIME part's entire body. -# -# Arguments: -# token The MIME token to parse. -# args Optional arguments of the form: -# ?-decode? ?-command callback ?-blocksize octets? ? -# -# Results: -# Returns a string containing the MIME part's entire body, or -# if '-command' is specified, the return value of the command -# is returned. - -proc ::mime::getbody {token args} { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - - set decode 0 - if {[set pos [lsearch -exact $args -decode]] >= 0} { - set decode 1 - set args [lreplace $args $pos $pos] - } - - array set options [list -command [ - list mime::getbodyaux $token] -blocksize 4096] - array set options $args - if {$options(-blocksize) < 1} { - error "-blocksize expects a positive integer, not $options(-blocksize)" - } - - set code 0 - set ecode {} - set einfo {} - - switch -- $state(value)/$state(canonicalP) { - file/0 { - set fd [open $state(file) RDONLY] - - set code [catch { - fconfigure $fd -translation binary - seek $fd [set pos $state(offset)] start - set last [expr {$state(offset) + $state(count) - 1}] - - set fragment {} - while {$pos <= $last} { - if {[set cc [ - expr {($last - $pos) + 1}]] > $options(-blocksize)} { - set cc $options(-blocksize) - } - incr pos [set len [ - string length [set chunk [read $fd $cc]]]] - switch -exact -- $state(encoding) { - base64 - - - quoted-printable { - if {([set x [string last \n $chunk]] > 0) \ - && ($x + 1 != $len)} { - set chunk [string range $chunk 0 $x] - seek $fd [incr pos [expr {($x + 1) - $len}]] start - } - set chunk [ - $state(encoding) -mode decode -- $chunk] - } - 7bit - 8bit - binary - {} { - # Bugfix for [#477088] - # Go ahead, leave chunk alone - } - default { - error "Can't handle content encoding \"$state(encoding)\"" - } - } - append fragment $chunk - - set cc [expr {$options(-blocksize) - 1}] - while {[string length $fragment] > $options(-blocksize)} { - uplevel #0 $options(-command) [ - list data [string range $fragment 0 $cc]] - - set fragment [ - string range $fragment $options(-blocksize) end] - } - } - if {[string length $fragment] > 0} { - uplevel #0 $options(-command) [list data $fragment] - } - } result] - set ecode $errorCode - set einfo $errorInfo - - catch {close $fd} - } - - file/1 { - set fd [open $state(file) RDONLY] - - set code [catch { - fconfigure $fd -translation binary - - while {[string length [ - set fragment [read $fd $options(-blocksize)]]] > 0} { - uplevel #0 $options(-command) [list data $fragment] - } - } result] - set ecode $errorCode - set einfo $errorInfo - - catch {close $fd} - } - - parts/0 - - - parts/1 { - error {MIME part isn't a leaf} - } - - string/0 - - - string/1 { - switch -- $state(encoding)/$state(canonicalP) { - base64/0 - - - quoted-printable/0 { - set fragment [ - $state(encoding) -mode decode -- $state(string)] - } - - default { - # Not a bugfix for [#477088], but clarification - # This handles no-encoding, 7bit, 8bit, and binary. - set fragment $state(string) - } - } - - set code [catch { - set cc [expr {$options(-blocksize) -1}] - while {[string length $fragment] > $options(-blocksize)} { - uplevel #0 $options(-command) [ - list data [string range $fragment 0 $cc]] - - set fragment [ - string range $fragment $options(-blocksize) end] - } - if {[string length $fragment] > 0} { - uplevel #0 $options(-command) [list data $fragment] - } - } result] - set ecode $errorCode - set einfo $errorInfo - } - default { - error "Unknown combination \"$state(value)/$state(canonicalP)\"" - } - } - - set code [catch { - if {$code} { - uplevel #0 $options(-command) [list error $result] - } else { - uplevel #0 $options(-command) [list end] - } - } result] - set ecode $errorCode - set einfo $errorInfo - - if {$code} { - return -code $code -errorinfo $einfo -errorcode $ecode $result - } - - if {$decode} { - array set params [mime::getproperty $token params] - - if {[info exists params(charset)]} { - set charset $params(charset) - } else { - set charset US-ASCII - } - - set enc [reversemapencoding $charset] - if {$enc ne {}} { - set result [::encoding convertfrom $enc $result] - } else { - return -code error "-decode failed: can't reversemap charset $charset" - } - } - - return $result -} - -# ::mime::getbodyaux -- -# -# Builds up the body of the message, fragment by fragment. When -# the entire message has been retrieved, it is returned. -# -# Arguments: -# token The MIME token to parse. -# reason One of 'data', 'end', or 'error'. -# fragment The section of data data fragment to extract a -# string from. -# -# Results: -# Returns nothing, except when called with the 'end' argument -# in which case it returns a string that contains all of the -# data that 'getbodyaux' has been called with. Will throw an -# error if it is called with the reason of 'error'. - -proc ::mime::getbodyaux {token reason {fragment {}}} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - switch $reason { - data { - append state(getbody) $fragment - return {} - } - - end { - if {[info exists state(getbody)]} { - set result $state(getbody) - unset state(getbody) - } else { - set result {} - } - - return $result - } - - error { - catch {unset state(getbody)} - error $reason - } - - default { - error "Unknown reason \"$reason\"" - } - } -} - -# ::mime::copymessage -- -# -# mime::copymessage copies the MIME part to the specified channel. -# -# mime::copymessage operates synchronously, and uses fileevent to -# allow asynchronous operations to proceed independently. -# -# Arguments: -# token The MIME token to parse. -# channel The channel to copy the message to. -# -# Results: -# Returns nothing unless an error is thrown while the message -# is being written to the channel. - -proc ::mime::copymessage {token channel} { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - - set openP [info exists state(fd)] - - set code [catch {mime::copymessageaux $token $channel} result] - set ecode $errorCode - set einfo $errorInfo - - if {!$openP && [info exists state(fd)]} { - if {![info exists state(root)]} { - catch {close $state(fd)} - } - unset state(fd) - } - - return -code $code -errorinfo $einfo -errorcode $ecode $result -} - -# ::mime::copymessageaux -- -# -# mime::copymessageaux copies the MIME part to the specified channel. -# -# Arguments: -# token The MIME token to parse. -# channel The channel to copy the message to. -# -# Results: -# Returns nothing unless an error is thrown while the message -# is being written to the channel. - -proc ::mime::copymessageaux {token channel} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - array set header $state(header) - - set boundary {} - - set result {} - foreach {mixed value} [getheader $token] { - puts $channel "$mixed: $value" - } - - foreach {k v} $state(params) { - if {$k eq {boundary}} { - set boundary $v - } - } - - set converter {} - set encoding {} - if {$state(value) ne {parts}} { - if {$state(canonicalP)} { - if {[set encoding $state(encoding)] eq {}} { - set encoding [encoding $token] - } - if {$encoding ne {}} { - puts $channel "Content-Transfer-Encoding: $encoding" - } - switch -- $encoding { - base64 - - - quoted-printable { - set converter $encoding - } - 7bit - 8bit - binary - {} { - # Bugfix for [#477088], also [#539952] - # Go ahead - } - default { - error "Can't handle content encoding \"$encoding\"" - } - } - } - } elseif {([string match multipart/* $state(content)]) \ - && ($boundary eq {})} { - # we're doing everything in one pass... - set key [clock seconds]$token[info hostname][array get state] - set seqno 8 - while {[incr seqno -1] >= 0} { - set key [md5 -- $key] - } - set boundary "----- =_[string trim [base64 -mode encode -- $key]]" - - puts $channel ";\n boundary=\"$boundary\"" - } - - if {[info exists state(error)]} { - unset state(error) - } - - switch -- $state(value) { - file { - set closeP 1 - if {[info exists state(root)]} { - # FRINK: nocheck - variable $state(root) - upvar 0 $state(root) root - - if {[info exists root(fd)]} { - set fd $root(fd) - set closeP 0 - } else { - set fd [set state(fd) [open $state(file) RDONLY]] - } - set size $state(count) - } else { - set fd [set state(fd) [open $state(file) RDONLY]] - # read until eof - set size -1 - } - seek $fd $state(offset) start - if {$closeP} { - fconfigure $fd -translation binary - } - - puts $channel {} - - while {$size != 0 && ![eof $fd]} { - if {$size < 0 || $size > 32766} { - set X [read $fd 32766] - } else { - set X [read $fd $size] - } - if {$size > 0} { - set size [expr {$size - [string length $X]}] - } - if {$converter eq {}} { - puts -nonewline $channel $X - } else { - puts -nonewline $channel [$converter -mode encode -- $X] - } - } - - if {$closeP} { - catch {close $state(fd)} - unset state(fd) - } - } - - parts { - if { - ![info exists state(root)] - && - [info exists state(file)] - } { - set state(fd) [open $state(file) RDONLY] - fconfigure $state(fd) -translation binary - } - - switch -glob -- $state(content) { - message/* { - puts $channel {} - foreach part $state(parts) { - mime::copymessage $part $channel - break - } - } - - default { - # Note RFC 2046: See buildmessageaux for details. - # - # The boundary delimiter MUST occur at the - # beginning of a line, i.e., following a CRLF, and - # the initial CRLF is considered to be attached to - # the boundary delimiter line rather than part of - # the preceding part. - # - # - The above means that the CRLF before $boundary - # is needed per the RFC, and the parts must not - # have a closing CRLF of their own. See Tcllib bug - # 1213527, and patch 1254934 for the problems when - # both file/string branches added CRLF after the - # body parts. - - - foreach part $state(parts) { - puts $channel \n--$boundary - mime::copymessage $part $channel - } - puts $channel \n--$boundary-- - } - } - - if {[info exists state(fd)]} { - catch {close $state(fd)} - unset state(fd) - } - } - - string { - if {[catch {fconfigure $channel -buffersize} blocksize]} { - set blocksize 4096 - } elseif {$blocksize < 512} { - set blocksize 512 - } - set blocksize [expr {($blocksize / 4) * 3}] - - # [893516] - fconfigure $channel -buffersize $blocksize - - puts $channel {} - - #TODO: tests don't cover these paths - if {$converter eq {}} { - puts -nonewline $channel $state(string) - } else { - puts -nonewline $channel [$converter -mode encode -- $state(string)] - } - } - default { - error "Unknown value \"$state(value)\"" - } - } - - flush $channel - - if {[info exists state(error)]} { - error $state(error) - } -} - -# ::mime::buildmessage -- -# -# Like copymessage, but produces a string rather than writing the message into a channel. -# -# Arguments: -# token The MIME token to parse. -# -# Results: -# The message. - -proc ::mime::buildmessage token { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - - set openP [info exists state(fd)] - - set code [catch {mime::buildmessageaux $token} result] - if {![info exists errorCode]} { - set ecode {} - } else { - set ecode $errorCode - } - set einfo $errorInfo - - if {!$openP && [info exists state(fd)]} { - if {![info exists state(root)]} { - catch {close $state(fd)} - } - unset state(fd) - } - - return -code $code -errorinfo $einfo -errorcode $ecode $result -} - - -proc ::mime::buildmessageaux token { - set chan [tcl::chan::memchan] - chan configure $chan -translation crlf - copymessageaux $token $chan - seek $chan 0 - chan configure $chan -translation binary - set res [read $chan] - close $chan - return $res -} - -# ::mime::encoding -- -# -# Determines how a token is encoded. -# -# Arguments: -# token The MIME token to parse. -# -# Results: -# Returns the encoding of the message (the null string, base64, -# or quoted-printable). - -proc ::mime::encoding {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - switch -glob -- $state(content) { - audio/* - - - image/* - - - video/* { - return base64 - } - - message/* - - - multipart/* { - return {} - } - default {# Skip} - } - - set asciiP 1 - set lineP 1 - switch -- $state(value) { - file { - set fd [open $state(file) RDONLY] - fconfigure $fd -translation binary - - while {[gets $fd line] >= 0} { - if {$asciiP} { - set asciiP [encodingasciiP $line] - } - if {$lineP} { - set lineP [encodinglineP $line] - } - if {(!$asciiP) && (!$lineP)} { - break - } - } - - catch {close $fd} - } - - parts { - return {} - } - - string { - foreach line [split $state(string) "\n"] { - if {$asciiP} { - set asciiP [encodingasciiP $line] - } - if {$lineP} { - set lineP [encodinglineP $line] - } - if {(!$asciiP) && (!$lineP)} { - break - } - } - } - default { - error "Unknown value \"$state(value)\"" - } - } - - switch -glob -- $state(content) { - text/* { - if {!$asciiP} { - #TODO: this path is not covered by tests - foreach {k v} $state(params) { - if {$k eq "charset"} { - set v [string tolower $v] - if {($v ne "us-ascii") \ - && (![string match {iso-8859-[1-8]} $v])} { - return base64 - } - - break - } - } - } - - if {!$lineP} { - return quoted-printable - } - } - - - default { - if {(!$asciiP) || (!$lineP)} { - return base64 - } - } - } - - return {} -} - -# ::mime::encodingasciiP -- -# -# Checks if a string is a pure ascii string, or if it has a non-standard -# form. -# -# Arguments: -# line The line to check. -# -# Results: -# Returns 1 if \r only occurs at the end of lines, and if all -# characters in the line are between the ASCII codes of 32 and 126. - -proc ::mime::encodingasciiP {line} { - foreach c [split $line {}] { - switch -- $c { - { } - \t - \r - \n { - } - - default { - binary scan $c c c - if {($c < 32) || ($c > 126)} { - return 0 - } - } - } - } - if { - [set r [string first \r $line]] < 0 - || - $r == {[string length $line] - 1} - } { - return 1 - } - - return 0 -} - -# ::mime::encodinglineP -- -# -# Checks if a string is a line is valid to be processed. -# -# Arguments: -# line The line to check. -# -# Results: -# Returns 1 the line is less than 76 characters long, the line -# contains more characters than just whitespace, the line does -# not start with a '.', and the line does not start with 'From '. - -proc ::mime::encodinglineP {line} { - if {([string length $line] > 76) \ - || ($line ne [string trimright $line]) \ - || ([string first . $line] == 0) \ - || ([string first {From } $line] == 0)} { - return 0 - } - - return 1 -} - -# ::mime::fcopy -- -# -# Appears to be unused. -# -# Arguments: -# -# Results: -# - -proc ::mime::fcopy {token count {error {}}} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {$error ne {}} { - set state(error) $error - } - set state(doneP) 1 -} - -# ::mime::scopy -- -# -# Copy a portion of the contents of a mime token to a channel. -# -# Arguments: -# token The token containing the data to copy. -# channel The channel to write the data to. -# offset The location in the string to start copying -# from. -# len The amount of data to write. -# blocksize The block size for the write operation. -# -# Results: -# The specified portion of the string in the mime token is -# copied to the specified channel. - -proc ::mime::scopy {token channel offset len blocksize} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {$len <= 0} { - set state(doneP) 1 - fileevent $channel writable {} - return - } - - if {[set cc $len] > $blocksize} { - set cc $blocksize - } - - if {[catch { - puts -nonewline $channel [ - string range $state(string) $offset [expr {$offset + $cc - 1}]] - fileevent $channel writable [ - list mime::scopy $token $channel [ - incr offset $cc] [incr len -$cc] $blocksize] - } result] - } { - set state(error) $result - set state(doneP) 1 - fileevent $channel writable {} - } - return -} - -# ::mime::qp_encode -- -# -# Tcl version of quote-printable encode -# -# Arguments: -# string The string to quote. -# encoded_word Boolean value to determine whether or not encoded words -# (RFC 2047) should be handled or not. (optional) -# -# Results: -# The properly quoted string is returned. - -proc ::mime::qp_encode {string {encoded_word 0} {no_softbreak 0}} { - # 8.1+ improved string manipulation routines used. - # Replace outlying characters, characters that would normally - # be munged by EBCDIC gateways, and special Tcl characters "[\]{} - # with =xx sequence - - if {$encoded_word} { - # Special processing for encoded words (RFC 2047) - set regexp {[\x00-\x08\x0B-\x1E\x21-\x24\x3D\x40\x5B-\x5E\x60\x7B-\xFF\x09\x5F\x3F]} - lappend mapChars { } _ - } else { - set regexp {[\x00-\x08\x0B-\x1E\x21-\x24\x3D\x40\x5B-\x5E\x60\x7B-\xFF]} - } - regsub -all -- $regexp $string {[format =%02X [scan "\\&" %c]]} string - - # Replace the format commands with their result - - set string [subst -novariables $string] - - # soft/hard newlines and other - # Funky cases for SMTP compatibility - lappend mapChars " \n" =20\n \t\n =09\n \n\.\n =2E\n "\nFrom " "\n=46rom " - - set string [string map $mapChars $string] - - # Break long lines - ugh - - # Implementation of FR #503336 - if {$no_softbreak} { - set result $string - } else { - set result {} - foreach line [split $string \n] { - while {[string length $line] > 72} { - set chunk [string range $line 0 72] - if {[regexp -- (=|=.)$ $chunk dummy end]} { - - # Don't break in the middle of a code - - set len [expr {72 - [string length $end]}] - set chunk [string range $line 0 $len] - incr len - set line [string range $line $len end] - } else { - set line [string range $line 73 end] - } - append result $chunk=\n - } - append result $line\n - } - - # Trim off last \n, since the above code has the side-effect - # of adding an extra \n to the encoded string and return the - # result. - set result [string range $result 0 end-1] - } - - # If the string ends in space or tab, replace with =xx - - set lastChar [string index $result end] - if {$lastChar eq { }} { - set result [string replace $result end end =20] - } elseif {$lastChar eq "\t"} { - set result [string replace $result end end =09] - } - - return $result -} - -# ::mime::qp_decode -- -# -# Tcl version of quote-printable decode -# -# Arguments: -# string The quoted-printable string to decode. -# encoded_word Boolean value to determine whether or not encoded words -# (RFC 2047) should be handled or not. (optional) -# -# Results: -# The decoded string is returned. - -proc ::mime::qp_decode {string {encoded_word 0}} { - # 8.1+ improved string manipulation routines used. - # Special processing for encoded words (RFC 2047) - - if {$encoded_word} { - # _ == \x20, even if SPACE occupies a different code position - set string [string map [list _ \u0020] $string] - } - - # smash the white-space at the ends of lines since that must've been - # generated by an MUA. - - regsub -all -- {[ \t]+\n} $string \n string - set string [string trimright $string " \t"] - - # Protect the backslash for later subst and - # smash soft newlines, has to occur after white-space smash - # and any encoded word modification. - - #TODO: codepath not tested - set string [string map [list \\ {\\} =\n {}] $string] - - # Decode specials - - regsub -all -nocase {=([a-f0-9][a-f0-9])} $string {\\u00\1} string - - # process \u unicode mapped chars - - return [subst -novariables -nocommands $string] -} - -# ::mime::parseaddress -- -# -# This was originally written circa 1982 in C. we're still using it -# because it recognizes virtually every buggy address syntax ever -# generated! -# -# mime::parseaddress takes a string containing one or more 822-style -# address specifications and returns a list of dictionaries, for each -# address specified in the argument. -# -# Each dictionary contains these properties: -# -# property value -# ======== ===== -# address local@domain -# comment 822-style comment -# domain the domain part (rhs) -# error non-empty on a parse error -# group this address begins a group -# friendly user-friendly rendering -# local the local part (lhs) -# memberP this address belongs to a group -# phrase the phrase part -# proper 822-style address specification -# route 822-style route specification (obsolete) -# -# Note that one or more of these properties may be empty. -# -# Arguments: -# string The address string to parse -# -# Results: -# Returns a list of dictionaries, one element for each address -# specified in the argument. - -proc ::mime::parseaddress {string} { - global errorCode errorInfo - - variable mime - - set token [namespace current]::[incr mime(uid)] - # FRINK: nocheck - variable $token - upvar 0 $token state - - set code [catch {mime::parseaddressaux $token $string} result] - set ecode $errorCode - set einfo $errorInfo - - foreach name [array names state] { - unset state($name) - } - # FRINK: nocheck - catch {unset $token} - - return -code $code -errorinfo $einfo -errorcode $ecode $result -} - -# ::mime::parseaddressaux -- -# -# This was originally written circa 1982 in C. we're still using it -# because it recognizes virtually every buggy address syntax ever -# generated! -# -# mime::parseaddressaux does the actually parsing for mime::parseaddress -# -# Each dictionary contains these properties: -# -# property value -# ======== ===== -# address local@domain -# comment 822-style comment -# domain the domain part (rhs) -# error non-empty on a parse error -# group this address begins a group -# friendly user-friendly rendering -# local the local part (lhs) -# memberP this address belongs to a group -# phrase the phrase part -# proper 822-style address specification -# route 822-style route specification (obsolete) -# -# Note that one or more of these properties may be empty. -# -# Arguments: -# token The MIME token to work from. -# string The address string to parse -# -# Results: -# Returns a list of dictionaries, one for each address specified in the -# argument. - -proc ::mime::parseaddressaux {token string} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - variable addrtokenL - variable addrlexemeL - - set state(input) $string - set state(glevel) 0 - set state(buffer) {} - set state(lastC) LX_END - set state(tokenL) $addrtokenL - set state(lexemeL) $addrlexemeL - - set result {} - while {[addr_next $token]} { - if {[set tail $state(domain)] ne {}} { - set tail @$state(domain) - } else { - set tail @[info hostname] - } - if {[set address $state(local)] ne {}} { - #TODO: this path is not covered by tests - append address $tail - } - - if {$state(phrase) ne {}} { - #TODO: this path is not covered by tests - set state(phrase) [string trim $state(phrase) \"] - foreach t $state(tokenL) { - if {[string first $t $state(phrase)] >= 0} { - #TODO: is this quoting robust enough? - set state(phrase) \"$state(phrase)\" - break - } - } - - set proper "$state(phrase) <$address>" - } else { - set proper $address - } - - if {[set friendly $state(phrase)] eq {}} { - #TODO: this path is not covered by tests - if {[set note $state(comment)] ne {}} { - if {[string first ( $note] == 0} { - set note [string trimleft [string range $note 1 end]] - } - if { - [string last ) $note] - == [set len [expr {[string length $note] - 1}]] - } { - set note [string range $note 0 [expr {$len - 1}]] - } - set friendly $note - } - - if { - $friendly eq {} - && - [set mbox $state(local)] ne {} - } { - #TODO: this path is not covered by tests - set mbox [string trim $mbox \"] - - if {[string first / $mbox] != 0} { - set friendly $mbox - } elseif {[set friendly [addr_x400 $mbox PN]] ne {}} { - } elseif { - [set friendly [addr_x400 $mbox S]] ne {} - && - [set g [addr_x400 $mbox G]] ne {} - } { - set friendly "$g $friendly" - } - - if {$friendly eq {}} { - set friendly $mbox - } - } - } - set friendly [string trim $friendly \"] - - lappend result [list address $address \ - comment $state(comment) \ - domain $state(domain) \ - error $state(error) \ - friendly $friendly \ - group $state(group) \ - local $state(local) \ - memberP $state(memberP) \ - phrase $state(phrase) \ - proper $proper \ - route $state(route)] - - } - - unset {*}{ - state(input) - state(glevel) - state(buffer) - state(lastC) - state(tokenL) - state(lexemeL) - } - - return $result -} - -# ::mime::addr_next -- -# -# Locate the next address in a mime token. -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns 1 if there is another address, and 0 if there is not. - -proc ::mime::addr_next {token} { - global errorCode errorInfo - # FRINK: nocheck - variable $token - upvar 0 $token state - set nocomplain [package vsatisfies [package provide Tcl] 8.4] - foreach prop {comment domain error group local memberP phrase route} { - if {$nocomplain} { - unset -nocomplain state($prop) - } else { - if {[catch {unset state($prop)}]} {set ::errorInfo {}} - } - } - - switch -- [set code [catch {mime::addr_specification $token} result]] { - 0 { - if {!$result} { - return 0 - } - - switch -- $state(lastC) { - LX_COMMA - - - LX_END { - } - default { - # catch trailing comments... - set lookahead $state(input) - mime::parselexeme $token - set state(input) $lookahead - } - } - } - - 7 { - set state(error) $result - - while {1} { - switch -- $state(lastC) { - LX_COMMA - - - LX_END { - break - } - - default { - mime::parselexeme $token - } - } - } - } - - default { - set ecode $errorCode - set einfo $errorInfo - - return -code $code -errorinfo $einfo -errorcode $ecode $result - } - } - - foreach prop {comment domain error group local memberP phrase route} { - if {![info exists state($prop)]} { - set state($prop) {} - } - } - - return 1 -} - -# ::mime::addr_specification -- -# -# Uses lookahead parsing to determine whether there is another -# valid e-mail address or not. Throws errors if unrecognized -# or invalid e-mail address syntax is used. -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns 1 if there is another address, and 0 if there is not. - -proc ::mime::addr_specification {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - set lookahead $state(input) - switch -- [parselexeme $token] { - LX_ATOM - - - LX_QSTRING { - set state(phrase) $state(buffer) - } - - LX_SEMICOLON { - if {[incr state(glevel) -1] < 0} { - return -code 7 "extraneous semi-colon" - } - - catch {unset state(comment)} - return [addr_specification $token] - } - - LX_COMMA { - catch {unset state(comment)} - return [addr_specification $token] - } - - LX_END { - return 0 - } - - LX_LBRACKET { - return [addr_routeaddr $token] - } - - LX_ATSIGN { - set state(input) $lookahead - return [addr_routeaddr $token 0] - } - - default { - return -code 7 [ - format "unexpected character at beginning (found %s)" \ - $state(buffer)] - } - } - - switch -- [parselexeme $token] { - LX_ATOM - - - LX_QSTRING { - append state(phrase) " " $state(buffer) - - return [addr_phrase $token] - } - - LX_LBRACKET { - return [addr_routeaddr $token] - } - - LX_COLON { - return [addr_group $token] - } - - LX_DOT { - set state(local) "$state(phrase)$state(buffer)" - unset state(phrase) - mime::addr_routeaddr $token 0 - mime::addr_end $token - } - - LX_ATSIGN { - set state(memberP) $state(glevel) - set state(local) $state(phrase) - unset state(phrase) - mime::addr_domain $token - mime::addr_end $token - } - - LX_SEMICOLON - - - LX_COMMA - - - LX_END { - set state(memberP) $state(glevel) - if { - $state(lastC) eq "LX_SEMICOLON" - && - ([incr state(glevel) -1] < 0) - } { - #TODO: this path is not covered by tests - return -code 7 "extraneous semi-colon" - } - - set state(local) $state(phrase) - unset state(phrase) - } - - default { - return -code 7 [ - format "expecting mailbox (found %s)" $state(buffer)] - } - } - - return 1 -} - -# ::mime::addr_routeaddr -- -# -# Parses the domain portion of an e-mail address. Finds the '@' -# sign and then calls mime::addr_route to verify the domain. -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns 1 if there is another address, and 0 if there is not. - -proc ::mime::addr_routeaddr {token {checkP 1}} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - set lookahead $state(input) - if {[parselexeme $token] eq "LX_ATSIGN"} { - #TODO: this path is not covered by tests - mime::addr_route $token - } else { - set state(input) $lookahead - } - - mime::addr_local $token - - switch -- $state(lastC) { - LX_ATSIGN { - mime::addr_domain $token - } - - LX_SEMICOLON - - - LX_RBRACKET - - - LX_COMMA - - - LX_END { - } - - default { - return -code 7 [ - format "expecting at-sign after local-part (found %s)" \ - $state(buffer)] - } - } - - if {($checkP) && ($state(lastC) ne "LX_RBRACKET")} { - return -code 7 [ - format "expecting right-bracket (found %s)" $state(buffer)] - } - - return 1 -} - -# ::mime::addr_route -- -# -# Attempts to parse the portion of the e-mail address after the @. -# Tries to verify that the domain definition has a valid form. -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_route {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - set state(route) @ - - while 1 { - switch -- [parselexeme $token] { - LX_ATOM - - - LX_DLITERAL { - append state(route) $state(buffer) - } - - default { - return -code 7 \ - [format "expecting sub-route in route-part (found %s)" \ - $state(buffer)] - } - } - - switch -- [parselexeme $token] { - LX_COMMA { - append state(route) $state(buffer) - while 1 { - switch -- [parselexeme $token] { - LX_COMMA { - } - - LX_ATSIGN { - append state(route) $state(buffer) - break - } - - default { - return -code 7 \ - [format "expecting at-sign in route (found %s)" \ - $state(buffer)] - } - } - } - } - - LX_ATSIGN - - - LX_DOT { - append state(route) $state(buffer) - } - - LX_COLON { - append state(route) $state(buffer) - return - } - - default { - return -code 7 [ - format "expecting colon to terminate route (found %s)" \ - $state(buffer)] - } - } - } -} - -# ::mime::addr_domain -- -# -# Attempts to parse the portion of the e-mail address after the @. -# Tries to verify that the domain definition has a valid form. -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_domain {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - while 1 { - switch -- [parselexeme $token] { - LX_ATOM - - - LX_DLITERAL { - append state(domain) $state(buffer) - } - - default { - return -code 7 [ - format "expecting sub-domain in domain-part (found %s)" \ - $state(buffer)] - } - } - - switch -- [parselexeme $token] { - LX_DOT { - append state(domain) $state(buffer) - } - - LX_ATSIGN { - append state(local) % $state(domain) - unset state(domain) - } - - default { - return - } - } - } -} - -# ::mime::addr_local -- -# -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_local {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - set state(memberP) $state(glevel) - - while 1 { - switch -- [parselexeme $token] { - LX_ATOM - - - LX_QSTRING { - append state(local) $state(buffer) - } - - default { - return -code 7 \ - [format "expecting mailbox in local-part (found %s)" \ - $state(buffer)] - } - } - - switch -- [parselexeme $token] { - LX_DOT { - append state(local) $state(buffer) - } - - default { - return - } - } - } -} - -# ::mime::addr_phrase -- -# -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - - -proc ::mime::addr_phrase {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - while {1} { - switch -- [parselexeme $token] { - LX_ATOM - - - LX_QSTRING { - append state(phrase) " " $state(buffer) - } - - default { - break - } - } - } - - switch -- $state(lastC) { - LX_LBRACKET { - return [addr_routeaddr $token] - } - - LX_COLON { - return [addr_group $token] - } - - LX_DOT { - append state(phrase) $state(buffer) - return [addr_phrase $token] - } - - default { - return -code 7 [ - format "found phrase instead of mailbox (%s%s)" \ - $state(phrase) $state(buffer)] - } - } -} - -# ::mime::addr_group -- -# -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_group {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - if {[incr state(glevel)] > 1} { - return -code 7 [ - format "nested groups not allowed (found %s)" $state(phrase)] - } - - set state(group) $state(phrase) - unset state(phrase) - - set lookahead $state(input) - while 1 { - switch -- [parselexeme $token] { - LX_SEMICOLON - - - LX_END { - set state(glevel) 0 - return 1 - } - - LX_COMMA { - } - - default { - set state(input) $lookahead - return [addr_specification $token] - } - } - } -} - -# ::mime::addr_end -- -# -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_end {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - switch -- $state(lastC) { - LX_SEMICOLON { - if {[incr state(glevel) -1] < 0} { - return -code 7 "extraneous semi-colon" - } - } - - LX_COMMA - - - LX_END { - } - - default { - return -code 7 [ - format "junk after local@domain (found %s)" $state(buffer)] - } - } -} - -# ::mime::addr_x400 -- -# -# -# Arguments: -# token The MIME token to work from. -# -# Results: -# Returns nothing if successful, and throws an error if invalid -# syntax is found. - -proc ::mime::addr_x400 {mbox key} { - if {[set x [string first /$key= [string toupper $mbox]]] < 0} { - return {} - } - set mbox [string range $mbox [expr {$x + [string length $key] + 2}] end] - - if {[set x [string first / $mbox]] > 0} { - set mbox [string range $mbox 0 [expr {$x - 1}]] - } - - return [string trim $mbox \"] -} - -# ::mime::parsedatetime -- -# -# Fortunately the clock command in the Tcl 8.x core does all the heavy -# lifting for us (except for timezone calculations). -# -# mime::parsedatetime takes a string containing an 822-style date-time -# specification and returns the specified property. -# -# The list of properties and their ranges are: -# -# property range -# ======== ===== -# clock raw result of "clock scan" -# hour 0 .. 23 -# lmonth January, February, ..., December -# lweekday Sunday, Monday, ... Saturday -# mday 1 .. 31 -# min 0 .. 59 -# mon 1 .. 12 -# month Jan, Feb, ..., Dec -# proper 822-style date-time specification -# rclock elapsed seconds between then and now -# sec 0 .. 59 -# wday 0 .. 6 (Sun .. Mon) -# weekday Sun, Mon, ..., Sat -# yday 1 .. 366 -# year 1900 ... -# zone -720 .. 720 (minutes east of GMT) -# -# Arguments: -# value Either a 822-style date-time specification or '-now' -# if the current date/time should be used. -# property The property (from the list above) to return -# -# Results: -# Returns the string value of the 'property' for the date/time that was -# specified in 'value'. - -namespace eval ::mime { - variable WDAYS_SHORT [list Sun Mon Tue Wed Thu Fri Sat] - variable WDAYS_LONG [list Sunday Monday Tuesday Wednesday Thursday \ - Friday Saturday] - - # Counting months starts at 1, so just insert a dummy element - # at index 0. - variable MONTHS_SHORT [list {} \ - Jan Feb Mar Apr May Jun \ - Jul Aug Sep Oct Nov Dec] - variable MONTHS_LONG [list {} \ - January February March April May June July \ - August Sepember October November December] -} -proc ::mime::parsedatetime {value property} { - if {$value eq "-now"} { - set clock [clock seconds] - } elseif {[regexp {^(.*) ([+-])([0-9][0-9])([0-9][0-9])$} $value \ - -> value zone_sign zone_hour zone_min] - } { - set clock [clock scan $value -gmt 1] - if {[info exists zone_min]} { - set zone_min [scan $zone_min %d] - set zone_hour [scan $zone_hour %d] - set zone [expr {60 * ($zone_min + 60 * $zone_hour)}] - if {$zone_sign eq "+"} { - set zone -$zone - } - incr clock $zone - } - } else { - set clock [clock scan $value] - } - - switch -- $property { - clock { - return $clock - } - - hour { - set value [clock format $clock -format %H] - } - - lmonth { - variable MONTHS_LONG - return [lindex $MONTHS_LONG \ - [scan [clock format $clock -format %m] %d]] - } - - lweekday { - variable WDAYS_LONG - return [lindex $WDAYS_LONG [clock format $clock -format %w]] - } - - mday { - set value [clock format $clock -format %d] - } - - min { - set value [clock format $clock -format %M] - } - - mon { - set value [clock format $clock -format %m] - } - - month { - variable MONTHS_SHORT - return [lindex $MONTHS_SHORT [ - scan [clock format $clock -format %m] %d]] - } - - proper { - set gmt [clock format $clock -format "%Y-%m-%d %H:%M:%S" -gmt true] - if {[set diff [expr {($clock-[clock scan $gmt]) / 60}]] < 0} { - set s - - set diff [expr {-($diff)}] - } else { - set s + - } - set zone [format %s%02d%02d $s [ - expr {$diff / 60}] [expr {$diff % 60}]] - - variable WDAYS_SHORT - set wday [lindex $WDAYS_SHORT [clock format $clock -format %w]] - variable MONTHS_SHORT - set mon [lindex $MONTHS_SHORT [ - scan [clock format $clock -format %m] %d]] - - return [ - clock format $clock -format "$wday, %d $mon %Y %H:%M:%S $zone"] - } - - rclock { - #TODO: these paths are not covered by tests - if {$value eq "-now"} { - return 0 - } else { - return [expr {[clock seconds] - $clock}] - } - } - - sec { - set value [clock format $clock -format %S] - } - - wday { - return [clock format $clock -format %w] - } - - weekday { - variable WDAYS_SHORT - return [lindex $WDAYS_SHORT [clock format $clock -format %w]] - } - - yday { - set value [clock format $clock -format %j] - } - - year { - set value [clock format $clock -format %Y] - } - - zone { - set value [string trim [string map [list \t { }] $value]] - if {[set x [string last { } $value]] < 0} { - return 0 - } - set value [string range $value [expr {$x + 1}] end] - switch -- [set s [string index $value 0]] { - + - - { - if {$s eq "+"} { - #TODO: This path is not covered by tests - set s {} - } - set value [string trim [string range $value 1 end]] - if {( - [string length $value] != 4) - || - [scan $value %2d%2d h m] != 2 - || - $h > 12 - || - $m > 59 - || - ($h == 12 && $m > 0) - } { - error "malformed timezone-specification: $value" - } - set value $s[expr {$h * 60 + $m}] - } - - default { - set value [string toupper $value] - set z1 [list UT GMT EST EDT CST CDT MST MDT PST PDT] - set z2 [list 0 0 -5 -4 -6 -5 -7 -6 -8 -7] - if {[set x [lsearch -exact $z1 $value]] < 0} { - error "unrecognized timezone-mnemonic: $value" - } - set value [expr {[lindex $z2 $x] * 60}] - } - } - } - - date2gmt - - - date2local - - - dst - - - sday - - - szone - - - tzone - - - default { - error "unknown property $property" - } - } - - if {[set value [string trimleft $value 0]] eq {}} { - #TODO: this path is not covered by tests - set value 0 - } - return $value -} - -# ::mime::uniqueID -- -# -# Used to generate a 'globally unique identifier' for the content-id. -# The id is built from the pid, the current time, the hostname, and -# a counter that is incremented each time a message is sent. -# -# Arguments: -# -# Results: -# Returns the a string that contains the globally unique identifier -# that should be used for the Content-ID of an e-mail message. - -proc ::mime::uniqueID {} { - variable mime - - return <[pid].[clock seconds].[incr mime(cid)]@[info hostname]> -} - -# ::mime::parselexeme -- -# -# Used to implement a lookahead parser. -# -# Arguments: -# token The MIME token to operate on. -# -# Results: -# Returns the next token found by the parser. - -proc ::mime::parselexeme {token} { - # FRINK: nocheck - variable $token - upvar 0 $token state - - set state(input) [string trimleft $state(input)] - - set state(buffer) {} - if {$state(input) eq {}} { - set state(buffer) end-of-input - return [set state(lastC) LX_END] - } - - set c [string index $state(input) 0] - set state(input) [string range $state(input) 1 end] - - if {$c eq "("} { - set noteP 0 - set quoteP 0 - - while 1 { - append state(buffer) $c - - #TODO: some of these paths are not covered by tests - switch -- $c/$quoteP { - (/0 { - incr noteP - } - - \\/0 { - set quoteP 1 - } - - )/0 { - if {[incr noteP -1] < 1} { - if {[info exists state(comment)]} { - append state(comment) { } - } - append state(comment) $state(buffer) - - return [parselexeme $token] - } - } - - default { - set quoteP 0 - } - } - - if {[set c [string index $state(input) 0]] eq {}} { - set state(buffer) "end-of-input during comment" - return [set state(lastC) LX_ERR] - } - set state(input) [string range $state(input) 1 end] - } - } - - if {$c eq "\""} { - set firstP 1 - set quoteP 0 - - while 1 { - append state(buffer) $c - - switch -- $c/$quoteP { - "\\/0" { - set quoteP 1 - } - - "\"/0" { - if {!$firstP} { - return [set state(lastC) LX_QSTRING] - } - set firstP 0 - } - - default { - set quoteP 0 - } - } - - if {[set c [string index $state(input) 0]] eq {}} { - set state(buffer) "end-of-input during quoted-string" - return [set state(lastC) LX_ERR] - } - set state(input) [string range $state(input) 1 end] - } - } - - if {$c eq {[}} { - set quoteP 0 - - while 1 { - append state(buffer) $c - - switch -- $c/$quoteP { - \\/0 { - set quoteP 1 - } - - ]/0 { - return [set state(lastC) LX_DLITERAL] - } - - default { - set quoteP 0 - } - } - - if {[set c [string index $state(input) 0]] eq {}} { - set state(buffer) "end-of-input during domain-literal" - return [set state(lastC) LX_ERR] - } - set state(input) [string range $state(input) 1 end] - } - } - - if {[set x [lsearch -exact $state(tokenL) $c]] >= 0} { - append state(buffer) $c - - return [set state(lastC) [lindex $state(lexemeL) $x]] - } - - while 1 { - append state(buffer) $c - - switch -- [set c [string index $state(input) 0]] { - {} - " " - "\t" - "\n" { - break - } - - default { - if {[lsearch -exact $state(tokenL) $c] >= 0} { - break - } - } - } - - set state(input) [string range $state(input) 1 end] - } - - return [set state(lastC) LX_ATOM] -} - -# ::mime::mapencoding -- -# -# mime::mapencodings maps tcl encodings onto the proper names for their -# MIME charset type. This is only done for encodings whose charset types -# were known. The remaining encodings return {} for now. -# -# Arguments: -# enc The tcl encoding to map. -# -# Results: -# Returns the MIME charset type for the specified tcl encoding, or {} -# if none is known. - -proc ::mime::mapencoding {enc} { - - variable encodings - - if {[info exists encodings($enc)]} { - return $encodings($enc) - } - return {} -} - -# ::mime::reversemapencoding -- -# -# mime::reversemapencodings maps MIME charset types onto tcl encoding names. -# Those that are unknown return {}. -# -# Arguments: -# mimeType The MIME charset to convert into a tcl encoding type. -# -# Results: -# Returns the tcl encoding name for the specified mime charset, or {} -# if none is known. - -proc ::mime::reversemapencoding {mimeType} { - - variable reversemap - - set lmimeType [string tolower $mimeType] - if {[info exists reversemap($lmimeType)]} { - return $reversemap($lmimeType) - } - return {} -} - -# ::mime::word_encode -- -# -# Word encodes strings as per RFC 2047. -# -# Arguments: -# charset The character set to encode the message to. -# method The encoding method (base64 or quoted-printable). -# string The string to encode. -# ?-charset_encoded 0 or 1 Whether the data is already encoded -# in the specified charset (default 1) -# ?-maxlength maxlength The maximum length of each encoded -# word to return (default 66) -# -# Results: -# Returns a word encoded string. - -proc ::mime::word_encode {charset method string {args}} { - - variable encodings - - if {![info exists encodings($charset)]} { - error "unknown charset '$charset'" - } - - if {$encodings($charset) eq {}} { - error "invalid charset '$charset'" - } - - if {$method ne "base64" && $method ne "quoted-printable"} { - error "unknown method '$method', must be base64 or quoted-printable" - } - - # default to encoded and a length that won't make the Subject header to long - array set options [list -charset_encoded 1 -maxlength 66] - array set options $args - - if {$options(-charset_encoded)} { - set unencoded_string [::encoding convertfrom $charset $string] - } else { - set unencoded_string $string - } - - set string_length [string length $unencoded_string] - - if {!$string_length} { - return {} - } - - set string_bytelength [string bytelength $unencoded_string] - - # the 7 is for =?, ?Q?, ?= delimiters of the encoded word - set maxlength [expr {$options(-maxlength) - [string length $encodings($charset)] - 7}] - switch -exact -- $method { - base64 { - if {$maxlength < 4} { - error "maxlength $options(-maxlength) too short for chosen charset and encoding" - } - set count 0 - set maxlength [expr {($maxlength / 4) * 3}] - while {$count < $string_length} { - set length 0 - set enc_string {} - while {$length < $maxlength && $count < $string_length} { - set char [string range $unencoded_string $count $count] - set enc_char [::encoding convertto $charset $char] - if {$length + [string length $enc_char] > $maxlength} { - set length $maxlength - } else { - append enc_string $enc_char - incr count - incr length [string length $enc_char] - } - } - set encoded_word [string map [ - list \n {}] [base64 -mode encode -- $enc_string]] - append result "=?$encodings($charset)?B?$encoded_word?=\n " - } - # Trim off last "\n ", since the above code has the side-effect - # of adding an extra "\n " to the encoded string. - - set result [string range $result 0 end-2] - } - quoted-printable { - if {$maxlength < 1} { - error "maxlength $options(-maxlength) too short for chosen charset and encoding" - } - set count 0 - while {$count < $string_length} { - set length 0 - set encoded_word {} - while {$length < $maxlength && $count < $string_length} { - set char [string range $unencoded_string $count $count] - set enc_char [::encoding convertto $charset $char] - set qp_enc_char [qp_encode $enc_char 1] - set qp_enc_char_length [string length $qp_enc_char] - if {$qp_enc_char_length > $maxlength} { - error "maxlength $options(-maxlength) too short for chosen charset and encoding" - } - if { - $length + [string length $qp_enc_char] > $maxlength - } { - set length $maxlength - } else { - append encoded_word $qp_enc_char - incr count - incr length [string length $qp_enc_char] - } - } - append result "=?$encodings($charset)?Q?$encoded_word?=\n " - } - # Trim off last "\n ", since the above code has the side-effect - # of adding an extra "\n " to the encoded string. - - set result [string range $result 0 end-2] - } - {} { - # Go ahead - } - default { - error "Can't handle content encoding \"$method\"" - } - } - return $result -} - -# ::mime::word_decode -- -# -# Word decodes strings that have been word encoded as per RFC 2047. -# -# Arguments: -# encoded The word encoded string to decode. -# -# Results: -# Returns the string that has been decoded from the encoded message. - -proc ::mime::word_decode {encoded} { - - variable reversemap - - if {[regexp -- {=\?([^?]+)\?(.)\?([^?]*)\?=} $encoded \ - - charset method string] != 1 - } { - error "malformed word-encoded expression '$encoded'" - } - - set enc [reversemapencoding $charset] - if {$enc eq {}} { - error "unknown charset '$charset'" - } - - switch -exact -- $method { - b - - B { - set method base64 - } - q - - Q { - set method quoted-printable - } - default { - error "unknown method '$method', must be B or Q" - } - } - - switch -exact -- $method { - base64 { - set result [base64 -mode decode -- $string] - } - quoted-printable { - set result [qp_decode $string 1] - } - {} { - # Go ahead - } - default { - error "Can't handle content encoding \"$method\"" - } - } - - return [list $enc $method $result] -} - -# ::mime::field_decode -- -# -# Word decodes strings that have been word encoded as per RFC 2047 -# and converts the string from the original encoding/charset to UTF. -# -# Arguments: -# field The string to decode -# -# Results: -# Returns the decoded string in UTF. - -proc ::mime::field_decode {field} { - # ::mime::field_decode is broken. Here's a new version. - # This code is in the public domain. Don Libes - - # Step through a field for mime-encoded words, building a new - # version with unencoded equivalents. - - # Sorry about the grotesque regexp. Most of it is sensible. One - # notable fudge: the final $ is needed because of an apparent bug - # in the regexp engine where the preceding .* otherwise becomes - # non-greedy - perhaps because of the earlier ".*?", sigh. - - while {[regexp {(.*?)(=\?(?:[^?]+)\?(?:.)\?(?:[^?]*)\?=)(.*)$} $field \ - ignore prefix encoded field] - } { - # don't allow whitespace between encoded words per RFC 2047 - if {{} ne $prefix} { - if {![string is space $prefix]} { - append result $prefix - } - } - - set decoded [word_decode $encoded] - foreach {charset - string} $decoded break - - append result [::encoding convertfrom $charset $string] - } - append result $field - return $result -} - -## One-Shot Initialization - -::apply {{} { - variable encList - variable encAliasList - variable reversemap - - foreach {enc mimeType} $encList { - if {$mimeType eq {}} continue - set reversemap([string tolower $mimeType]) $enc - } - - foreach {enc mimeType} $encAliasList { - set reversemap([string tolower $mimeType]) $enc - } - - # Drop the helper variables - unset encList encAliasList - -} ::mime} - - -variable ::mime::internal 0 diff --git a/src/bootsupport/modules/oolib-0.1.2.tm b/src/bootsupport/modules/oolib-0.1.2.tm index af5da523..858c61cd 100644 --- a/src/bootsupport/modules/oolib-0.1.2.tm +++ b/src/bootsupport/modules/oolib-0.1.2.tm @@ -1,201 +1,201 @@ -#JMN - api should be kept in sync with package patternlib where possible -# -package provide oolib [namespace eval oolib { - variable version - set version 0.1.2 -}] - -namespace eval oolib { - oo::class create collection { - variable o_data ;#dict - #variable o_alias - constructor {} { - set o_data [dict create] - } - method info {} { - return [dict info $o_data] - } - method count {} { - return [dict size $o_data] - } - method isEmpty {} { - expr {[dict size $o_data] == 0} - } - method names {{globOrIdx {}}} { - if {[llength $globOrIdx]} { - if {[string is integer -strict $globOrIdx]} { - set idx $globOrIdx - if {$idx < 0} { - set idx "end-[expr {abs($idx + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $idx} result]} { - error "[self object] no such index : '$idx'" - } else { - return $result - } - } else { - #glob - return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] - } - } else { - return [dict keys $o_data] - } - } - #like names but without globbing - method keys {} { - dict keys $o_data - } - method key {{posn 0}} { - if {$posn < 0} { - set posn "end-[expr {abs($posn + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $posn} result]} { - error "[self object] no such index : '$posn'" - } else { - return $result - } - } - method hasKey {key} { - dict exists $o_data $key - } - method get {} { - return $o_data - } - method items {} { - return [dict values $o_data] - } - method item {key} { - if {[string is integer -strict $key]} { - if {$key >= 0} { - set valposn [expr {(2*$key) +1}] - return [lindex $o_data $valposn] - } else { - set key "end-[expr {abs($key + 1)}]" - return [lindex $o_data $key] - #return [lindex [dict keys $o_data] $key] - } - } - if {[dict exists $o_data $key]} { - return [dict get $o_data $key] - } - } - #inverse lookup - method itemKeys {value} { - set value_indices [lsearch -all [dict values $o_data] $value] - set keylist [list] - foreach i $value_indices { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - method search {value args} { - set matches [lsearch {*}$args [dict values $o_data] $value] - if {"-inline" in $args} { - return $matches - } else { - set keylist [list] - foreach i $matches { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - } - #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? - #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well? - #method alias {newAlias existingKeyOrAlias} { - # if {[string is integer -strict $newAlias]} { - # error "[self object] collection key alias cannot be integer" - # } - # if {[string length $existingKeyOrAlias]} { - # set o_alias($newAlias) $existingKeyOrAlias - # } else { - # unset o_alias($newAlias) - # } - #} - #method aliases {{key ""}} { - # if {[string length $key]} { - # set result [list] - # foreach {n v} [array get o_alias] { - # if {$v eq $key} { - # lappend result $n $v - # } - # } - # return $result - # } else { - # return [array get o_alias] - # } - #} - ##if the supplied index is an alias, return the underlying key; else return the index supplied. - #method realKey {idx} { - # if {[catch {set o_alias($idx)} key]} { - # return $idx - # } else { - # return $key - # } - #} - method add {value key} { - if {[string is integer -strict $key]} { - error "[self object] collection key must not be an integer. Use another structure if integer keys required" - } - if {[dict exists $o_data $key]} { - error "[self object] col_processors object error: key '$key' already exists in collection" - } - dict set o_data $key $value - return [expr {[dict size $o_data] - 1}] ;#return index of item - } - method remove {idx {endRange ""}} { - if {[string length $endRange]} { - error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" - } - if {[string is integer -strict $idx]} { - if {$idx < 0} { - set idx "end-[expr {abs($idx+1)}]" - } - set key [lindex [dict keys $o_data] $idx] - set posn $idx - } else { - set key $idx - set posn [lsearch -exact [dict keys $o_data] $key] - if {$posn < 0} { - error "[self object] no such index: '$idx' in this collection" - } - } - dict unset o_data $key - return - } - method clear {} { - set o_data [dict create] - return - } - method reverse_the_collection {} { - #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs - #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy. - #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations. - set dictnew [dict create] - foreach k [lreverse [dict keys $o_data]] { - dict set dictnew $k [dict get $o_data $k] - } - set o_data $dictnew - return - } - #review - cmd as list vs cmd as script? - method map {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list {*}$cmd $v]] - } - return $seed - } - method objectmap {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list $v {*}$cmd]] - } - return $seed - } - } - -} - +#JMN - api should be kept in sync with package patternlib where possible +# +package provide oolib [namespace eval oolib { + variable version + set version 0.1.2 +}] + +namespace eval oolib { + oo::class create collection { + variable o_data ;#dict + #variable o_alias + constructor {} { + set o_data [dict create] + } + method info {} { + return [dict info $o_data] + } + method count {} { + return [dict size $o_data] + } + method isEmpty {} { + expr {[dict size $o_data] == 0} + } + method names {{globOrIdx {}}} { + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + set idx $globOrIdx + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "[self object] no such index : '$idx'" + } else { + return $result + } + } else { + #glob + return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] + } + } else { + return [dict keys $o_data] + } + } + #like names but without globbing + method keys {} { + dict keys $o_data + } + method key {{posn 0}} { + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "[self object] no such index : '$posn'" + } else { + return $result + } + } + method hasKey {key} { + dict exists $o_data $key + } + method get {} { + return $o_data + } + method items {} { + return [dict values $o_data] + } + method item {key} { + if {[string is integer -strict $key]} { + if {$key >= 0} { + set valposn [expr {(2*$key) +1}] + return [lindex $o_data $valposn] + } else { + set key "end-[expr {abs($key + 1)}]" + return [lindex $o_data $key] + #return [lindex [dict keys $o_data] $key] + } + } + if {[dict exists $o_data $key]} { + return [dict get $o_data $key] + } + } + #inverse lookup + method itemKeys {value} { + set value_indices [lsearch -all [dict values $o_data] $value] + set keylist [list] + foreach i $value_indices { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + method search {value args} { + set matches [lsearch {*}$args [dict values $o_data] $value] + if {"-inline" in $args} { + return $matches + } else { + set keylist [list] + foreach i $matches { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + } + #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? + #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well? + #method alias {newAlias existingKeyOrAlias} { + # if {[string is integer -strict $newAlias]} { + # error "[self object] collection key alias cannot be integer" + # } + # if {[string length $existingKeyOrAlias]} { + # set o_alias($newAlias) $existingKeyOrAlias + # } else { + # unset o_alias($newAlias) + # } + #} + #method aliases {{key ""}} { + # if {[string length $key]} { + # set result [list] + # foreach {n v} [array get o_alias] { + # if {$v eq $key} { + # lappend result $n $v + # } + # } + # return $result + # } else { + # return [array get o_alias] + # } + #} + ##if the supplied index is an alias, return the underlying key; else return the index supplied. + #method realKey {idx} { + # if {[catch {set o_alias($idx)} key]} { + # return $idx + # } else { + # return $key + # } + #} + method add {value key} { + if {[string is integer -strict $key]} { + error "[self object] collection key must not be an integer. Use another structure if integer keys required" + } + if {[dict exists $o_data $key]} { + error "[self object] col_processors object error: key '$key' already exists in collection" + } + dict set o_data $key $value + return [expr {[dict size $o_data] - 1}] ;#return index of item + } + method remove {idx {endRange ""}} { + if {[string length $endRange]} { + error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" + } + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx+1)}]" + } + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + error "[self object] no such index: '$idx' in this collection" + } + } + dict unset o_data $key + return + } + method clear {} { + set o_data [dict create] + return + } + method reverse_the_collection {} { + #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs + #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy. + #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations. + set dictnew [dict create] + foreach k [lreverse [dict keys $o_data]] { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + return + } + #review - cmd as list vs cmd as script? + method map {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + return $seed + } + method objectmap {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + return $seed + } + } + +} + diff --git a/src/bootsupport/modules/oolib-0.1.tm b/src/bootsupport/modules/oolib-0.1.tm deleted file mode 100644 index 3756fceb..00000000 --- a/src/bootsupport/modules/oolib-0.1.tm +++ /dev/null @@ -1,195 +0,0 @@ -#JMN - api should be kept in sync with package patternlib where possible -# -package provide oolib [namespace eval oolib { - variable version - set version 0.1 -}] - -namespace eval oolib { - oo::class create collection { - variable o_data ;#dict - variable o_alias - constructor {} { - set o_data [dict create] - } - method info {} { - return [dict info $o_data] - } - method count {} { - return [dict size $o_data] - } - method isEmpty {} { - expr {[dict size $o_data] == 0} - } - method names {{globOrIdx {}}} { - if {[llength $globOrIdx]} { - if {[string is integer -strict $globOrIdx]} { - if {$idx < 0} { - set idx "end-[expr {abs($idx + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $idx} result]} { - error "[self object] no such index : '$idx'" - } else { - return $result - } - } else { - #glob - return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] - } - } else { - return [dict keys $o_data] - } - } - #like names but without globbing - method keys {} { - dict keys $o_data - } - method key {{posn 0}} { - if {$posn < 0} { - set posn "end-[expr {abs($posn + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $posn} result]} { - error "[self object] no such index : '$posn'" - } else { - return $result - } - } - method hasKey {key} { - dict exists $o_data $key - } - method get {} { - return $o_data - } - method items {} { - return [dict values $o_data] - } - method item {key} { - if {[string is integer -strict $key]} { - if {$key > 0} { - set valposn [expr {(2*$key) +1}] - return [lindex $o_data $valposn] - } else { - set key "end-[expr {abs($key + 1)}]" - return [lindex [dict keys $o_data] $key] - } - } - if {[dict exists $o_data $key]} { - return [dict get $o_data $key] - } - } - #inverse lookup - method itemKeys {value} { - set value_indices [lsearch -all [dict values $o_data] $value] - set keylist [list] - foreach i $value_indices { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - method search {value args} { - set matches [lsearch {*}$args [dict values $o_data] $value] - if {"-inline" in $args} { - return $matches - } else { - set keylist [list] - foreach i $matches { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - } - #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? - method alias {newAlias existingKeyOrAlias} { - if {[string is integer -strict $newAlias]} { - error "[self object] collection key alias cannot be integer" - } - if {[string length $existingKeyOrAlias]} { - set o_alias($newAlias) $existingKeyOrAlias - } else { - unset o_alias($newAlias) - } - } - method aliases {{key ""}} { - if {[string length $key]} { - set result [list] - foreach {n v} [array get o_alias] { - if {$v eq $key} { - lappend result $n $v - } - } - return $result - } else { - return [array get o_alias] - } - } - #if the supplied index is an alias, return the underlying key; else return the index supplied. - method realKey {idx} { - if {[catch {set o_alias($idx)} key]} { - return $idx - } else { - return $key - } - } - method add {value key} { - if {[string is integer -strict $key]} { - error "[self object] collection key must not be an integer. Use another structure if integer keys required" - } - if {[dict exists $o_data $key]} { - error "[self object] col_processors object error: key '$key' already exists in collection" - } - dict set o_data $key $value - return [expr {[dict size $o_data] - 1}] ;#return index of item - } - method remove {idx {endRange ""}} { - if {[string length $endRange]} { - error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" - } - if {[string is integer -strict $idx]} { - if {$idx < 0} { - set idx "end-[expr {abs($idx+1)}]" - } - set key [lindex [dict keys $o_data] $idx] - set posn $idx - } else { - set key $idx - set posn [lsearch -exact [dict keys $o_data] $key] - if {$posn < 0} { - error "[self object] no such index: '$idx' in this collection" - } - } - dict unset o_data $key - return - } - method clear {} { - set o_data [dict create] - return - } - method reverse {} { - set dictnew [dict create] - foreach k [lreverse [dict keys $o_data]] { - dict set dictnew $k [dict get $o_data $k] - } - set o_data $dictnew - return - } - #review - cmd as list vs cmd as script? - method map {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list {*}$cmd $v]] - } - return $seed - } - method objectmap {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list $v {*}$cmd]] - } - return $seed - } - } - -} - diff --git a/src/bootsupport/modules/overtype-1.6.1.tm b/src/bootsupport/modules/overtype-1.6.1.tm deleted file mode 100644 index 91ed77ec..00000000 --- a/src/bootsupport/modules/overtype-1.6.1.tm +++ /dev/null @@ -1,3399 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.6.1 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.1] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -package require punk::assertion -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace import ::punk::assertion::assert - punk::assertion::active true - - namespace path ::punk::lib - - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable grapheme_widths [dict create] - - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::ansifreestring_width $text] -} - -#todo - consider a way to merge overtype::left/centre/right -#These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock -#overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay. -#(i.e not even necessariy having it's top left within the underlay) -namespace eval overtype::priv { -} - -#could return larger than colwidth -proc _get_row_append_column {row} { - upvar outputlines outputlines - set idx [expr {$row -1}] - if {$row <= 1 || $row > [llength $outputlines]} { - return 1 - } else { - upvar opt_overflow opt_overflow - upvar colwidth colwidth - set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]] - set endpos [expr {$existinglen +1}] - if {$opt_overflow} { - return $endpos - } else { - if {$endpos > $colwidth} { - return $colwidth + 1 - } else { - return $endpos - } - } - } -} -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -#render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string. -#The underlay and overlay can be multiline blocks of text of varying line lengths. -#The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text. -#This is a single-shot rendering of strings - ie there is no way to chain another call containing a cursor-restore to previously rendered output and have it know about any cursor-saves in the first call. -# a cursor start position other than top-left is a possible addition to consider. -#see editbuf in punk::repl for a more stateful ansi-processor. Both systems use loops over overtype::renderline -proc overtype::left {args} { - #*** !doctools - #[call [fun overtype::left] [arg args] ] - #[para] usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext - - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -width \uFFEF\ - -height \uFFEF\ - -wrap 0\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -appendlines 1\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -experimental 0\ - -looplimit \uFFEF\ - ] - #-ellipsis args not used if -wrap is true - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -looplimit - -width - -height - -bias - -wrap - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -appendlines - -transparent - -exposed1 - -exposed2 - -experimental {} - default { - set known_opts [dict keys $defaults] - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - ##### - # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. - set opt_wrap [dict get $opts -wrap] ;#wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) - ##### - #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. - set opt_width [dict get $opts -width] - set opt_height [dict get $opts -height] - set opt_appendlines [dict get $opts -appendlines] - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - # ---------------------------- - # -experimental dev flag to set flags etc - # ---------------------------- - set data_mode 0 - set test_mode 1 - set info_mode 0 - set edit_mode 0 - set opt_experimental [dict get $opts -experimental] - foreach o $opt_experimental { - switch -- $o { - test_mode { - set test_mode 1 - set info_mode 1 - } - old_mode { - set test_mode 0 - set info_mode 1 - } - data_mode { - set data_mode 1 - } - info_mode { - set info_mode 1 - } - edit_mode { - set edit_mode 1 - } - } - } - # ---------------------------- - - #modes - set insert_mode 0 ;#can be toggled by insert key or ansi IRM sequence ESC [ 4 h|l - set autowrap_mode $opt_wrap - set reverse_mode 0 - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - - #set underlines [split $underblock \n] - - #underblock is a 'rendered' block - so width height make sense - #colwidth & colheight were originally named with reference to rendering into a 'column' of output e.g a table column - before cursor row/col was implemented. - #The naming is now confusing. It should be something like renderwidth renderheight ?? review - - if {$opt_width eq "\uFFEF"} { - lassign [blocksize $underblock] _w colwidth _h colheight - } else { - set colwidth $opt_width - set colheight $opt_height - } - if {$underblock eq ""} { - set blank "\x1b\[0m\x1b\[0m" - #set underlines [list "\x1b\[0m\x1b\[0m"] - set underlines [lrepeat $colheight $blank] - } else { - set underlines [lines_as_list -ansiresets 1 $underblock] - } - - #todo - reconsider the 'line' as the natural chunking mechanism for the overlay. - #In practice an overlay ANSI stream can be a single line with ansi moves/restores etc - or even have no moves or newlines, just relying on wrapping at the output colwidth - #In such cases - we process the whole shebazzle for the first output line - only reducing by the applied amount at the head each time, reprocessing the long tail each time. - #(in cases where there are interline moves or cursor jumps anyway) - #This works - but doesn't seem efficient. - #On the other hand.. maybe it depends on the data. For simpler files it's more efficient than splitting first - - #a hack until we work out how to avoid infinite loops... - # - set looplimit [dict get $opts -looplimit] - if {$looplimit eq "\uFFEF"} { - #looping for each char is worst case (all newlines?) - anything over that is an indication of something broken? - #do we need any margin above the length? (telnet mapscii.me test) - set looplimit [expr {[string length $overblock] + 10}] - } - - if {!$test_mode} { - set inputchunks [split $overblock \n] - } else { - set scheme 3 - switch -- $scheme { - 0 { - #one big chunk - set inputchunks [list $overblock] - } - 1 { - set inputchunks [punk::ansi::ta::split_codes $overblock] - } - 2 { - - #split into lines if possible first - then into plaintext/ansi-sequence chunks ? - set inputchunks [list ""] ;#put an empty plaintext split in for starters - set i 1 - set lines [split $overblock \n] - foreach ln $lines { - if {$i < [llength $lines]} { - append ln \n - } - set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single? - set lastpt [lindex $inputchunks end] - lset inputchunks end [string cat $lastpt [lindex $sequence_split 0]] - lappend inputchunks {*}[lrange $sequence_split 1 end] - incr i - } - } - 3 { - #it turns out line based chunks are faster than the above.. probably because some of those end up doing the regex splitting twice - set lflines [list] - set inputchunks [split $overblock \n] - foreach ln $inputchunks { - append ln \n - lappend lflines $ln - } - if {[llength $lflines]} { - lset lflines end [string range [lindex $lflines end] 0 end-1] - } - set inputchunks $lflines[unset lflines] - - } - } - } - - - #overblock height/width isn't useful in the presence of an ansi input overlay with movements. The number of lines may bear little relationship to the output height - #lassign [blocksize $overblock] _w overblock_width _h overblock_height - - - set replay_codes_underlay [dict create 1 ""] - #lappend replay_codes_overlay "" - set replay_codes_overlay "" - set unapplied "" - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - - - set outputlines $underlines - set overidx 0 - - #underlines are not necessarily processed in order - depending on cursor-moves applied from overtext - set row 1 - if {$data_mode} { - set col [_get_row_append_column $row] - } else { - set col 1 - } - - set instruction_stats [dict create] - - set loop 0 - #while {$overidx < [llength $inputchunks]} { } - - while {[llength $inputchunks]} { - #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx "" - set overtext [lpop inputchunks 0] - if {![string length $overtext]} { - incr loop - continue - } - #puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----" - set undertext [lindex $outputlines [expr {$row -1}]] - set renderedrow $row - - #renderline pads each underaly line to width with spaces and should track where end of data is - - - #set overtext [string cat [lindex $replay_codes_overlay $overidx] $overtext] - set overtext [string cat $replay_codes_overlay $overtext] - if {[dict exists $replay_codes_underlay $row]} { - set undertext [string cat [dict get $replay_codes_underlay $row] $undertext] - } - #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary - - #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l - set LASTCALL [list -info 1 -insert_mode $insert_mode -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set rinfo [renderline -experimental $opt_experimental -info 1 -insert_mode $insert_mode -cursor_restore_attributes $cursor_saved_attributes -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set instruction [dict get $rinfo instruction] - set insert_mode [dict get $rinfo insert_mode] - set autowrap_mode [dict get $rinfo autowrap_mode] ;# - #set reverse_mode [dict get $rinfo reverse_mode];#how to support in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set overflow_right_column [dict get $rinfo overflow_right_column] - set unapplied [dict get $rinfo unapplied] - set unapplied_list [dict get $rinfo unapplied_list] - set post_render_col [dict get $rinfo cursor_column] - set post_render_row [dict get $rinfo cursor_row] - set c_saved_pos [dict get $rinfo cursor_saved_position] - set c_saved_attributes [dict get $rinfo cursor_saved_attributes] - set visualwidth [dict get $rinfo visualwidth] - set insert_lines_above [dict get $rinfo insert_lines_above] - set insert_lines_below [dict get $rinfo insert_lines_below] - dict set replay_codes_underlay [expr {$renderedrow+1}] [dict get $rinfo replay_codes_underlay] - #lset replay_codes_overlay [expr $overidx+1] [dict get $rinfo replay_codes_overlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - - - - #-- todo - detect looping properly - if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} { - puts stderr "overtype::left loop?" - puts [ansistring VIEW $rinfo] - break - } - #-- - - if {[dict size $c_saved_pos] >= 1} { - set cursor_saved_position $c_saved_pos - set cursor_saved_attributes $c_saved_attributes - } - - - set overflow_handled 0 - - - - set nextprefix "" - - - #todo - handle potential insertion mode as above for cursor restore? - #keeping separate branches for debugging - review and merge as appropriate when stable - dict incr instruction_stats $instruction - switch -- $instruction { - {} { - if {$test_mode == 0} { - incr row - if {$data_mode} { - set col [_get_row_append_column $row] - if {$col > $colwidth} { - - } - } else { - set col 1 - } - } else { - #lf included in data - set row $post_render_row - set col $post_render_col - - #set col 1 - #if {$post_render_row != $renderedrow} { - # set col 1 - #} else { - # set col $post_render_col - #} - } - } - up { - - #renderline knows it's own line number, and knows not to go above row l - #it knows that a move whilst 1-beyond the width conflicts with the linefeed and reduces the move by one accordingly. - #row returned should be correct. - #column may be the overflow column - as it likes to report that to the caller. - - #Note that an ansi up sequence after last column going up to a previous line and also beyond the last column, will result in the next grapheme going onto the following line. - #this seems correct - as the column remains beyond the right margin so subsequent chars wrap (?) review - #puts stderr "up $post_render_row" - #puts stderr "$rinfo" - - #puts stdout "1 row:$row col $col" - set row $post_render_row - #data_mode (naming?) determines if we move to end of existing data or not. - #data_mode 0 means ignore existing line length and go to exact column - #set by -experimental flag - if {$data_mode == 0} { - set col $post_render_col - } else { - #This doesn't really work if columns are pre-filled with spaces..we can't distinguish them from data - #we need renderline to return the number of the maximum column filled (or min if we ever do r-to-l) - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - - #puts stdout "2 row:$row col $col" - #puts stdout "-----------------------" - #puts stdout $rinfo - #flush stdout - } - down { - if {$data_mode == 0} { - #renderline doesn't know how far down we can go.. - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set row $post_render_row - set col $post_render_col - } else { - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - - } - } - restore_cursor { - #testfile belinda.ans uses this - - #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" - if {[dict exists $cursor_saved_position row]} { - set row [dict get $cursor_saved_position row] - set col [dict get $cursor_saved_position column] - #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]" - #set nextprefix $cursor_saved_attributes - #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes - set replay_codes_overlay [dict get $rinfo replay_codes_overlay]$cursor_saved_attributes - #set replay_codes_overlay $cursor_saved_attributes - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - } else { - #TODO - #?restore without save? - #should move to home position and reset ansi SGR? - #puts stderr "overtype::left cursor_restore without save data available" - } - #If we were inserting prior to hitting the cursor_restore - there could be overflow_right data - generally the overtype functions aren't for inserting - but ansi can enable it - #if we were already in overflow when cursor_restore was hit - it shouldn't have been processed as an action - just stored. - if {!$overflow_handled && $overflow_right ne ""} { - #wrap before restore? - possible effect on saved cursor position - #this overflow data has previously been rendered so has no cursor movements or further save/restore operations etc - #we can just insert another call to renderline to solve this.. ? - #It would perhaps be more properly handled as a queue of instructions from our initial renderline call - #we don't need to worry about overflow next call (?)- but we should carry forward our gx and ansi stacks - - puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]" - - set sub_info [overtype::renderline -info 1 -width $colwidth -insert_mode $insert_mode -autowrap_mode $autowrap_mode -overflow [dict get $opts -overflow] "" $overflow_right] - set foldline [dict get $sub_info result] - set insert_mode [dict get $sub_info insert_mode] ;#probably not needed.. - set autowrap_mode [dict get $sub_info autowrap_mode] ;#nor this.. - linsert outputlines $renderedrow $foldline - #review - row & col set by restore - but not if there was no save.. - } - set overflow_handled 1 - - } - move { - ######## - if {$post_render_row > [llength $outputlines]} { - #Ansi moves need to create new lines ? - #if {$opt_appendlines} { - # set diff [expr {$post_render_row - [llength $outputlines]}] - # if {$diff > 0} { - # lappend outputlines {*}[lrepeat $diff ""] - # } - # set row $post_render_row - #} else { - set row [llength $outputlines] - #} - } else { - set row $post_render_row - } - ####### - set col $post_render_col - #overflow + unapplied? - } - lf_start { - #raw newlines - must be test_mode - # ---------------------- - #test with fruit.ans - #test - treating as newline below... - #append rendered $overflow_right - #set overflow_right "" - set row $renderedrow - incr row - if {$row > [llength $outputlines]} { - lappend outputlines "" - } - set col 1 - # ---------------------- - } - lf_mid { - - if 0 { - #set rhswidth [punk::ansi::printing_length $overflow_right] - #only show debug when we have overflow? - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - - set rhs "" - if {$overflow_right ne ""} { - set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $overflow_right]] - set rhs [textblock::frame -title overflow_right $rhs] - } - puts [textblock::join $lhs " $post_render_col " $rhs] - } - - if {!$test_mode} { - #rendered - append rendered $overflow_right - #set replay_codes_overlay "" - set overflow_right "" - - - set row $renderedrow - - set col 1 - incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - set edit_mode 0 - if {$edit_mode} { - set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied] - set overflow_right "" - set unapplied "" - set row $post_render_row - #set col $post_render_col - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - append rendered $overflow_right - set overflow_right "" - set row $post_render_row - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } - } - } - lf_overflow { - #linefeed after colwidth e.g at column 81 for an 80 col width - #we may also have other control sequences that came after col 80 e.g cursor save - - if 0 { - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - set rhs "" - - #assertion - there should be no overflow.. - puts $lhs - } - assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right - - set row $post_render_row - #set row $renderedrow - #incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - set col 1 - - } - newlines_above { - #we get a newlines_above instruction when received at column 1 - #In some cases we want to treat that as request to insert a new blank line above, and move our row 1 down (staying with the data) - #in other cases - we want to treat at column 1 the same as any other - - puts "--->newlines_above" - puts "rinfo: $rinfo" - #renderline doesn't advance the row for us - the caller has the choice to implement or not - set row $post_render_row - set col $post_render_col - if {$insert_lines_above > 0} { - set row $renderedrow - set outputlines [linsert $outputlines $renderedrow-1 {*}[lrepeat $insert_lines_above ""]] - incr row [expr {$insert_lines_above -1}] ;#we should end up on the same line of text (at a different index), with new empties inserted above - #? set row $post_render_row #can renderline tell us? - } - } - newlines_below { - #obsolete? - use for ANSI insert lines sequence - if {$data_mode == 0} { - puts --->nl_below - set row $post_render_row - set col $post_render_col - if {$insert_lines_below == 1} { - if {$test_mode == 0} { - set row $renderedrow - set outputlines [linsert $outputlines [expr {$renderedrow }] {*}[lrepeat $insert_lines_below ""]] ;#note - linsert can add to end too - incr row $insert_lines_below - set col 1 - } else { - #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]] - #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs] - #set rhs "" - #if {$overflow_right ne ""} { - # set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $overflow_right]] - # set rhs [textblock::frame -title overflow_right $rhs] - #} - #puts [textblock::join $lhs $rhs] - - #rendered - append rendered $overflow_right - # - - - set overflow_right "" - set row $renderedrow - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat $insert_lines_below ""] - } - incr row $insert_lines_below - set col 1 - - - - } - } - } else { - set row $post_render_row - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } else { - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - } - } - wrapmoveforward { - #doesn't seem to be used by fruit.ans testfile - #used by dzds.ans - #note that cursor_forward may move deep into the next line - or even span multiple lines !TODO - set c $colwidth - set r $post_render_row - if {$post_render_col > $colwidth} { - set i $c - while {$i <= $post_render_col} { - if {$c == $colwidth+1} { - incr r - if {$opt_appendlines} { - if {$r < [llength $outputlines]} { - lappend outputlines "" - } - } - set c 1 - } else { - incr c - } - incr i - } - set col $c - } else { - #why are we getting this instruction then? - puts stderr "wrapmoveforward - test" - set r [expr {$post_render_row +1}] - set c $post_render_col - } - set row $r - set col $c - } - wrapmovebackward { - set c $colwidth - set r $post_render_row - if {$post_render_col < 1} { - set c 1 - set i $c - while {$i >= $post_render_col} { - if {$c == 0} { - if {$r > 1} { - incr r -1 - set c $colwidth - } else { - #leave r at 1 set c 1 - #testfile besthpav.ans first line top left border alignment - set c 1 - break - } - } else { - incr c -1 - } - incr i -1 - } - set col $c - } else { - puts stderr "Wrapmovebackward - but postrendercol >= 1???" - } - set row $r - set col $c - } - overflow { - #normal single-width grapheme overflow - #puts "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]" - set row $post_render_row ;#renderline will not advance row when reporting overflow char - if {$autowrap_mode} { - incr row - set col 1 ;#whether wrap or not - next data is at column 1 ?? - } else { - #this works for test_mode (which should become the default) - but could give a bad result otherwise - review - add tests fix. - set col $post_render_col - #set unapplied "" ;#this seems wrong? - #set unapplied [string range $unapplied 1 end] - #The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs - #There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate - #We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow' - set idx 0 - set next_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set next_grapheme_index $idx - break - } - incr idx - } - assert {$next_grapheme_index >= 0} - #drop the overflow grapheme - keeping all codes in place. - set unapplied [join [lreplace $unapplied_list $next_grapheme_index $next_grapheme_index] ""] - #we need to run the reduced unapplied on the same line - further graphemes will just overflow again, but codes or control chars could trigger jumps to other lines - - set overflow_handled 1 - #handled by dropping overflow if any - } - } - overflow_splitchar { - set row $post_render_row ;#renderline will not advance row when reporting overflow char - - #2nd half of grapheme would overflow - treggering grapheme is returned in unapplied. There may also be overflow_right from earlier inserts - #todo - consider various options .. re-render a single trailing space or placeholder on same output line, etc - if {$autowrap_mode} { - if {$colwidth < 2} { - #edge case of rendering to a single column output - any 2w char will just cause a loop if we don't substitute with something, or drop the character - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } else { - set col 1 - incr row - } - } else { - set overflow_handled 1 - #handled by dropping entire overflow if any - if {$colwidth < 2} { - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } - } - - } - vt { - - #can vt add a line like a linefeed can? - set row $post_render_row - set col $post_render_col - } - default { - puts stderr "overtype::left unhandled renderline instruction '$instruction'" - } - - } - - - if {!$opt_overflow && !$autowrap_mode} { - #not allowed to overflow column or wrap therefore we get overflow data to truncate - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim [ansistrip $lostdata]] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - set overflow_handled 1 - } else { - #no wrap - no ellipsis - silently truncate - set overflow_handled 1 - } - } - - - - if {$renderedrow <= [llength $outputlines]} { - lset outputlines [expr {$renderedrow-1}] $rendered - } else { - if {$opt_appendlines} { - lappend outputlines $rendered - } else { - #? - lset outputlines [expr {$renderedrow-1}] $rendered - } - } - - if {!$overflow_handled} { - append nextprefix $overflow_right - } - - append nextprefix $unapplied - - if 0 { - if {$nextprefix ne ""} { - set nextoveridx [expr {$overidx+1}] - if {$nextoveridx >= [llength $inputchunks]} { - lappend inputchunks $nextprefix - } else { - #lset overlines $nextoveridx $nextprefix[lindex $overlines $nextoveridx] - set inputchunks [linsert $inputchunks $nextoveridx $nextprefix] - } - } - } - - if {$nextprefix ne ""} { - set inputchunks [linsert $inputchunks 0 $nextprefix] - } - - - incr overidx - incr loop - if {$loop >= $looplimit} { - puts stderr "overtype::left looplimit reached ($looplimit)" - lappend outputlines "[a+ red bold] - looplimit $looplimit reached[a]" - set Y [a+ yellow bold] - set RST [a] - set sep_header ----DEBUG----- - set debugmsg "" - append debugmsg "${Y}${sep_header}${RST}" \n - append debugmsg "looplimit $looplimit reached\n" - append debugmsg "test_mode:$test_mode\n" - append debugmsg "data_mode:$data_mode\n" - append debugmsg "opt_appendlines:$opt_appendlines\n" - append debugmsg "prev_row :[dict get $LASTCALL -cursor_row]\n" - append debugmsg "prev_col :[dict get $LASTCALL -cursor_column]\n" - dict for {k v} $rinfo { - append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n - } - append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n - - puts stdout $debugmsg - #todo - config regarding error dumps rather than just dumping in working dir - set fd [open [pwd]/error_overtype.txt w] - puts $fd $debugmsg - close $fd - error $debugmsg - break - } - } - - set result [join $outputlines \n] - if {$info_mode} { - #emit to debug window like basictelnet does? make debug configurable as syslog or even a telnet server to allow on 2nd window? - #append result \n$instruction_stats\n - } - return $result -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] - #todo - get replay_codes from overflow_right instead of wherever it was truncated? - - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #don't use string range on ANSI data - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -align "left"\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_align [dict get $opts -align] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$colwidth - $overblock_width)}] - set left_exposed $under_exposed_max - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_align { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set ellipsis [string cat $replay_codes $opt_ellipsistext] - #todo - overflow on left if allign = right?? - set rendered [overtype::right $rendered $ellipsis] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#intended for single grapheme - but will work for multiple -#cannot contain ansi or newlines -#(a cache of ansifreestring_width calls - as these are quite regex heavy) -proc overtype::grapheme_width_cached {ch} { - variable grapheme_widths - if {[dict exists $grapheme_widths $ch]} { - return [dict get $grapheme_widths $ch] - } - set width [punk::char::ansifreestring_width $ch] - dict set grapheme_widths $ch $width - return $width -} - - - -# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### -# renderline written from a left-right line orientation perspective as a first-shot at getting something useful. -# ultimately right-to-left, top-to-bottom and bottom-to-top are probably needed. -# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### -# -# -#-returnextra enables returning of overflow and length -#review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#(could render it by faking it with sixels and a lot of work - find/make a sixel font and ensure it's exactly 2 cols per char) -#todo - review transparency issues with single/double width characters -#bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? -proc overtype::renderline {args} { - #*** !doctools - #[call [fun overtype::renderline] [arg args] ] - #[para] renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode commandline repl for the Tcl Punk Shell - #[para] It is also a central part of an ansi (micro) virtual terminal-emulator of sorts - #[para] This system does a half decent job at rendering 90's ANSI art to manipulable colour text blocks that can be joined & framed for layout display within a unix or windows terminal - #[para] Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't affect another. - #[para] Calling on the punk::ansi library - it can coalesce codes to keep the size down. - #[para] It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - #[para] renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a static underlay - #[para] The underlay is generally expected to be an ordered set of lines or a rectangular text block analogous to a terminal screen - but it can also be ragged in line length, or just blank. - #[para] The overlay couuld be similar - in which case it may often be used to overwrite a column or section of the underlay. - #[para] The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - # - #[para] renderline itself only deals with a single line - or sometimes a single character. It is generally called from a loop that does further terminal-like or textblock processing. - #[para] By suppyling the -info 1 option - it can return various fields indicating the state of the render. - #[para] The main 3 are the result, overflow_right, and unapplied. - #[para] Renderline handles cursor movements from either keystrokes or ANSI sequences but for a full system the aforementioned loop will need to be in place to manage the set of lines under manipulation. - - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines in undertext" - } - #if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - # error "overtype::renderline not allowed to contain newlines" - #} - - set defaults [dict create\ - -etabs 0\ - -width \uFFEF\ - -overflow 0\ - -transparent 0\ - -startcolumn 1\ - -cursor_column 1\ - -cursor_row ""\ - -insert_mode 1\ - -autowrap_mode 1\ - -reverse_mode 0\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -cursor_restore_attributes ""\ - -experimental {}\ - ] - #-cursor_restore_attributes only - for replay stack - position and actual setting/restoring handled by throwback to caller - - #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return - #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs - - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error - - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -experimental - -width - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -autowrap_mode - -reverse_mode - -info - -exposed1 - -exposed2 - -cursor_restore_attributes {} - default { - set known_opts [dict keys $defaults] - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_width [dict get $opts -width] - set opt_etabs [dict get $opts -etabs] - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [dict get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [dict get $opts -cursor_row] - if {[string length $opt_row_context]} { - if {![string is integer -strict $opt_row_context] || $opt_row_context <1 } { - error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode) - set opt_insert_mode [dict get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) - #default is for overtype - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_autowrap_mode [dict get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line - set opt_reverse_mode [dict get $opts -reverse_mode] ;#DECSNM - # -- --- --- --- --- --- --- --- --- --- --- --- - set temp_cursor_saved [dict get $opts -cursor_restore_attributes] - - set test_mode 0 - set cp437_glyphs 0 - foreach e [dict get $opts -experimental] { - switch -- $e { - test_mode { - set test_mode 1 - set cp437_glyphs 1 - } - } - } - set cp437_map [dict create] - if {$cp437_glyphs} { - set cp437_map [set ::punk::ansi::cp437_map] - #for cp437 images we need to map these *after* splitting ansi - #some old files might use newline for its glyph.. but we can't easily support that. - #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs? - dict unset cp437_map \n - } - - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - if {$opt_row_context eq ""} { - set cursor_row 1 - } else { - set cursor_row $opt_row_context - } - - - #----- - # - if {[info exists punk::console::tabwidth]} { - #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted - #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - - set overdata $over - if {!$cp437_glyphs} { - #REVIEW! tabify will give different answers for an ANSI colourised string vs plain text - if {!$opt_etabs} { - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under $tw] - } - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over $tw] - } - } - } - #------- - - #ta_detect ansi and do simpler processing? - - #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column, - #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway. - - # -- --- --- --- --- --- --- --- - if {$under ne ""} { - set undermap [punk::ansi::ta::split_codes_single $under] - } else { - set undermap [list] - } - set understacks [list] - set understacks_gx [list] - - set i_u -1 ;#underlay may legitimately be empty - set undercols [list] - set u_codestack [list] - #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway - set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics) - #set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - #append pt_underchars $pt - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. - #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - set width 1 - } - default { - if {$grapheme eq "\u0000"} { - #use null as empty cell representation - review - #use of this will probably collide with some application at some point - #consider an option to set the empty cell character - set width 1 - } else { - set width [grapheme_width_cached $grapheme] - #we still want most controls and other zero-length codepoints such as \u200d (zero width joiner) to stay zero-length - #we substitute lone ESC that weren't captured within ANSI context as a debugging aid to see malformed ANSI - #todo - default to off and add a flag (?) to enable this substitution - set sub_stray_escapes 0 - if {$sub_stray_escapes && $width == 0} { - if {$grapheme eq "\x1b"} { - set gvis [ansistring VIEW $grapheme] - set grapheme $gvis - set width 1 - } - } - } - } - } - #set width [grapheme_width_cached $grapheme] - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - - lappend undercols $grapheme - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - #todo - if -etabs enabled - then we treat \t as the width determined by our elastic tabstop - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - lappend undercols "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {$code ne ""} { - set c1c2 [string range $code 0 1] - set leadernorm [string range [string map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - switch -- $leadernorm { - 7CSI - 8CSI { - #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse - #REVIEW - what else could end in m but be mistaken as a normal SGR code here? - set maybemouse "" - if {[string index $c1c2 0] eq "\x1b"} { - set maybemouse [string index $code 2] - } - - if {$maybemouse ne "<" && [string index $code end] eq "m"} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - #basic simplification first.. straight dups - set dup_posns [lsearch -all -exact $u_codestack $code] ;#-exact because of square-bracket glob chars - set u_codestack [lremove $u_codestack {*}$dup_posns] - lappend u_codestack $code - } - } - } - 7GFX { - switch -- [string index $code 2] { - "0" { - set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess - } - B { - set u_gx_stack [list] - } - } - } - default { - - } - - } - - #if {[punk::ansi::codetype::is_sgr_reset $code]} { - # #set u_codestack [list] - #} elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #} elseif {[punk::ansi::codetype::is_sgr $code]} { - #} else { - # #leave SGR stack as is - # if {[punk::ansi::codetype::is_gx_open $code]} { - # } elseif {[punk::ansi::codetype::is_gx_close $code]} { - # } - #} - } - #consider also if there are other codes that should be stacked..? - } - - if {!$test_mode} { - #fill columns to width with spaces, and carry over stacks - we will have to keep track of where the underlying data ends manually - TODO - #Specifying a width is suitable for terminal-like applications and text-blocks - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff " "] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - } else { - #NULL empty cell indicator - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff "\u0000"] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - - } - if {$opt_width ne "\uFFEF"} { - set colwidth $opt_width - } else { - set colwidth [llength $undercols] - } - - - if 0 { - # ----------------- - # if we aren't extending understacks & understacks_gx each time we incr idx above the undercols length.. this doesn't really serve a purpose - # Review. - # ----------------- - #replay code for last overlay position in input line - # whether or not we get that far - we need to return it for possible replay on next line - if {[llength $understacks]} { - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - } else { - #in case overlay onto emptystring as underlay - lappend understacks [list] - lappend understacks_gx [list] - } - # ----------------- - } - - #trailing codes in effect for underlay - if {[llength $u_codestack]} { - #set replay_codes_underlay [join $u_codestack ""] - set replay_codes_underlay [punk::ansi::codetype::sgr_merge_list {*}$u_codestack] - } else { - set replay_codes_underlay "" - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char ?) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad_overlay [string repeat " " [expr {$opt_colstart -1}]] - append startpad_overlay $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] - #### - - #??? - set colcursor $opt_colstart - #TODO - make a little virtual column object - #we need to refer to column1 or columnmin? or columnmax without calculating offsets due to to startcolumn - #need to lock-down what start column means from perspective of ANSI codes moving around - the offset perspective is unclear and a mess. - - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - set overstacks [list] - set overstacks_gx [list] - - set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc) - set o_gxstack [list] - set pt_overchars "" - set i_o 0 - set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use - #experiment - set overlay_grapheme_control_stacks [list] - foreach {pt code} $overmap { - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - append pt_overchars $pt - #will get empty pt between adjacent codes - foreach grapheme [punk::char::grapheme_split $pt] { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - lappend overlay_grapheme_control_stacks $o_codestack - #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split) - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] ;#reset better than empty list - fixes some ansi art issues - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $o_codestack $code] ;#must be -exact because of square-bracket glob chars - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[regexp {\x1b7|\x1b\[s} $code]} { - #experiment - #cursor_save - for the replays review. - #jmn - #set temp_cursor_saved [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - lappend overlay_grapheme_control_list [list other $code] - } elseif {[regexp {\x1b8|\x1b\[u} $code]} { - #experiment - #cursor_restore - for the replays - set o_codestack [list $temp_cursor_saved] - lappend overlay_grapheme_control_list [list other $code] - } else { - if {[punk::ansi::codetype::is_gx_open $code]} { - set o_gxstack [list "gx0_on"] - lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set o_gxstack [list] - lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } else { - lappend overlay_grapheme_control_list [list other $code] - } - } - } - - } - #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme - set max_overlay_grapheme_index [expr {$i_o -1}] - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - - #set replay_codes_overlay [join $o_codestack ""] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - - #if {[dict exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [dict get $overstacks $max_overlay_grapheme_index] ""] - #} else { - # set replay_codes_overlay "" - #} - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - # -- --- --- - #we need to initialise overflow_idx before any potential row-movements - as they need to perform a loop break and force in_excess to 1 - if {$opt_overflow} { - #somewhat counterintuitively - overflow true means we can have lines as long as we want, but either way there can be excess data that needs to be thrown back to the calling loop. - set overflow_idx -1 - } else { - #overflow zero - we can't grow beyond our column width - so we get ellipsis or truncation - if {$opt_width ne "\uFFEF"} { - set overflow_idx [expr {$opt_width}] - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - } - # -- --- --- - - set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to. - - set unapplied "" ;#if we break for move row (but not for /v ?) - set unapplied_list [list] - - set insert_lines_above 0 ;#return key - set insert_lines_below 0 - set instruction "" - - # -- --- --- - #cursor_save_dec, cursor_restore_dec etc - set cursor_restore_required 0 - set cursor_saved_attributes "" - set cursor_saved_position "" - # -- --- --- - - #set idx 0 ;# line index (cursor - 1) - #set idx [expr {$opt_colstart + $opt_colcursor} -1] - - #idx is the per column output index - set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1 - #cursor_column is usually one above idx - but we have opt_colstart which is like a margin - todo: remove cursor_column from the following loop and calculate it's offset when breaking or at end. - #(for now we are incrementing/decrementing both in sync - which is a bit silly) - set cursor_column $opt_colcursor - - #idx_over is the per grapheme overlay index - set idx_over -1 - - - #movements only occur within the overlay range. - #an underlay is however not necessary.. e.g - #renderline -overflow 1 "" data - #foreach {pt code} $overmap {} - set insert_mode $opt_insert_mode ;#default 1 - set autowrap_mode $opt_autowrap_mode ;#default 1 - - - #puts "-->$overlay_grapheme_control_list<--" - #puts "-->overflow_idx: $overflow_idx" - for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} { - set gc [lindex $overlay_grapheme_control_list $gci] - lassign $gc type item - - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - #foreach ch $overlay_graphemes {} - switch -- $type { - g { - set ch $item - incr idx_over; #idx_over (until unapplied reached anyway) is per *grapheme* in the overlay - not per col. - if {($idx < ($opt_colstart -1))} { - incr idx [grapheme_width_cached $ch] - continue - } - #set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our active data width - set within_undercols [expr {$idx <= $colwidth-1}] - - #https://www.enigma.com/resources/blog/the-secret-world-of-newline-characters - #\x85 NEL in the c1 control set is treated by some terminal emulators (e.g Hyper) as a newline, - #on some it's invisble but doesn't change the line, on some it's a visible glyph of width 1. - #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable - #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE - - set chtest [string map [list \n \x85 \b \r \v \x7f ] $ch] - #puts --->chtest:$chtest - #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached - switch -- $chtest { - "" { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - if {$idx == 0} { - #puts "---a at col 1" - #linefeed at column 1 - #leave the overflow_idx ;#? review - set instruction lf_start ;#specific instruction for newline at column 1 - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } elseif {$overflow_idx != -1 && $idx == $overflow_idx} { - #linefeed after final column - #puts "---c at overflow_idx=$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } else { - #linefeed occurred in middle or at end of text - #puts "---mid-or-end-text-linefeed idx:$idx overflow_idx:$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_mid - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } - - } - "" { - #will we want/need to use raw for keypresses in terminal? (terminal with LNM in standard reset mode means enter= this is the usual config for terminals) - #So far we are assuming the caller has translated to and handle above.. REVIEW. - - #consider also the old space-carriagereturn softwrap convention used in some terminals. - #In the context of rendering to a block of text - this works similarly in that the space gets eaten so programs emitting space-cr at the terminal width col will pretty much get what they expect. - set idx [expr {$opt_colstart -1}] - set cursor_column $opt_colstart ;#? - } - "" { - #literal backspace char - not necessarily from keyboard - #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype - #(important for -transparent option - hence replacement chars for half-exposed etc) - #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR) - if {$idx > ($opt_colstart -1)} { - incr idx -1 - incr cursor_column -1 - } else { - set flag 0 - if $flag { - #review - conflicting requirements? Need a different sequence for destructive interactive backspace? - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction backspace_at_start - break - } - } - } - "" { - #literal del character - some terminals send just this for what is generally expected to be a destructive backspace - #We instead treat this as a pure delete at current cursor position - it is up to the repl or terminal to remap backspace key to a sequence that has the desired effect. - priv::render_delchar $idx - } - "" { - #end processing this overline. rest of line is remainder. cursor for column as is. - #REVIEW - this theoretically depends on terminal's vertical tabulation setting (name?) - #e.g it could be configured to jump down 6 rows. - #On the other hand I've seen indications that some modern terminal emulators treat it pretty much as a linefeed. - #todo? - incr cursor_row - set overflow_idx $idx - #idx_over has already been incremented as this is both a movement-control and in some sense a grapheme - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction vt - break - } - default { - if {$overflow_idx != -1} { - #review - how to check arbitrary length item such as tab is going to overflow .. before we get to overflow_idx? - #call grapheme_width_cached on each ch, or look for tab specifically as it's currently the only known reason to have a grapheme width > 2? - #we need to decide what a tab spanning the overflow_idx means and how it affects wrap etc etc - if {$idx == $overflow_idx-1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 2} { - #review split 2w overflow? - #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line - #better to consider the overlay char as unable to be applied to the line - #render empty string to column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied - #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #change the overflow_idx - set overflow_idx $idx - incr idx - incr idx_over -1 ;#set overlay grapheme index back one so that sgr stack from previous overlay grapheme used - priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#note $gci-1 instead of just gci - #throw back to caller's loop - add instruction to caller as this is not the usual case - #caller may for example choose to render a single replacement char to this line and omit the grapheme, or wrap it to the next line - set instruction overflow_splitchar - break - } elseif {$owidth > 2} { - #? tab? - #TODO! - puts stderr "overtype::renderline long overtext grapheme '[ansistring VIEW -lf 1 -vt 1 $ch]' not handled" - #tab of some length dependent on tabstops/elastic tabstop settings? - } - } elseif {$idx >= $overflow_idx} { - #jmn? - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci-1]] - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #don't incr idx beyond the overflow_idx - #idx_over already incremented - decrement so current overlay grapheme stacks go to unapplied - incr idx_over -1 - #priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#back one index here too - priv::render_this_unapplied $overlay_grapheme_control_list $gci ;# - set instruction overflow - break - } - } else { - #review. - #This corresponds to opt_overflow being true (at least until overflow_idx is in some cases forced to a value when throwing back to calling loop) - } - - if {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - #dict set understacks $idx [list] ;#review - use idx-1 codestack? - lset understacks $idx [list] - incr idx - incr cursor_column - } else { - #todo - punk::char::char_width - set g [lindex $outcols $idx] - set uwidth [grapheme_width_cached $g] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - incr cursor_column - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - incr cursor_column - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - incr idx - incr cursor_column - if {$owidth > 1} { - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - if {[grapheme_width_cached $ch] == 1} { - if {!$insert_mode} { - #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - incr cursor_column - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - incr cursor_column - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } else { - #? todo - decide what transparency even means for insert mode - incr idx - incr cursor_column - } - } else { - #2wide transparency over 2wide in underlay - review - incr idx - incr cursor_column - } - } - } - } else { - - set idxchar [lindex $outcols $idx] - #non-transparent char in overlay or empty cell - if {$idxchar eq "\u0000"} { - #empty/erased cell indicator - set uwidth 1 - } else { - set uwidth [grapheme_width_cached $idxchar] - } - if {$within_undercols} { - if {$idxchar eq ""} { - #2nd col of 2wide char in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??) - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 - } - incr idx - } else { - set prevcolinfo [lindex $outcols $idx-1] - #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right - #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?) - #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char - #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises - #It is perhaps best avoided at another level and try to make renderline do exactly as it's told - #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect. - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index - if {$prevcolinfo ne ""} { - #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 ;#replace not insert - } ;# else?? - incr idx - } - if {$cursor_column < [llength $outcols] || $overflow_idx == -1} { - incr cursor_column - } - } elseif {$uwidth == 0} { - #what if this is some other c0/c1 control we haven't handled specifically? - - #by emitting a preceding empty-string column - we associate whatever this char is with the preceeding non-zero-length character and any existing zero-lengths that follow it - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - #if we can get a proper grapheme_split function - this should be easier to tidy up. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column 2 - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } elseif {$uwidth == 1} { - #includes null empty cells - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {([llength $outcols] >= $idx +2) && [lindex $outcols $idx+1] eq ""} { - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks $idx+1] [lindex $understacks_gx $idx+1] $insert_mode - } - incr idx - } - if {($cursor_column < [llength $outcols]) || $overflow_idx == -1 || $test_mode} { - incr cursor_column - } - } elseif {$uwidth > 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - #1wide over 2wide in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #insert mode just pushes all to right - no exposition char here - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx 2 - incr cursor_column 2 - } - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - if {$overflow_idx !=-1 && !$test_mode} { - #overflow - if {$cursor_column > [llength $outcols]} { - set cursor_column [llength $outcols] - } - } - } - } - } - } ;# end switch - - - } - other { - set code $item - #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' - - set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM - set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - set re_row_move {\x1b\[([0-9]*)(A|B)$} - set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? - set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! - set re_cursor_restore {\x1b\[u$} - set re_cursor_save_dec {\x1b7$} - set re_cursor_restore_dec {\x1b8$} - set re_other_single {\x1b(D|M|E)$} - set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins - set matchinfo [list] - - #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI - #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping - #review - cost/benefit of function calls within these switch-arms instead of inline code? - - #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. - #we should probably therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore - set code [string map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $code] - - - set c1 [string index $code 0] - set c1c2c3 [string range $code 0 2] - #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} - set leadernorm [string range [string map [list\ - \x1b\[< 1006\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\] 7OSC\ - \x9d 8OSC\ - \x1b 7ESC\ - ] $c1c2c3] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - #we leave the tail of the code unmapped for now - switch -- $leadernorm { - 1006 { - #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html - #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release - set codenorm [string cat $leadernorm [string range $code 3 end]] - } - 7CSI - 7OSC { - set codenorm [string cat $leadernorm [string range $code 2 end]] - } - 7ESC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - 8CSI - 8OSC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - default { - #we haven't made a mapping for this - set codenorm $code - } - } - - #we've mapped 7 and 8bit escapes to values we can handle as literals in switch statements to take advantange of jump tables. - switch -- $leadernorm { - 1006 { - #TODO - # - switch -- [string index $codenorm end] { - M { - puts stderr "mousedown $codenorm" - } - m { - puts stderr "mouseup $codenorm" - } - } - - } - {7CSI} - {8CSI} { - set param [string range $codenorm 4 end-1] - #puts stdout "--> CSI [string index $leadernorm 0] bit param:$param" - switch -- [string index $codenorm end] { - D { - #Col move - #puts stdout "<-back" - #cursor back - #left-arrow/move-back when ltr mode - set num $param - if {$num eq ""} {set num 1} - - set version 2 - if {$version eq "2"} { - #todo - startcolumn offset! - if {$cursor_column - $num >= 1} { - incr idx -$num - incr cursor_column -$num - } else { - if {!$autowrap_mode} { - set cursor_column 1 - set idx 0 - } else { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr cursor_column -$num - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmovebackward - break - } - } - } else { - incr idx -$num - incr cursor_column -$num - if {$idx < $opt_colstart-1} { - #wrap to previous line and position cursor at end of data - set idx [expr {$opt_colstart-1}] - set cursor_column $opt_colstart - } - } - } - C { - #Col move - #puts stdout "->forward" - #todo - consider right-to-left cursor mode (e.g Hebrew).. some day. - #cursor forward - #right-arrow/move forward - set num $param - if {$num eq ""} {set num 1} - - #todo - retrict to moving 1 position past datalen? restrict to column width? - #should ideally wrap to next line when interactive and not on last row - #(some ansi art seems to expect this behaviour) - #This presumably depends on the terminal's wrap mode - #e.g DECAWM autowrap mode - # CSI ? 7 h - set: autowrap (also tput smam) - # CSI ? 7 l - reset: no autowrap (also tput rmam) - set version 2 - if {$version eq "2"} { - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$test_mode && $cursor_column == $max+1} { - #move_forward while in overflow - incr cursor_column -1 - } - - if {($cursor_column + $num) <= $max} { - incr idx $num - incr cursor_column $num - } else { - if {$autowrap_mode} { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #jmn - if {$idx == $overflow_idx} { - incr num - } - - #horizontal movement beyond line extent needs to wrap - throw back to caller - #we may have both overflow_rightand unapplied data - #(can have overflow_right if we were in insert_mode and processed chars prior to this movement) - #leave row as is - caller will need to determine how many rows the column-movement has consumed - incr cursor_column $num ;#give our caller the necessary info as columns from start of row - #incr idx_over - #should be gci following last one applied - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmoveforward - break - } else { - set cursor_column $max - set idx [expr {$cursor_column -1}] - } - } - } else { - if {!$opt_overflow || ($cursor_column + $num) <= [llength $outcols+1]} { - incr idx $num - incr cursor_column $num - } else { - if {!$insert_mode} { - #block editing style with arrow keys - #overtype mode - set idxstart $idx - set idxend [llength $outcols] - set moveend [expr {$idxend - $idxstart}] - if {$moveend < 0} {set moveend 0} ;#sanity? - #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" - incr idx $moveend - incr cursor_column $moveend - #if {[dict exists $understacks $idx]} { - # set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #} else { - # set stackinfo [list] - #} - if {$idx < [llength $understacks]} { - set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - } else { - set stackinfo [list] - } - if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [dict get $understacks_gx $idx] - set gxstackinfo [lindex $understacks_gx $idx] - } else { - set gxstackinfo [list] - } - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - incr cursor_column - priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode - } - } else { - #normal - insert - incr idx $num - incr cursor_column $num - if {$idx > [llength $outcols]} { - set idx [llength $outcols];#allow one beyond - for adding character at end of line - set cursor_column [expr {[llength $outcols]+1}] - } - } - } - } - } - G { - #Col move - #move absolute column - #adjust to colstart - as column 1 is within overlay - #??? - set idx [expr {$param + $opt_colstart -1}] - set cursor_column $param - error "renderline absolute col move ESC G unimplemented" - } - A { - #Row move - up - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set num $param - if {$num eq ""} {set num 1} - incr cursor_row -$num - - if {$cursor_row < 1} { - set cursor_row 1 - } - - #ensure rest of *overlay* is emitted to remainder - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up - #retain cursor_column - break - } - B { - #Row move - down - set num $param - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move down - if {$num eq ""} {set num 1} - incr cursor_row $num - - - incr idx_over ;#idx_over hasn't encountered a grapheme and hasn't advanced yet - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - H - f { - #$re_both_move - lassign [split $param {;}] row col - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #lassign $matchinfo _match row col - - if {$col eq ""} {set col 1} - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$col > $max} { - set cursor_column $max - } else { - set cursor_column $col - } - set idx [expr {$cursor_column -1}] - - if {$row eq ""} {set row 1} - set cursor_row $row - if {$cursor_row < 1} { - set cursor_row 1 - } - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - - } - X { - puts stderr "X - $param" - #ECH - erase character - if {$param eq "" || $param eq "0"} {set param 1}; #param=count of chars to erase - priv::render_erasechar $idx $param - #cursor position doesn't change. - } - r { - #$re_decstbm - #https://www.vt100.net/docs/vt510-rm/DECSTBM.html - #This control function sets the top and bottom margins for the current page. You cannot perform scrolling outside the margins - lassign [split $param {;}] margin_top margin_bottom - - #todo - return these for the caller to process.. - puts stderr "overtype::renderline DECSTBM set top and bottom margin not implemented" - #Also moves the cursor to col 1 line 1 of the page - set cursor_column 1 - set cursor_row 1 - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move ;#own instruction? decstbm? - break - } - s { - # - todo - make ansi.sys CSI s cursor save only apply for certain cases? - may need to support DECSLRM instead which uses same code - - #$re_cursor_save - #cursor save could come after last column - if {$overflow_idx != -1 && $idx == $overflow_idx} { - #bartman2.ans test file - fixes misalignment at bottom of dialog bubble - #incr cursor_row - #set cursor_column 1 - #bwings1.ans test file - breaks if we actually incr cursor (has repeated saves) - set cursor_saved_position [list row [expr {$cursor_row+1}] column 1] - } else { - set cursor_saved_position [list row $cursor_row column $cursor_column] - } - #there may be overlay stackable codes emitted that aren't in the understacks because they come between the last emmited character and the cursor_save control. - #we need the SGR and gx overlay codes prior to the cursor_save - - #a real terminal would not be able to know the state of the underlay.. so we should probably ignore it. - #set sgr_stack [lindex $understacks $idx] - #set gx_stack [lindex $understacks_gx $idx] ;#not actually a stack - just a boolean state (for now?) - - set sgr_stack [list] - set gx_stack [list] - - #we shouldn't need to scan for intermediate cursor save/restores - as restores would throw-back to the calling loop - so our overlay 'line' is since those. - #The overlay_grapheme_control_list had leading resets from previous lines - so we go back to the beginning not just the first grapheme. - - foreach gc [lrange $overlay_grapheme_control_list 0 $gci-1] { - lassign $gc type code - #types g other sgr gx0 - switch -- $type { - gx0 { - #code is actually a stand-in for the graphics on/off code - not the raw code - #It is either gx0_on or gx0_off - set gx_stack [list $code] - } - sgr { - #code is the raw code - if {[punk::ansi::codetype::is_sgr_reset $code]} { - #jmn - set sgr_stack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set sgr_stack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #often we don't get resets - and codes just pile up. - #as a first step to simplifying - at least remove earlier straight up dupes - set dup_posns [lsearch -all -exact $sgr_stack $code] ;#needs -exact - codes have square-brackets (glob chars) - set sgr_stack [lremove $sgr_stack {*}$dup_posns] - lappend sgr_stack $code - } - } - } - } - set cursor_saved_attributes "" - switch -- [lindex $gx_stack 0] { - gx0_on { - append cursor_saved_attributes "\x1b(0" - } - gx0_off { - append cursor_saved_attributes "\x1b(B" - } - } - #append cursor_saved_attributes [join $sgr_stack ""] - append cursor_saved_attributes [punk::ansi::codetype::sgr_merge_list {*}$sgr_stack] - - #as there is apparently only one cursor storage element we don't need to throw back to the calling loop for a save. - - #don't incr index - or the save will cause cursor to move to the right - #carry on - - } - u { - #$re_cursor_restore - #we are going to jump somewhere.. for now we will assume another line, and process accordingly. - #The caller has the cursor_saved_position/cursor_saved_attributes if any (?review - if we always pass it back it, we could save some calls for moves in same line) - #don't set overflow at this point. The existing underlay to the right must be preserved. - #we only want to jump and render the unapplied at the new location. - - #lset overstacks $idx_over [list] - #set replay_codes_overlay "" - - #if {$cursor_saved_attributes ne ""} { - # set replay_codes_overlay $cursor_saved_attributes ;#empty - or last save if it happend in this input chunk - #} else { - #jj - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set replay_codes_overlay "" - #} - - #like priv::render_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code - incr idx_over - - set unapplied "" - set unapplied_list [list] - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - #incr idx_over - } - set unapplied [join $unapplied_list ""] - #if the save occured within this line - that's ok - it's in the return value list and caller can prepend for the next loop. - set instruction restore_cursor - break - } - ~ { - #$re_vt_sequence - #lassign $matchinfo _match key mod - lassign [split $param {;}] key mod - - #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~ - # - #e.g esc \[2~ insert esc \[2;2~ shift-insert - #mod - subtract 1, and then use bitmask - #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?) - #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]" - if {$key eq "1"} { - #home - } elseif {$key eq "2"} { - #Insert - if {$mod eq ""} { - #no modifier key - set insert_mode [expr {!$insert_mode}] - #rather than set the cursor - we return the insert mode state so the caller can decide - } - } elseif {$key eq "3"} { - #Delete - presumably this shifts other chars in the line, with empty cells coming in from the end - switch -- $mod { - "" { - priv::render_delchar $idx - } - "5" { - #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?) - } - } - } elseif {$key eq "4"} { - #End - } elseif {$key eq "5"} { - #pgup - } elseif {$key eq "6"} { - #pgDn - } elseif {$key eq "7"} { - #Home - #?? - set idx [expr {$opt_colstart -1}] - set cursor_column 1 - } elseif {$key eq "8"} { - #End - } elseif {$key eq "11"} { - #F1 - or ESCOP or e.g shift F1 ESC\[1;2P - } elseif {$key eq "12"} { - #F2 - or ESCOQ - } elseif {$key eq "13"} { - #F3 - or ESCOR - } elseif {$key eq "14"} { - #F4 - or ESCOS - } elseif {$key eq "15"} { - #F5 or shift F5 ESC\[15;2~ - } elseif {$key eq "17"} { - #F6 - } elseif {$key eq "18"} { - #F7 - } elseif {$key eq "19"} { - #F8 - } elseif {$key eq "20"} { - #F9 - } elseif {$key eq "21"} { - #F10 - } elseif {$key eq "23"} { - #F11 - } elseif {$key eq "24"} { - #F12 - } - - } - h - l { - #we are matching only last char to get to this arm - but are there other sequences ending in h|l we need to handle? - - #$re_mode if first after CSI is "?" - #some docs mention ESC=h|l - not seen on windows terminals.. review - #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html - if {[string index $codenorm 4] eq "?"} { - set num [string range $codenorm 5 end-1] ;#param between ? and h|l - #lassign $matchinfo _match num type - switch -- $num { - 5 { - #DECSNM - reverse video - #How we simulate this to render within a block of text is an open question. - #track all SGR stacks and constantly flip based on the current SGR reverse state? - #It is the job of the calling loop to do this - so at this stage we'll just set the states - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set reverse_mode 1 - } else { - #reset (disable) - set reverse_mode 0 - } - - } - 7 { - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set autowrap_mode 1 - if {$opt_width ne "\uFFEF"} { - set overflow_idx $opt_width - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - #review - can idx ever be beyond overflow_idx limit when we change e.g with a width setting and cursor movements? presume not - but sanity check for now. - if {$idx >= $overflow_idx} { - puts stderr "renderline error - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected" - } - } else { - #reset (disable) - set autowrap_mode 0 - set overflow_idx -1 - } - } - 25 { - if {$type eq "h"} { - #visible cursor - - } else { - #invisible cursor - - } - } - } - - } else { - puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - default { - puts stderr "overtype::renderline CSI code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - 7ESC { - #$re_other_single - switch -- [string index $codenorm end] { - D { - #\x84 - #index (IND) - #vt102-docs: "Moves cursor down one line in same column. If cursor is at bottom margin, screen performs a scroll-up" - puts stderr "ESC D not fully implemented" - incr cursor_row - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - M { - #\x8D - #Reverse Index (RI) - #vt102-docs: "Moves cursor up one line in same column. If cursor is at top margin, screen performs a scroll-down" - puts stderr "ESC M not fully implemented" - - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move up - incr cursor_row -1 - if {$cursor_row < 1} { - set cursor_row 1 - } - #ensure rest of *overlay* is emitted to remainder - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up ;#need instruction for scroll-down? - #retain cursor_column - break - } - E { - #\x85 - #review - is behaviour different to lf? - #todo - possibly(?) same logic as handling above. i.e return instruction depends on where column_cursor is at the time we get NEL - #leave implementation until logic for is set in stone... still under review - #It's arguable NEL is a pure cursor movement as opposed to the semantic meaning of crlf or lf in a file. - # - #Next Line (NEL) "Move the cursor to the left margin on the next line. If the cursor is at the bottom margin, scroll the page up" - puts stderr "ESC E unimplemented" - - } - default { - puts stderr "overtype::renderline ESC code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - - } - } - - #switch -regexp -matchvar matchinfo -- $code\ - #$re_mode { - #}\ - #default { - # puts stderr "overtype::renderline code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - #} - - } - default { - #don't need to handle sgr or gx0 types - #we have our sgr gx0 codes already in stacks for each overlay grapheme - } - } - } - - #-------- - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - #set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - if {$overflow_idx == -1} { - #overflow was initially unlimited and hasn't been overridden - } else { - - } - #-------- - - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" ;#output prior to overflow - set overflow_right "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set prev_g0 [list] - #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves - set in_overflow 0 ;#used to stop char-width scanning once in overflow - if {$overflow_idx == 0} { - #how does caller avoid an infinite loop if they have autowrap on and keep throwing graphemes to the next line? REVIEW - set in_overflow 1 - } - foreach ch $outcols { - #puts "---- [ansistring VIEW $ch]" - - set gxleader "" - if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {$g0 ne $prev_g0} { - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } else { - set gxleader "\x1b(B" - } - } - set prev_g0 $g0 - } else { - set prev_g0 [list] - } - - set sgrleader "" - if {$i < [llength $understacks]} { - #set cstack [dict get $understacks $i] - set cstack [lindex $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack] && ![llength $cstack]} { - #This reset is important e.g testfile fruit.ans - we get overhang on rhs without it. But why is cstack empty? - append sgrleader \033\[m - } else { - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - } - } - set prevstack $cstack - } else { - set prevstack [list] - } - - - - if {$in_overflow} { - if {$i == $overflow_idx} { - set 0 [lindex $understacks_gx $i] - set gxleader "" - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } elseif {$g0 eq [list "gx0_off"]} { - set gxleader "\x1b(B" - } - append overflow_right $gxleader - set cstack [lindex $understacks $i] - set sgrleader "" - #whether cstack is same or differs from previous char's stack - we must have an output at the start of the overflow_right - #if {[llength $prevstack] && ![llength $cstack]} { - # append sgrleader \033\[m - #} - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - append overflow_right $sgrleader - append overflow_right $ch - } else { - append overflow_right $gxleader - append overflow_right $sgrleader - append overflow_right $ch - } - } else { - if {$overflow_idx != -1 && $i+1 == $overflow_idx} { - #one before overflow - #will be in overflow in next iteration - set in_overflow 1 - if {[grapheme_width_cached $ch]> 1} { - #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide) - set ch $opt_exposed1 - } - } - append outstring $gxleader - append outstring $sgrleader - if {$idx+1 < $cursor_column} { - append outstring [string map [list "\u0000" " "] $ch] - } else { - append outstring $ch - } - } - incr i - } - #flower.ans good test for null handling - reverse line building - if {![ansistring length $overflow_right]} { - set outstring [string trimright $outstring "\u0000"] - } - set outstring [string map [list "\u0000" " "] $outstring] - set overflow_right [string trimright $overflow_right "\u0000"] - set overflow_right [string map [list "\u0000" " "] $overflow_right] - - set replay_codes "" - if {[llength $understacks] > 0} { - if {$overflow_idx == -1} { - #set tail_idx [dict size $understacks] - set tail_idx [llength $understacks] - } else { - set tail_idx [llength $undercols] - } - if {$tail_idx-1 < [llength $understacks]} { - #set replay_codes [join [lindex $understacks $tail_idx-1] ""] ;#tail replay codes - set replay_codes [punk::ansi::codetype::sgr_merge_list {*}[lindex $understacks $tail_idx-1]] ;#tail replay codes - } - if {$tail_idx-1 < [llength $understacks_gx]} { - set gx0 [lindex $understacks_gx $tail_idx-1] - if {$gx0 eq [list "gx0_on"]} { - #if it was on, turn gx0 off at the point we stop processing overlay - append outstring "\x1b(B" - } - } - } - if {[string length $overflow_right]} { - #puts stderr "remainder:$overflow_right" - } - #pdict $understacks - - if {[punk::ansi::ta::detect_sgr $outstring]} { - append outstring [punk::ansi::a] ;#without this - we would get for example, trailing backgrounds after rightmost column - - #close off any open gx? - #probably should - and overflow_right reopen? - } - - if {$opt_returnextra} { - #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review - #replay_codes_underlay is the set of codes in effect at the very end of the original underlay - - #review - #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied) - #todo - replay_codes for gx0 mode - - #overflow_idx may change during ansi & character processing - if {$overflow_idx == -1} { - set overflow_right_column "" - } else { - set overflow_right_column [expr {$overflow_idx+1}] - } - set result [dict create\ - result $outstring\ - visualwidth [punk::ansi::printing_length $outstring]\ - instruction $instruction\ - stringlen [string length $outstring]\ - overflow_right_column $overflow_right_column\ - overflow_right $overflow_right\ - unapplied $unapplied\ - unapplied_list $unapplied_list\ - insert_mode $insert_mode\ - autowrap_mode $autowrap_mode\ - insert_lines_above $insert_lines_above\ - insert_lines_below $insert_lines_below\ - cursor_saved_position $cursor_saved_position\ - cursor_saved_attributes $cursor_saved_attributes\ - cursor_column $cursor_column\ - cursor_row $cursor_row\ - opt_overflow $opt_overflow\ - replay_codes $replay_codes\ - replay_codes_underlay $replay_codes_underlay\ - replay_codes_overlay $replay_codes_overlay\ - ] - if {$opt_returnextra == 1} { - return $result - } else { - #human/debug - map special chars to visual glyphs - set viewop VIEW - switch -- $opt_returnextra { - 2 { - #codes and character data - set viewop VIEWCODES ;#ansi colorisation of codes - green for SGR, blue/blue reverse for cursor_save/cursor_restore, cyan for movements, orange for others - } - 3 { - set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ. - } - } - dict set result result [ansistring $viewop -lf 1 -vt 1 [dict get $result result]] - dict set result overflow_right [ansistring VIEW -lf 1 -vt 1 [dict get $result overflow_right]] - dict set result unapplied [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied]] - dict set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied_list]] - dict set result replay_codes [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes]] - dict set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_underlay]] - dict set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_overlay]] - dict set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [dict get $result cursor_saved_attributes]] - return $result - } - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - if {[string first \t $textblock] >= 0} { - if {[info exists punk::console::tabwidth]} { - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - set textblock [textutil::tabify::untabify2 $textblock $tw] - } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] - } - if {[string first \n $textblock] >= 0} { - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] - } else { - set num_le 0 - set width [punk::char::ansifreestring_width $textblock] - } - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - variable cache_is_sgr [dict create] - - #we are likely to be asking the same question of the same ansi codes repeatedly - #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS - #todo - test if still worthwhile after a large cache is built up. (limit cache size?) - proc is_sgr {code} { - variable cache_is_sgr - if {[dict exists $cache_is_sgr $code]} { - return [dict get $cache_is_sgr $code] - } - set answer [punk::ansi::codetype::is_sgr $code] - dict set cache_is_sgr $code $answer - return $answer - } - proc render_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list ;#maintaining as a list allows caller to utilize it without having to re-split - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - #append unapplied [join [lindex $overstacks $idx_over] ""] - #append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]] - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - - #clearer - renders the specific gci forward as unapplied - prefixed with it's merged sgr stack - proc render_this_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - proc render_delchar {i} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - set nxt [llength $o] - if {$i < $nxt} { - set o [lreplace $o $i $i] - set ustacks [lreplace $ustacks $i $i] - set gxstacks [lreplace $gxstacks $i $i] - } else { - - } - } - proc render_erasechar {i count} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - #ECH clears character attributes from erased character positions - #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller should do that mapping and only supply 1 or greater. - if {![string is integer -strict $count] || $count < 1} { - error "render_erasechar count must be integer >= 1" - } - set start $i - set end [expr {$i + $count -1}] - #we restrict ECH to current line - as some terminals do - review - is that the only way it's implemented? - if {$i > [llength $o]-1} { - return - } - if {$end > [llength $o]-1} { - set end [expr {[llength $o]-1}] - } - set num [expr {$end - $start + 1}] - set o [lreplace $o $start $end {*}[lrepeat $num \u0000]] ;#or space? - set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list]]] - set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] - return - } - proc render_setchar {i c } { - upvar outcols o - lset o $i $c - } - #is actually addgrapheme? - proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - - if 0 { - if {$c eq "c"} { - puts "i:$i c:$c sgrstack:[ansistring VIEW $sgrstack]" - puts "understacks:[ansistring VIEW $ustacks]" - upvar overstacks overstacks - puts "overstacks:[ansistring VIEW $overstacks]" - puts "info level 0:[info level 0]" - } - } - - set nxt [llength $o] - if {!$insert_mode} { - if {$i < $nxt} { - #These lists must always be in sync - lset o $i $c - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - lset ustacks $i $sgrstack - lset gxstacks $i $gx0stack - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } else { - #insert of single-width vs double-width when underlying is double-width? - if {$i < $nxt} { - set o [linsert $o $i $c] - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - set ustacks [linsert $ustacks $i $sgrstack] - set gxstacks [linsert $gxstacks $i $gx0stack] - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.6.1 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.6.2.tm b/src/bootsupport/modules/overtype-1.6.2.tm deleted file mode 100644 index 0bdd4ca0..00000000 --- a/src/bootsupport/modules/overtype-1.6.2.tm +++ /dev/null @@ -1,3415 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.6.2 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.2] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6- -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -package require punk::assertion -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace import ::punk::assertion::assert - punk::assertion::active true - - namespace path ::punk::lib - - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable grapheme_widths [dict create] - - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::ansifreestring_width $text] -} - -#todo - consider a way to merge overtype::left/centre/right -#These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock -#overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay. -#(i.e not even necessariy having it's top left within the underlay) -namespace eval overtype::priv { -} - -#could return larger than colwidth -proc _get_row_append_column {row} { - upvar outputlines outputlines - set idx [expr {$row -1}] - if {$row <= 1 || $row > [llength $outputlines]} { - return 1 - } else { - upvar opt_overflow opt_overflow - upvar colwidth colwidth - set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]] - set endpos [expr {$existinglen +1}] - if {$opt_overflow} { - return $endpos - } else { - if {$endpos > $colwidth} { - return $colwidth + 1 - } else { - return $endpos - } - } - } -} - -namespace eval overtype { - #*** !doctools - #[subsection {Namespace overtype}] - #[para] Core API functions for overtype - #[list_begin definitions] - - - - #string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r - #render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string. - #The underlay and overlay can be multiline blocks of text of varying line lengths. - #The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text. - #This is a single-shot rendering of strings - ie there is no way to chain another call containing a cursor-restore to previously rendered output and have it know about any cursor-saves in the first call. - # a cursor start position other than top-left is a possible addition to consider. - #see editbuf in punk::repl for a more stateful ansi-processor. Both systems use loops over overtype::renderline - proc left {args} { - #*** !doctools - #[call [fun overtype::left] [arg args] ] - #[para] usage: ?-transparent [lb]0|1[rb]? ?-overflow [lb]1|0[rb]? ?-ellipsis [lb]1|0[rb]? ?-ellipsistext ...? undertext overtext - - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -width \uFFEF\ - -height \uFFEF\ - -wrap 0\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -appendlines 1\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -experimental 0\ - -looplimit \uFFEF\ - ] - #-ellipsis args not used if -wrap is true - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -looplimit - -width - -height - -bias - -wrap - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -appendlines - -transparent - -exposed1 - -exposed2 - -experimental {} - default { - set known_opts [dict keys $defaults] - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - ##### - # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. - set opt_wrap [dict get $opts -wrap] ;#wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) - ##### - #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. - set opt_width [dict get $opts -width] - set opt_height [dict get $opts -height] - set opt_appendlines [dict get $opts -appendlines] - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - # ---------------------------- - # -experimental dev flag to set flags etc - # ---------------------------- - set data_mode 0 - set test_mode 1 - set info_mode 0 - set edit_mode 0 - set opt_experimental [dict get $opts -experimental] - foreach o $opt_experimental { - switch -- $o { - test_mode { - set test_mode 1 - set info_mode 1 - } - old_mode { - set test_mode 0 - set info_mode 1 - } - data_mode { - set data_mode 1 - } - info_mode { - set info_mode 1 - } - edit_mode { - set edit_mode 1 - } - } - } - # ---------------------------- - - #modes - set insert_mode 0 ;#can be toggled by insert key or ansi IRM sequence ESC [ 4 h|l - set autowrap_mode $opt_wrap - set reverse_mode 0 - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - - #set underlines [split $underblock \n] - - #underblock is a 'rendered' block - so width height make sense - #colwidth & colheight were originally named with reference to rendering into a 'column' of output e.g a table column - before cursor row/col was implemented. - #The naming is now confusing. It should be something like renderwidth renderheight ?? review - - if {$opt_width eq "\uFFEF"} { - lassign [blocksize $underblock] _w colwidth _h colheight - } else { - set colwidth $opt_width - set colheight $opt_height - } - if {$underblock eq ""} { - set blank "\x1b\[0m\x1b\[0m" - #set underlines [list "\x1b\[0m\x1b\[0m"] - set underlines [lrepeat $colheight $blank] - } else { - set underlines [lines_as_list -ansiresets 1 $underblock] - } - - #todo - reconsider the 'line' as the natural chunking mechanism for the overlay. - #In practice an overlay ANSI stream can be a single line with ansi moves/restores etc - or even have no moves or newlines, just relying on wrapping at the output colwidth - #In such cases - we process the whole shebazzle for the first output line - only reducing by the applied amount at the head each time, reprocessing the long tail each time. - #(in cases where there are interline moves or cursor jumps anyway) - #This works - but doesn't seem efficient. - #On the other hand.. maybe it depends on the data. For simpler files it's more efficient than splitting first - - #a hack until we work out how to avoid infinite loops... - # - set looplimit [dict get $opts -looplimit] - if {$looplimit eq "\uFFEF"} { - #looping for each char is worst case (all newlines?) - anything over that is an indication of something broken? - #do we need any margin above the length? (telnet mapscii.me test) - set looplimit [expr {[string length $overblock] + 10}] - } - - if {!$test_mode} { - set inputchunks [split $overblock \n] - } else { - set scheme 3 - switch -- $scheme { - 0 { - #one big chunk - set inputchunks [list $overblock] - } - 1 { - set inputchunks [punk::ansi::ta::split_codes $overblock] - } - 2 { - - #split into lines if possible first - then into plaintext/ansi-sequence chunks ? - set inputchunks [list ""] ;#put an empty plaintext split in for starters - set i 1 - set lines [split $overblock \n] - foreach ln $lines { - if {$i < [llength $lines]} { - append ln \n - } - set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single? - set lastpt [lindex $inputchunks end] - lset inputchunks end [string cat $lastpt [lindex $sequence_split 0]] - lappend inputchunks {*}[lrange $sequence_split 1 end] - incr i - } - } - 3 { - #it turns out line based chunks are faster than the above.. probably because some of those end up doing the regex splitting twice - set lflines [list] - set inputchunks [split $overblock \n] - foreach ln $inputchunks { - append ln \n - lappend lflines $ln - } - if {[llength $lflines]} { - lset lflines end [string range [lindex $lflines end] 0 end-1] - } - set inputchunks $lflines[unset lflines] - - } - } - } - - - #overblock height/width isn't useful in the presence of an ansi input overlay with movements. The number of lines may bear little relationship to the output height - #lassign [blocksize $overblock] _w overblock_width _h overblock_height - - - set replay_codes_underlay [dict create 1 ""] - #lappend replay_codes_overlay "" - set replay_codes_overlay "" - set unapplied "" - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - - - set outputlines $underlines - set overidx 0 - - #underlines are not necessarily processed in order - depending on cursor-moves applied from overtext - set row 1 - if {$data_mode} { - set col [_get_row_append_column $row] - } else { - set col 1 - } - - set instruction_stats [dict create] - - set loop 0 - #while {$overidx < [llength $inputchunks]} { } - - while {[llength $inputchunks]} { - #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx "" - set overtext [lpop inputchunks 0] - if {![string length $overtext]} { - incr loop - continue - } - #puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----" - set undertext [lindex $outputlines [expr {$row -1}]] - set renderedrow $row - - #renderline pads each underaly line to width with spaces and should track where end of data is - - - #set overtext [string cat [lindex $replay_codes_overlay $overidx] $overtext] - set overtext [string cat $replay_codes_overlay $overtext] - if {[dict exists $replay_codes_underlay $row]} { - set undertext [string cat [dict get $replay_codes_underlay $row] $undertext] - } - #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary - - #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l - set LASTCALL [list -info 1 -insert_mode $insert_mode -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set rinfo [renderline -experimental $opt_experimental -info 1 -insert_mode $insert_mode -cursor_restore_attributes $cursor_saved_attributes -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set instruction [dict get $rinfo instruction] - set insert_mode [dict get $rinfo insert_mode] - set autowrap_mode [dict get $rinfo autowrap_mode] ;# - #set reverse_mode [dict get $rinfo reverse_mode];#how to support in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set overflow_right_column [dict get $rinfo overflow_right_column] - set unapplied [dict get $rinfo unapplied] - set unapplied_list [dict get $rinfo unapplied_list] - set post_render_col [dict get $rinfo cursor_column] - set post_render_row [dict get $rinfo cursor_row] - set c_saved_pos [dict get $rinfo cursor_saved_position] - set c_saved_attributes [dict get $rinfo cursor_saved_attributes] - set visualwidth [dict get $rinfo visualwidth] - set insert_lines_above [dict get $rinfo insert_lines_above] - set insert_lines_below [dict get $rinfo insert_lines_below] - dict set replay_codes_underlay [expr {$renderedrow+1}] [dict get $rinfo replay_codes_underlay] - #lset replay_codes_overlay [expr $overidx+1] [dict get $rinfo replay_codes_overlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - - - - #-- todo - detect looping properly - if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} { - puts stderr "overtype::left loop?" - puts [ansistring VIEW $rinfo] - break - } - #-- - - if {[dict size $c_saved_pos] >= 1} { - set cursor_saved_position $c_saved_pos - set cursor_saved_attributes $c_saved_attributes - } - - - set overflow_handled 0 - - - - set nextprefix "" - - - #todo - handle potential insertion mode as above for cursor restore? - #keeping separate branches for debugging - review and merge as appropriate when stable - dict incr instruction_stats $instruction - switch -- $instruction { - {} { - if {$test_mode == 0} { - incr row - if {$data_mode} { - set col [_get_row_append_column $row] - if {$col > $colwidth} { - - } - } else { - set col 1 - } - } else { - #lf included in data - set row $post_render_row - set col $post_render_col - - #set col 1 - #if {$post_render_row != $renderedrow} { - # set col 1 - #} else { - # set col $post_render_col - #} - } - } - up { - - #renderline knows it's own line number, and knows not to go above row l - #it knows that a move whilst 1-beyond the width conflicts with the linefeed and reduces the move by one accordingly. - #row returned should be correct. - #column may be the overflow column - as it likes to report that to the caller. - - #Note that an ansi up sequence after last column going up to a previous line and also beyond the last column, will result in the next grapheme going onto the following line. - #this seems correct - as the column remains beyond the right margin so subsequent chars wrap (?) review - #puts stderr "up $post_render_row" - #puts stderr "$rinfo" - - #puts stdout "1 row:$row col $col" - set row $post_render_row - #data_mode (naming?) determines if we move to end of existing data or not. - #data_mode 0 means ignore existing line length and go to exact column - #set by -experimental flag - if {$data_mode == 0} { - set col $post_render_col - } else { - #This doesn't really work if columns are pre-filled with spaces..we can't distinguish them from data - #we need renderline to return the number of the maximum column filled (or min if we ever do r-to-l) - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - - #puts stdout "2 row:$row col $col" - #puts stdout "-----------------------" - #puts stdout $rinfo - #flush stdout - } - down { - if {$data_mode == 0} { - #renderline doesn't know how far down we can go.. - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set row $post_render_row - set col $post_render_col - } else { - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - - } - } - restore_cursor { - #testfile belinda.ans uses this - - #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" - if {[dict exists $cursor_saved_position row]} { - set row [dict get $cursor_saved_position row] - set col [dict get $cursor_saved_position column] - #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]" - #set nextprefix $cursor_saved_attributes - #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes - set replay_codes_overlay [dict get $rinfo replay_codes_overlay]$cursor_saved_attributes - #set replay_codes_overlay $cursor_saved_attributes - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - } else { - #TODO - #?restore without save? - #should move to home position and reset ansi SGR? - #puts stderr "overtype::left cursor_restore without save data available" - } - #If we were inserting prior to hitting the cursor_restore - there could be overflow_right data - generally the overtype functions aren't for inserting - but ansi can enable it - #if we were already in overflow when cursor_restore was hit - it shouldn't have been processed as an action - just stored. - if {!$overflow_handled && $overflow_right ne ""} { - #wrap before restore? - possible effect on saved cursor position - #this overflow data has previously been rendered so has no cursor movements or further save/restore operations etc - #we can just insert another call to renderline to solve this.. ? - #It would perhaps be more properly handled as a queue of instructions from our initial renderline call - #we don't need to worry about overflow next call (?)- but we should carry forward our gx and ansi stacks - - puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]" - - set sub_info [overtype::renderline -info 1 -width $colwidth -insert_mode $insert_mode -autowrap_mode $autowrap_mode -overflow [dict get $opts -overflow] "" $overflow_right] - set foldline [dict get $sub_info result] - set insert_mode [dict get $sub_info insert_mode] ;#probably not needed.. - set autowrap_mode [dict get $sub_info autowrap_mode] ;#nor this.. - linsert outputlines $renderedrow $foldline - #review - row & col set by restore - but not if there was no save.. - } - set overflow_handled 1 - - } - move { - ######## - if {$post_render_row > [llength $outputlines]} { - #Ansi moves need to create new lines ? - #if {$opt_appendlines} { - # set diff [expr {$post_render_row - [llength $outputlines]}] - # if {$diff > 0} { - # lappend outputlines {*}[lrepeat $diff ""] - # } - # set row $post_render_row - #} else { - set row [llength $outputlines] - #} - } else { - set row $post_render_row - } - ####### - set col $post_render_col - #overflow + unapplied? - } - lf_start { - #raw newlines - must be test_mode - # ---------------------- - #test with fruit.ans - #test - treating as newline below... - #append rendered $overflow_right - #set overflow_right "" - set row $renderedrow - incr row - if {$row > [llength $outputlines]} { - lappend outputlines "" - } - set col 1 - # ---------------------- - } - lf_mid { - - if 0 { - #set rhswidth [punk::ansi::printing_length $overflow_right] - #only show debug when we have overflow? - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - - set rhs "" - if {$overflow_right ne ""} { - set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $overflow_right]] - set rhs [textblock::frame -title overflow_right $rhs] - } - puts [textblock::join $lhs " $post_render_col " $rhs] - } - - if {!$test_mode} { - #rendered - append rendered $overflow_right - #set replay_codes_overlay "" - set overflow_right "" - - - set row $renderedrow - - set col 1 - incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - set edit_mode 0 - if {$edit_mode} { - set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied] - set overflow_right "" - set unapplied "" - set row $post_render_row - #set col $post_render_col - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - append rendered $overflow_right - set overflow_right "" - set row $post_render_row - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } - } - } - lf_overflow { - #linefeed after colwidth e.g at column 81 for an 80 col width - #we may also have other control sequences that came after col 80 e.g cursor save - - if 0 { - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - set rhs "" - - #assertion - there should be no overflow.. - puts $lhs - } - assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right - - set row $post_render_row - #set row $renderedrow - #incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - set col 1 - - } - newlines_above { - #we get a newlines_above instruction when received at column 1 - #In some cases we want to treat that as request to insert a new blank line above, and move our row 1 down (staying with the data) - #in other cases - we want to treat at column 1 the same as any other - - puts "--->newlines_above" - puts "rinfo: $rinfo" - #renderline doesn't advance the row for us - the caller has the choice to implement or not - set row $post_render_row - set col $post_render_col - if {$insert_lines_above > 0} { - set row $renderedrow - set outputlines [linsert $outputlines $renderedrow-1 {*}[lrepeat $insert_lines_above ""]] - incr row [expr {$insert_lines_above -1}] ;#we should end up on the same line of text (at a different index), with new empties inserted above - #? set row $post_render_row #can renderline tell us? - } - } - newlines_below { - #obsolete? - use for ANSI insert lines sequence - if {$data_mode == 0} { - puts --->nl_below - set row $post_render_row - set col $post_render_col - if {$insert_lines_below == 1} { - if {$test_mode == 0} { - set row $renderedrow - set outputlines [linsert $outputlines [expr {$renderedrow }] {*}[lrepeat $insert_lines_below ""]] ;#note - linsert can add to end too - incr row $insert_lines_below - set col 1 - } else { - #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]] - #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs] - #set rhs "" - #if {$overflow_right ne ""} { - # set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $overflow_right]] - # set rhs [textblock::frame -title overflow_right $rhs] - #} - #puts [textblock::join $lhs $rhs] - - #rendered - append rendered $overflow_right - # - - - set overflow_right "" - set row $renderedrow - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat $insert_lines_below ""] - } - incr row $insert_lines_below - set col 1 - - - - } - } - } else { - set row $post_render_row - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } else { - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - } - } - wrapmoveforward { - #doesn't seem to be used by fruit.ans testfile - #used by dzds.ans - #note that cursor_forward may move deep into the next line - or even span multiple lines !TODO - set c $colwidth - set r $post_render_row - if {$post_render_col > $colwidth} { - set i $c - while {$i <= $post_render_col} { - if {$c == $colwidth+1} { - incr r - if {$opt_appendlines} { - if {$r < [llength $outputlines]} { - lappend outputlines "" - } - } - set c 1 - } else { - incr c - } - incr i - } - set col $c - } else { - #why are we getting this instruction then? - puts stderr "wrapmoveforward - test" - set r [expr {$post_render_row +1}] - set c $post_render_col - } - set row $r - set col $c - } - wrapmovebackward { - set c $colwidth - set r $post_render_row - if {$post_render_col < 1} { - set c 1 - set i $c - while {$i >= $post_render_col} { - if {$c == 0} { - if {$r > 1} { - incr r -1 - set c $colwidth - } else { - #leave r at 1 set c 1 - #testfile besthpav.ans first line top left border alignment - set c 1 - break - } - } else { - incr c -1 - } - incr i -1 - } - set col $c - } else { - puts stderr "Wrapmovebackward - but postrendercol >= 1???" - } - set row $r - set col $c - } - overflow { - #normal single-width grapheme overflow - #puts "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]" - set row $post_render_row ;#renderline will not advance row when reporting overflow char - if {$autowrap_mode} { - incr row - set col 1 ;#whether wrap or not - next data is at column 1 ?? - } else { - #this works for test_mode (which should become the default) - but could give a bad result otherwise - review - add tests fix. - set col $post_render_col - #set unapplied "" ;#this seems wrong? - #set unapplied [string range $unapplied 1 end] - #The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs - #There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate - #We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow' - set idx 0 - set next_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set next_grapheme_index $idx - break - } - incr idx - } - assert {$next_grapheme_index >= 0} - #drop the overflow grapheme - keeping all codes in place. - set unapplied [join [lreplace $unapplied_list $next_grapheme_index $next_grapheme_index] ""] - #we need to run the reduced unapplied on the same line - further graphemes will just overflow again, but codes or control chars could trigger jumps to other lines - - set overflow_handled 1 - #handled by dropping overflow if any - } - } - overflow_splitchar { - set row $post_render_row ;#renderline will not advance row when reporting overflow char - - #2nd half of grapheme would overflow - treggering grapheme is returned in unapplied. There may also be overflow_right from earlier inserts - #todo - consider various options .. re-render a single trailing space or placeholder on same output line, etc - if {$autowrap_mode} { - if {$colwidth < 2} { - #edge case of rendering to a single column output - any 2w char will just cause a loop if we don't substitute with something, or drop the character - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } else { - set col 1 - incr row - } - } else { - set overflow_handled 1 - #handled by dropping entire overflow if any - if {$colwidth < 2} { - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } - } - - } - vt { - - #can vt add a line like a linefeed can? - set row $post_render_row - set col $post_render_col - } - default { - puts stderr "overtype::left unhandled renderline instruction '$instruction'" - } - - } - - - if {!$opt_overflow && !$autowrap_mode} { - #not allowed to overflow column or wrap therefore we get overflow data to truncate - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim [ansistrip $lostdata]] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - set overflow_handled 1 - } else { - #no wrap - no ellipsis - silently truncate - set overflow_handled 1 - } - } - - - - if {$renderedrow <= [llength $outputlines]} { - lset outputlines [expr {$renderedrow-1}] $rendered - } else { - if {$opt_appendlines} { - lappend outputlines $rendered - } else { - #? - lset outputlines [expr {$renderedrow-1}] $rendered - } - } - - if {!$overflow_handled} { - append nextprefix $overflow_right - } - - append nextprefix $unapplied - - if 0 { - if {$nextprefix ne ""} { - set nextoveridx [expr {$overidx+1}] - if {$nextoveridx >= [llength $inputchunks]} { - lappend inputchunks $nextprefix - } else { - #lset overlines $nextoveridx $nextprefix[lindex $overlines $nextoveridx] - set inputchunks [linsert $inputchunks $nextoveridx $nextprefix] - } - } - } - - if {$nextprefix ne ""} { - set inputchunks [linsert $inputchunks 0 $nextprefix] - } - - - incr overidx - incr loop - if {$loop >= $looplimit} { - puts stderr "overtype::left looplimit reached ($looplimit)" - lappend outputlines "[a+ red bold] - looplimit $looplimit reached[a]" - set Y [a+ yellow bold] - set RST [a] - set sep_header ----DEBUG----- - set debugmsg "" - append debugmsg "${Y}${sep_header}${RST}" \n - append debugmsg "looplimit $looplimit reached\n" - append debugmsg "test_mode:$test_mode\n" - append debugmsg "data_mode:$data_mode\n" - append debugmsg "opt_appendlines:$opt_appendlines\n" - append debugmsg "prev_row :[dict get $LASTCALL -cursor_row]\n" - append debugmsg "prev_col :[dict get $LASTCALL -cursor_column]\n" - dict for {k v} $rinfo { - append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n - } - append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n - - puts stdout $debugmsg - #todo - config regarding error dumps rather than just dumping in working dir - set fd [open [pwd]/error_overtype.txt w] - puts $fd $debugmsg - close $fd - error $debugmsg - break - } - } - - set result [join $outputlines \n] - if {$info_mode} { - #emit to debug window like basictelnet does? make debug configurable as syslog or even a telnet server to allow on 2nd window? - #append result \n$instruction_stats\n - } - return $result - } - - #todo - left-right ellipsis ? - proc centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] - #todo - get replay_codes from overflow_right instead of wherever it was truncated? - - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #don't use string range on ANSI data - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] - } - - proc right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -align "left"\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_align [dict get $opts -align] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$colwidth - $overblock_width)}] - set left_exposed $under_exposed_max - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_align { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set ellipsis [string cat $replay_codes $opt_ellipsistext] - #todo - overflow on left if allign = right?? - set rendered [overtype::right $rendered $ellipsis] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] - } - - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # renderline written from a left-right line orientation perspective as a first-shot at getting something useful. - # ultimately right-to-left, top-to-bottom and bottom-to-top are probably needed. - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # - # - #-returnextra enables returning of overflow and length - #review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation? - #review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements - #(could render it by faking it with sixels and a lot of work - find/make a sixel font and ensure it's exactly 2 cols per char) - #todo - review transparency issues with single/double width characters - #bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? - proc renderline {args} { - #*** !doctools - #[call [fun overtype::renderline] [arg args] ] - #[para] renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode commandline repl for the Tcl Punk Shell - #[para] It is also a central part of an ansi (micro) virtual terminal-emulator of sorts - #[para] This system does a half decent job at rendering 90's ANSI art to manipulable colour text blocks that can be joined & framed for layout display within a unix or windows terminal - #[para] Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't affect another. - #[para] Calling on the punk::ansi library - it can coalesce codes to keep the size down. - #[para] It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - #[para] renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a static underlay - #[para] The underlay is generally expected to be an ordered set of lines or a rectangular text block analogous to a terminal screen - but it can also be ragged in line length, or just blank. - #[para] The overlay couuld be similar - in which case it may often be used to overwrite a column or section of the underlay. - #[para] The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - # - #[para] renderline itself only deals with a single line - or sometimes a single character. It is generally called from a loop that does further terminal-like or textblock processing. - #[para] By suppyling the -info 1 option - it can return various fields indicating the state of the render. - #[para] The main 3 are the result, overflow_right, and unapplied. - #[para] Renderline handles cursor movements from either keystrokes or ANSI sequences but for a full system the aforementioned loop will need to be in place to manage the set of lines under manipulation. - - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines in undertext" - } - #if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - # error "overtype::renderline not allowed to contain newlines" - #} - - set defaults [dict create\ - -etabs 0\ - -width \uFFEF\ - -overflow 0\ - -transparent 0\ - -startcolumn 1\ - -cursor_column 1\ - -cursor_row ""\ - -insert_mode 1\ - -autowrap_mode 1\ - -reverse_mode 0\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -cursor_restore_attributes ""\ - -experimental {}\ - ] - #-cursor_restore_attributes only - for replay stack - position and actual setting/restoring handled by throwback to caller - - #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return - #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs - - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error - - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -experimental - -width - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -autowrap_mode - -reverse_mode - -info - -exposed1 - -exposed2 - -cursor_restore_attributes {} - default { - set known_opts [dict keys $defaults] - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_width [dict get $opts -width] - set opt_etabs [dict get $opts -etabs] - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [dict get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [dict get $opts -cursor_row] - if {[string length $opt_row_context]} { - if {![string is integer -strict $opt_row_context] || $opt_row_context <1 } { - error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode) - set opt_insert_mode [dict get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) - #default is for overtype - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_autowrap_mode [dict get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line - set opt_reverse_mode [dict get $opts -reverse_mode] ;#DECSNM - # -- --- --- --- --- --- --- --- --- --- --- --- - set temp_cursor_saved [dict get $opts -cursor_restore_attributes] - - set test_mode 0 - set cp437_glyphs 0 - foreach e [dict get $opts -experimental] { - switch -- $e { - test_mode { - set test_mode 1 - set cp437_glyphs 1 - } - } - } - set cp437_map [dict create] - if {$cp437_glyphs} { - set cp437_map [set ::punk::ansi::cp437_map] - #for cp437 images we need to map these *after* splitting ansi - #some old files might use newline for its glyph.. but we can't easily support that. - #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs? - dict unset cp437_map \n - } - - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - if {$opt_row_context eq ""} { - set cursor_row 1 - } else { - set cursor_row $opt_row_context - } - - - #----- - # - if {[info exists punk::console::tabwidth]} { - #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted - #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - - set overdata $over - if {!$cp437_glyphs} { - #REVIEW! tabify will give different answers for an ANSI colourised string vs plain text - if {!$opt_etabs} { - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under $tw] - } - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over $tw] - } - } - } - #------- - - #ta_detect ansi and do simpler processing? - - #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column, - #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway. - - # -- --- --- --- --- --- --- --- - if {$under ne ""} { - set undermap [punk::ansi::ta::split_codes_single $under] - } else { - set undermap [list] - } - set understacks [list] - set understacks_gx [list] - - set i_u -1 ;#underlay may legitimately be empty - set undercols [list] - set u_codestack [list] - #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway - set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics) - #set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - #append pt_underchars $pt - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. - #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - set width 1 - } - default { - if {$grapheme eq "\u0000"} { - #use null as empty cell representation - review - #use of this will probably collide with some application at some point - #consider an option to set the empty cell character - set width 1 - } else { - set width [grapheme_width_cached $grapheme] - #we still want most controls and other zero-length codepoints such as \u200d (zero width joiner) to stay zero-length - #we substitute lone ESC that weren't captured within ANSI context as a debugging aid to see malformed ANSI - #todo - default to off and add a flag (?) to enable this substitution - set sub_stray_escapes 0 - if {$sub_stray_escapes && $width == 0} { - if {$grapheme eq "\x1b"} { - set gvis [ansistring VIEW $grapheme] - set grapheme $gvis - set width 1 - } - } - } - } - } - #set width [grapheme_width_cached $grapheme] - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - - lappend undercols $grapheme - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - #todo - if -etabs enabled - then we treat \t as the width determined by our elastic tabstop - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - lappend undercols "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {$code ne ""} { - set c1c2 [string range $code 0 1] - set leadernorm [string range [string map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - switch -- $leadernorm { - 7CSI - 8CSI { - #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse - #REVIEW - what else could end in m but be mistaken as a normal SGR code here? - set maybemouse "" - if {[string index $c1c2 0] eq "\x1b"} { - set maybemouse [string index $code 2] - } - - if {$maybemouse ne "<" && [string index $code end] eq "m"} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - #basic simplification first.. straight dups - set dup_posns [lsearch -all -exact $u_codestack $code] ;#-exact because of square-bracket glob chars - set u_codestack [lremove $u_codestack {*}$dup_posns] - lappend u_codestack $code - } - } - } - 7GFX { - switch -- [string index $code 2] { - "0" { - set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess - } - B { - set u_gx_stack [list] - } - } - } - default { - - } - - } - - #if {[punk::ansi::codetype::is_sgr_reset $code]} { - # #set u_codestack [list] - #} elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #} elseif {[punk::ansi::codetype::is_sgr $code]} { - #} else { - # #leave SGR stack as is - # if {[punk::ansi::codetype::is_gx_open $code]} { - # } elseif {[punk::ansi::codetype::is_gx_close $code]} { - # } - #} - } - #consider also if there are other codes that should be stacked..? - } - - if {!$test_mode} { - #fill columns to width with spaces, and carry over stacks - we will have to keep track of where the underlying data ends manually - TODO - #Specifying a width is suitable for terminal-like applications and text-blocks - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff " "] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - } else { - #NULL empty cell indicator - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff "\u0000"] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - - } - if {$opt_width ne "\uFFEF"} { - set colwidth $opt_width - } else { - set colwidth [llength $undercols] - } - - - if 0 { - # ----------------- - # if we aren't extending understacks & understacks_gx each time we incr idx above the undercols length.. this doesn't really serve a purpose - # Review. - # ----------------- - #replay code for last overlay position in input line - # whether or not we get that far - we need to return it for possible replay on next line - if {[llength $understacks]} { - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - } else { - #in case overlay onto emptystring as underlay - lappend understacks [list] - lappend understacks_gx [list] - } - # ----------------- - } - - #trailing codes in effect for underlay - if {[llength $u_codestack]} { - #set replay_codes_underlay [join $u_codestack ""] - set replay_codes_underlay [punk::ansi::codetype::sgr_merge_list {*}$u_codestack] - } else { - set replay_codes_underlay "" - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char ?) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad_overlay [string repeat " " [expr {$opt_colstart -1}]] - append startpad_overlay $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] - #### - - #??? - set colcursor $opt_colstart - #TODO - make a little virtual column object - #we need to refer to column1 or columnmin? or columnmax without calculating offsets due to to startcolumn - #need to lock-down what start column means from perspective of ANSI codes moving around - the offset perspective is unclear and a mess. - - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - set overstacks [list] - set overstacks_gx [list] - - set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc) - set o_gxstack [list] - set pt_overchars "" - set i_o 0 - set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use - #experiment - set overlay_grapheme_control_stacks [list] - foreach {pt code} $overmap { - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - append pt_overchars $pt - #will get empty pt between adjacent codes - foreach grapheme [punk::char::grapheme_split $pt] { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - lappend overlay_grapheme_control_stacks $o_codestack - #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split) - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] ;#reset better than empty list - fixes some ansi art issues - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $o_codestack $code] ;#must be -exact because of square-bracket glob chars - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[regexp {\x1b7|\x1b\[s} $code]} { - #experiment - #cursor_save - for the replays review. - #jmn - #set temp_cursor_saved [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - lappend overlay_grapheme_control_list [list other $code] - } elseif {[regexp {\x1b8|\x1b\[u} $code]} { - #experiment - #cursor_restore - for the replays - set o_codestack [list $temp_cursor_saved] - lappend overlay_grapheme_control_list [list other $code] - } else { - if {[punk::ansi::codetype::is_gx_open $code]} { - set o_gxstack [list "gx0_on"] - lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set o_gxstack [list] - lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } else { - lappend overlay_grapheme_control_list [list other $code] - } - } - } - - } - #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme - set max_overlay_grapheme_index [expr {$i_o -1}] - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - - #set replay_codes_overlay [join $o_codestack ""] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - - #if {[dict exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [dict get $overstacks $max_overlay_grapheme_index] ""] - #} else { - # set replay_codes_overlay "" - #} - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - # -- --- --- - #we need to initialise overflow_idx before any potential row-movements - as they need to perform a loop break and force in_excess to 1 - if {$opt_overflow} { - #somewhat counterintuitively - overflow true means we can have lines as long as we want, but either way there can be excess data that needs to be thrown back to the calling loop. - set overflow_idx -1 - } else { - #overflow zero - we can't grow beyond our column width - so we get ellipsis or truncation - if {$opt_width ne "\uFFEF"} { - set overflow_idx [expr {$opt_width}] - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - } - # -- --- --- - - set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to. - - set unapplied "" ;#if we break for move row (but not for /v ?) - set unapplied_list [list] - - set insert_lines_above 0 ;#return key - set insert_lines_below 0 - set instruction "" - - # -- --- --- - #cursor_save_dec, cursor_restore_dec etc - set cursor_restore_required 0 - set cursor_saved_attributes "" - set cursor_saved_position "" - # -- --- --- - - #set idx 0 ;# line index (cursor - 1) - #set idx [expr {$opt_colstart + $opt_colcursor} -1] - - #idx is the per column output index - set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1 - #cursor_column is usually one above idx - but we have opt_colstart which is like a margin - todo: remove cursor_column from the following loop and calculate it's offset when breaking or at end. - #(for now we are incrementing/decrementing both in sync - which is a bit silly) - set cursor_column $opt_colcursor - - #idx_over is the per grapheme overlay index - set idx_over -1 - - - #movements only occur within the overlay range. - #an underlay is however not necessary.. e.g - #renderline -overflow 1 "" data - #foreach {pt code} $overmap {} - set insert_mode $opt_insert_mode ;#default 1 - set autowrap_mode $opt_autowrap_mode ;#default 1 - - - #puts "-->$overlay_grapheme_control_list<--" - #puts "-->overflow_idx: $overflow_idx" - for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} { - set gc [lindex $overlay_grapheme_control_list $gci] - lassign $gc type item - - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - #foreach ch $overlay_graphemes {} - switch -- $type { - g { - set ch $item - incr idx_over; #idx_over (until unapplied reached anyway) is per *grapheme* in the overlay - not per col. - if {($idx < ($opt_colstart -1))} { - incr idx [grapheme_width_cached $ch] - continue - } - #set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our active data width - set within_undercols [expr {$idx <= $colwidth-1}] - - #https://www.enigma.com/resources/blog/the-secret-world-of-newline-characters - #\x85 NEL in the c1 control set is treated by some terminal emulators (e.g Hyper) as a newline, - #on some it's invisble but doesn't change the line, on some it's a visible glyph of width 1. - #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable - #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE - - set chtest [string map [list \n \x85 \b \r \v \x7f ] $ch] - #puts --->chtest:$chtest - #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached - switch -- $chtest { - "" { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - if {$idx == 0} { - #puts "---a at col 1" - #linefeed at column 1 - #leave the overflow_idx ;#? review - set instruction lf_start ;#specific instruction for newline at column 1 - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } elseif {$overflow_idx != -1 && $idx == $overflow_idx} { - #linefeed after final column - #puts "---c at overflow_idx=$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } else { - #linefeed occurred in middle or at end of text - #puts "---mid-or-end-text-linefeed idx:$idx overflow_idx:$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_mid - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } - - } - "" { - #will we want/need to use raw for keypresses in terminal? (terminal with LNM in standard reset mode means enter= this is the usual config for terminals) - #So far we are assuming the caller has translated to and handle above.. REVIEW. - - #consider also the old space-carriagereturn softwrap convention used in some terminals. - #In the context of rendering to a block of text - this works similarly in that the space gets eaten so programs emitting space-cr at the terminal width col will pretty much get what they expect. - set idx [expr {$opt_colstart -1}] - set cursor_column $opt_colstart ;#? - } - "" { - #literal backspace char - not necessarily from keyboard - #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype - #(important for -transparent option - hence replacement chars for half-exposed etc) - #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR) - if {$idx > ($opt_colstart -1)} { - incr idx -1 - incr cursor_column -1 - } else { - set flag 0 - if $flag { - #review - conflicting requirements? Need a different sequence for destructive interactive backspace? - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction backspace_at_start - break - } - } - } - "" { - #literal del character - some terminals send just this for what is generally expected to be a destructive backspace - #We instead treat this as a pure delete at current cursor position - it is up to the repl or terminal to remap backspace key to a sequence that has the desired effect. - priv::render_delchar $idx - } - "" { - #end processing this overline. rest of line is remainder. cursor for column as is. - #REVIEW - this theoretically depends on terminal's vertical tabulation setting (name?) - #e.g it could be configured to jump down 6 rows. - #On the other hand I've seen indications that some modern terminal emulators treat it pretty much as a linefeed. - #todo? - incr cursor_row - set overflow_idx $idx - #idx_over has already been incremented as this is both a movement-control and in some sense a grapheme - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction vt - break - } - default { - if {$overflow_idx != -1} { - #review - how to check arbitrary length item such as tab is going to overflow .. before we get to overflow_idx? - #call grapheme_width_cached on each ch, or look for tab specifically as it's currently the only known reason to have a grapheme width > 2? - #we need to decide what a tab spanning the overflow_idx means and how it affects wrap etc etc - if {$idx == $overflow_idx-1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 2} { - #review split 2w overflow? - #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line - #better to consider the overlay char as unable to be applied to the line - #render empty string to column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied - #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #change the overflow_idx - set overflow_idx $idx - incr idx - incr idx_over -1 ;#set overlay grapheme index back one so that sgr stack from previous overlay grapheme used - priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#note $gci-1 instead of just gci - #throw back to caller's loop - add instruction to caller as this is not the usual case - #caller may for example choose to render a single replacement char to this line and omit the grapheme, or wrap it to the next line - set instruction overflow_splitchar - break - } elseif {$owidth > 2} { - #? tab? - #TODO! - puts stderr "overtype::renderline long overtext grapheme '[ansistring VIEW -lf 1 -vt 1 $ch]' not handled" - #tab of some length dependent on tabstops/elastic tabstop settings? - } - } elseif {$idx >= $overflow_idx} { - #jmn? - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci-1]] - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #don't incr idx beyond the overflow_idx - #idx_over already incremented - decrement so current overlay grapheme stacks go to unapplied - incr idx_over -1 - #priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#back one index here too - priv::render_this_unapplied $overlay_grapheme_control_list $gci ;# - set instruction overflow - break - } - } else { - #review. - #This corresponds to opt_overflow being true (at least until overflow_idx is in some cases forced to a value when throwing back to calling loop) - } - - if {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - #dict set understacks $idx [list] ;#review - use idx-1 codestack? - lset understacks $idx [list] - incr idx - incr cursor_column - } else { - #todo - punk::char::char_width - set g [lindex $outcols $idx] - set uwidth [grapheme_width_cached $g] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - incr cursor_column - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - incr cursor_column - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - incr idx - incr cursor_column - if {$owidth > 1} { - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - if {[grapheme_width_cached $ch] == 1} { - if {!$insert_mode} { - #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - incr cursor_column - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - incr cursor_column - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } else { - #? todo - decide what transparency even means for insert mode - incr idx - incr cursor_column - } - } else { - #2wide transparency over 2wide in underlay - review - incr idx - incr cursor_column - } - } - } - } else { - - set idxchar [lindex $outcols $idx] - #non-transparent char in overlay or empty cell - if {$idxchar eq "\u0000"} { - #empty/erased cell indicator - set uwidth 1 - } else { - set uwidth [grapheme_width_cached $idxchar] - } - if {$within_undercols} { - if {$idxchar eq ""} { - #2nd col of 2wide char in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??) - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 - } - incr idx - } else { - set prevcolinfo [lindex $outcols $idx-1] - #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right - #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?) - #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char - #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises - #It is perhaps best avoided at another level and try to make renderline do exactly as it's told - #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect. - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index - if {$prevcolinfo ne ""} { - #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 ;#replace not insert - } ;# else?? - incr idx - } - if {$cursor_column < [llength $outcols] || $overflow_idx == -1} { - incr cursor_column - } - } elseif {$uwidth == 0} { - #what if this is some other c0/c1 control we haven't handled specifically? - - #by emitting a preceding empty-string column - we associate whatever this char is with the preceeding non-zero-length character and any existing zero-lengths that follow it - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - #if we can get a proper grapheme_split function - this should be easier to tidy up. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column 2 - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } elseif {$uwidth == 1} { - #includes null empty cells - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {([llength $outcols] >= $idx +2) && [lindex $outcols $idx+1] eq ""} { - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks $idx+1] [lindex $understacks_gx $idx+1] $insert_mode - } - incr idx - } - if {($cursor_column < [llength $outcols]) || $overflow_idx == -1 || $test_mode} { - incr cursor_column - } - } elseif {$uwidth > 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - #1wide over 2wide in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #insert mode just pushes all to right - no exposition char here - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx 2 - incr cursor_column 2 - } - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - if {$overflow_idx !=-1 && !$test_mode} { - #overflow - if {$cursor_column > [llength $outcols]} { - set cursor_column [llength $outcols] - } - } - } - } - } - } ;# end switch - - - } - other { - set code $item - #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' - - set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM - set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - set re_row_move {\x1b\[([0-9]*)(A|B)$} - set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? - set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! - set re_cursor_restore {\x1b\[u$} - set re_cursor_save_dec {\x1b7$} - set re_cursor_restore_dec {\x1b8$} - set re_other_single {\x1b(D|M|E)$} - set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins - set matchinfo [list] - - #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI - #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping - #review - cost/benefit of function calls within these switch-arms instead of inline code? - - #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. - #we should probably therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore - set code [string map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $code] - - - set c1 [string index $code 0] - set c1c2c3 [string range $code 0 2] - #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} - set leadernorm [string range [string map [list\ - \x1b\[< 1006\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\] 7OSC\ - \x9d 8OSC\ - \x1b 7ESC\ - ] $c1c2c3] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - #we leave the tail of the code unmapped for now - switch -- $leadernorm { - 1006 { - #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html - #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release - set codenorm [string cat $leadernorm [string range $code 3 end]] - } - 7CSI - 7OSC { - set codenorm [string cat $leadernorm [string range $code 2 end]] - } - 7ESC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - 8CSI - 8OSC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - default { - #we haven't made a mapping for this - set codenorm $code - } - } - - #we've mapped 7 and 8bit escapes to values we can handle as literals in switch statements to take advantange of jump tables. - switch -- $leadernorm { - 1006 { - #TODO - # - switch -- [string index $codenorm end] { - M { - puts stderr "mousedown $codenorm" - } - m { - puts stderr "mouseup $codenorm" - } - } - - } - {7CSI} - {8CSI} { - set param [string range $codenorm 4 end-1] - #puts stdout "--> CSI [string index $leadernorm 0] bit param:$param" - switch -- [string index $codenorm end] { - D { - #Col move - #puts stdout "<-back" - #cursor back - #left-arrow/move-back when ltr mode - set num $param - if {$num eq ""} {set num 1} - - set version 2 - if {$version eq "2"} { - #todo - startcolumn offset! - if {$cursor_column - $num >= 1} { - incr idx -$num - incr cursor_column -$num - } else { - if {!$autowrap_mode} { - set cursor_column 1 - set idx 0 - } else { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr cursor_column -$num - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmovebackward - break - } - } - } else { - incr idx -$num - incr cursor_column -$num - if {$idx < $opt_colstart-1} { - #wrap to previous line and position cursor at end of data - set idx [expr {$opt_colstart-1}] - set cursor_column $opt_colstart - } - } - } - C { - #Col move - #puts stdout "->forward" - #todo - consider right-to-left cursor mode (e.g Hebrew).. some day. - #cursor forward - #right-arrow/move forward - set num $param - if {$num eq ""} {set num 1} - - #todo - retrict to moving 1 position past datalen? restrict to column width? - #should ideally wrap to next line when interactive and not on last row - #(some ansi art seems to expect this behaviour) - #This presumably depends on the terminal's wrap mode - #e.g DECAWM autowrap mode - # CSI ? 7 h - set: autowrap (also tput smam) - # CSI ? 7 l - reset: no autowrap (also tput rmam) - set version 2 - if {$version eq "2"} { - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$test_mode && $cursor_column == $max+1} { - #move_forward while in overflow - incr cursor_column -1 - } - - if {($cursor_column + $num) <= $max} { - incr idx $num - incr cursor_column $num - } else { - if {$autowrap_mode} { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #jmn - if {$idx == $overflow_idx} { - incr num - } - - #horizontal movement beyond line extent needs to wrap - throw back to caller - #we may have both overflow_rightand unapplied data - #(can have overflow_right if we were in insert_mode and processed chars prior to this movement) - #leave row as is - caller will need to determine how many rows the column-movement has consumed - incr cursor_column $num ;#give our caller the necessary info as columns from start of row - #incr idx_over - #should be gci following last one applied - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmoveforward - break - } else { - set cursor_column $max - set idx [expr {$cursor_column -1}] - } - } - } else { - if {!$opt_overflow || ($cursor_column + $num) <= [llength $outcols+1]} { - incr idx $num - incr cursor_column $num - } else { - if {!$insert_mode} { - #block editing style with arrow keys - #overtype mode - set idxstart $idx - set idxend [llength $outcols] - set moveend [expr {$idxend - $idxstart}] - if {$moveend < 0} {set moveend 0} ;#sanity? - #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" - incr idx $moveend - incr cursor_column $moveend - #if {[dict exists $understacks $idx]} { - # set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #} else { - # set stackinfo [list] - #} - if {$idx < [llength $understacks]} { - set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - } else { - set stackinfo [list] - } - if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [dict get $understacks_gx $idx] - set gxstackinfo [lindex $understacks_gx $idx] - } else { - set gxstackinfo [list] - } - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - incr cursor_column - priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode - } - } else { - #normal - insert - incr idx $num - incr cursor_column $num - if {$idx > [llength $outcols]} { - set idx [llength $outcols];#allow one beyond - for adding character at end of line - set cursor_column [expr {[llength $outcols]+1}] - } - } - } - } - } - G { - #Col move - #move absolute column - #adjust to colstart - as column 1 is within overlay - #??? - set idx [expr {$param + $opt_colstart -1}] - set cursor_column $param - error "renderline absolute col move ESC G unimplemented" - } - A { - #Row move - up - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set num $param - if {$num eq ""} {set num 1} - incr cursor_row -$num - - if {$cursor_row < 1} { - set cursor_row 1 - } - - #ensure rest of *overlay* is emitted to remainder - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up - #retain cursor_column - break - } - B { - #Row move - down - set num $param - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move down - if {$num eq ""} {set num 1} - incr cursor_row $num - - - incr idx_over ;#idx_over hasn't encountered a grapheme and hasn't advanced yet - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - H - f { - #$re_both_move - lassign [split $param {;}] row col - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #lassign $matchinfo _match row col - - if {$col eq ""} {set col 1} - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$col > $max} { - set cursor_column $max - } else { - set cursor_column $col - } - set idx [expr {$cursor_column -1}] - - if {$row eq ""} {set row 1} - set cursor_row $row - if {$cursor_row < 1} { - set cursor_row 1 - } - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - - } - X { - puts stderr "X - $param" - #ECH - erase character - if {$param eq "" || $param eq "0"} {set param 1}; #param=count of chars to erase - priv::render_erasechar $idx $param - #cursor position doesn't change. - } - r { - #$re_decstbm - #https://www.vt100.net/docs/vt510-rm/DECSTBM.html - #This control function sets the top and bottom margins for the current page. You cannot perform scrolling outside the margins - lassign [split $param {;}] margin_top margin_bottom - - #todo - return these for the caller to process.. - puts stderr "overtype::renderline DECSTBM set top and bottom margin not implemented" - #Also moves the cursor to col 1 line 1 of the page - set cursor_column 1 - set cursor_row 1 - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move ;#own instruction? decstbm? - break - } - s { - # - todo - make ansi.sys CSI s cursor save only apply for certain cases? - may need to support DECSLRM instead which uses same code - - #$re_cursor_save - #cursor save could come after last column - if {$overflow_idx != -1 && $idx == $overflow_idx} { - #bartman2.ans test file - fixes misalignment at bottom of dialog bubble - #incr cursor_row - #set cursor_column 1 - #bwings1.ans test file - breaks if we actually incr cursor (has repeated saves) - set cursor_saved_position [list row [expr {$cursor_row+1}] column 1] - } else { - set cursor_saved_position [list row $cursor_row column $cursor_column] - } - #there may be overlay stackable codes emitted that aren't in the understacks because they come between the last emmited character and the cursor_save control. - #we need the SGR and gx overlay codes prior to the cursor_save - - #a real terminal would not be able to know the state of the underlay.. so we should probably ignore it. - #set sgr_stack [lindex $understacks $idx] - #set gx_stack [lindex $understacks_gx $idx] ;#not actually a stack - just a boolean state (for now?) - - set sgr_stack [list] - set gx_stack [list] - - #we shouldn't need to scan for intermediate cursor save/restores - as restores would throw-back to the calling loop - so our overlay 'line' is since those. - #The overlay_grapheme_control_list had leading resets from previous lines - so we go back to the beginning not just the first grapheme. - - foreach gc [lrange $overlay_grapheme_control_list 0 $gci-1] { - lassign $gc type code - #types g other sgr gx0 - switch -- $type { - gx0 { - #code is actually a stand-in for the graphics on/off code - not the raw code - #It is either gx0_on or gx0_off - set gx_stack [list $code] - } - sgr { - #code is the raw code - if {[punk::ansi::codetype::is_sgr_reset $code]} { - #jmn - set sgr_stack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set sgr_stack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #often we don't get resets - and codes just pile up. - #as a first step to simplifying - at least remove earlier straight up dupes - set dup_posns [lsearch -all -exact $sgr_stack $code] ;#needs -exact - codes have square-brackets (glob chars) - set sgr_stack [lremove $sgr_stack {*}$dup_posns] - lappend sgr_stack $code - } - } - } - } - set cursor_saved_attributes "" - switch -- [lindex $gx_stack 0] { - gx0_on { - append cursor_saved_attributes "\x1b(0" - } - gx0_off { - append cursor_saved_attributes "\x1b(B" - } - } - #append cursor_saved_attributes [join $sgr_stack ""] - append cursor_saved_attributes [punk::ansi::codetype::sgr_merge_list {*}$sgr_stack] - - #as there is apparently only one cursor storage element we don't need to throw back to the calling loop for a save. - - #don't incr index - or the save will cause cursor to move to the right - #carry on - - } - u { - #$re_cursor_restore - #we are going to jump somewhere.. for now we will assume another line, and process accordingly. - #The caller has the cursor_saved_position/cursor_saved_attributes if any (?review - if we always pass it back it, we could save some calls for moves in same line) - #don't set overflow at this point. The existing underlay to the right must be preserved. - #we only want to jump and render the unapplied at the new location. - - #lset overstacks $idx_over [list] - #set replay_codes_overlay "" - - #if {$cursor_saved_attributes ne ""} { - # set replay_codes_overlay $cursor_saved_attributes ;#empty - or last save if it happend in this input chunk - #} else { - #jj - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set replay_codes_overlay "" - #} - - #like priv::render_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code - incr idx_over - - set unapplied "" - set unapplied_list [list] - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - #incr idx_over - } - set unapplied [join $unapplied_list ""] - #if the save occured within this line - that's ok - it's in the return value list and caller can prepend for the next loop. - set instruction restore_cursor - break - } - ~ { - #$re_vt_sequence - #lassign $matchinfo _match key mod - lassign [split $param {;}] key mod - - #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~ - # - #e.g esc \[2~ insert esc \[2;2~ shift-insert - #mod - subtract 1, and then use bitmask - #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?) - #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]" - if {$key eq "1"} { - #home - } elseif {$key eq "2"} { - #Insert - if {$mod eq ""} { - #no modifier key - set insert_mode [expr {!$insert_mode}] - #rather than set the cursor - we return the insert mode state so the caller can decide - } - } elseif {$key eq "3"} { - #Delete - presumably this shifts other chars in the line, with empty cells coming in from the end - switch -- $mod { - "" { - priv::render_delchar $idx - } - "5" { - #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?) - } - } - } elseif {$key eq "4"} { - #End - } elseif {$key eq "5"} { - #pgup - } elseif {$key eq "6"} { - #pgDn - } elseif {$key eq "7"} { - #Home - #?? - set idx [expr {$opt_colstart -1}] - set cursor_column 1 - } elseif {$key eq "8"} { - #End - } elseif {$key eq "11"} { - #F1 - or ESCOP or e.g shift F1 ESC\[1;2P - } elseif {$key eq "12"} { - #F2 - or ESCOQ - } elseif {$key eq "13"} { - #F3 - or ESCOR - } elseif {$key eq "14"} { - #F4 - or ESCOS - } elseif {$key eq "15"} { - #F5 or shift F5 ESC\[15;2~ - } elseif {$key eq "17"} { - #F6 - } elseif {$key eq "18"} { - #F7 - } elseif {$key eq "19"} { - #F8 - } elseif {$key eq "20"} { - #F9 - } elseif {$key eq "21"} { - #F10 - } elseif {$key eq "23"} { - #F11 - } elseif {$key eq "24"} { - #F12 - } - - } - h - l { - #we are matching only last char to get to this arm - but are there other sequences ending in h|l we need to handle? - - #$re_mode if first after CSI is "?" - #some docs mention ESC=h|l - not seen on windows terminals.. review - #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html - if {[string index $codenorm 4] eq "?"} { - set num [string range $codenorm 5 end-1] ;#param between ? and h|l - #lassign $matchinfo _match num type - switch -- $num { - 5 { - #DECSNM - reverse video - #How we simulate this to render within a block of text is an open question. - #track all SGR stacks and constantly flip based on the current SGR reverse state? - #It is the job of the calling loop to do this - so at this stage we'll just set the states - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set reverse_mode 1 - } else { - #reset (disable) - set reverse_mode 0 - } - - } - 7 { - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set autowrap_mode 1 - if {$opt_width ne "\uFFEF"} { - set overflow_idx $opt_width - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - #review - can idx ever be beyond overflow_idx limit when we change e.g with a width setting and cursor movements? presume not - but sanity check for now. - if {$idx >= $overflow_idx} { - puts stderr "renderline error - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected" - } - } else { - #reset (disable) - set autowrap_mode 0 - set overflow_idx -1 - } - } - 25 { - if {$type eq "h"} { - #visible cursor - - } else { - #invisible cursor - - } - } - } - - } else { - puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - default { - puts stderr "overtype::renderline CSI code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - 7ESC { - #$re_other_single - switch -- [string index $codenorm end] { - D { - #\x84 - #index (IND) - #vt102-docs: "Moves cursor down one line in same column. If cursor is at bottom margin, screen performs a scroll-up" - puts stderr "ESC D not fully implemented" - incr cursor_row - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - M { - #\x8D - #Reverse Index (RI) - #vt102-docs: "Moves cursor up one line in same column. If cursor is at top margin, screen performs a scroll-down" - puts stderr "ESC M not fully implemented" - - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move up - incr cursor_row -1 - if {$cursor_row < 1} { - set cursor_row 1 - } - #ensure rest of *overlay* is emitted to remainder - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up ;#need instruction for scroll-down? - #retain cursor_column - break - } - E { - #\x85 - #review - is behaviour different to lf? - #todo - possibly(?) same logic as handling above. i.e return instruction depends on where column_cursor is at the time we get NEL - #leave implementation until logic for is set in stone... still under review - #It's arguable NEL is a pure cursor movement as opposed to the semantic meaning of crlf or lf in a file. - # - #Next Line (NEL) "Move the cursor to the left margin on the next line. If the cursor is at the bottom margin, scroll the page up" - puts stderr "ESC E unimplemented" - - } - default { - puts stderr "overtype::renderline ESC code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - - } - } - - #switch -regexp -matchvar matchinfo -- $code\ - #$re_mode { - #}\ - #default { - # puts stderr "overtype::renderline code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - #} - - } - default { - #don't need to handle sgr or gx0 types - #we have our sgr gx0 codes already in stacks for each overlay grapheme - } - } - } - - #-------- - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - #set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - if {$overflow_idx == -1} { - #overflow was initially unlimited and hasn't been overridden - } else { - - } - #-------- - - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" ;#output prior to overflow - set overflow_right "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set prev_g0 [list] - #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves - set in_overflow 0 ;#used to stop char-width scanning once in overflow - if {$overflow_idx == 0} { - #how does caller avoid an infinite loop if they have autowrap on and keep throwing graphemes to the next line? REVIEW - set in_overflow 1 - } - foreach ch $outcols { - #puts "---- [ansistring VIEW $ch]" - - set gxleader "" - if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {$g0 ne $prev_g0} { - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } else { - set gxleader "\x1b(B" - } - } - set prev_g0 $g0 - } else { - set prev_g0 [list] - } - - set sgrleader "" - if {$i < [llength $understacks]} { - #set cstack [dict get $understacks $i] - set cstack [lindex $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack] && ![llength $cstack]} { - #This reset is important e.g testfile fruit.ans - we get overhang on rhs without it. But why is cstack empty? - append sgrleader \033\[m - } else { - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - } - } - set prevstack $cstack - } else { - set prevstack [list] - } - - - - if {$in_overflow} { - if {$i == $overflow_idx} { - set 0 [lindex $understacks_gx $i] - set gxleader "" - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } elseif {$g0 eq [list "gx0_off"]} { - set gxleader "\x1b(B" - } - append overflow_right $gxleader - set cstack [lindex $understacks $i] - set sgrleader "" - #whether cstack is same or differs from previous char's stack - we must have an output at the start of the overflow_right - #if {[llength $prevstack] && ![llength $cstack]} { - # append sgrleader \033\[m - #} - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - append overflow_right $sgrleader - append overflow_right $ch - } else { - append overflow_right $gxleader - append overflow_right $sgrleader - append overflow_right $ch - } - } else { - if {$overflow_idx != -1 && $i+1 == $overflow_idx} { - #one before overflow - #will be in overflow in next iteration - set in_overflow 1 - if {[grapheme_width_cached $ch]> 1} { - #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide) - set ch $opt_exposed1 - } - } - append outstring $gxleader - append outstring $sgrleader - if {$idx+1 < $cursor_column} { - append outstring [string map [list "\u0000" " "] $ch] - } else { - append outstring $ch - } - } - incr i - } - #flower.ans good test for null handling - reverse line building - if {![ansistring length $overflow_right]} { - set outstring [string trimright $outstring "\u0000"] - } - set outstring [string map [list "\u0000" " "] $outstring] - set overflow_right [string trimright $overflow_right "\u0000"] - set overflow_right [string map [list "\u0000" " "] $overflow_right] - - set replay_codes "" - if {[llength $understacks] > 0} { - if {$overflow_idx == -1} { - #set tail_idx [dict size $understacks] - set tail_idx [llength $understacks] - } else { - set tail_idx [llength $undercols] - } - if {$tail_idx-1 < [llength $understacks]} { - #set replay_codes [join [lindex $understacks $tail_idx-1] ""] ;#tail replay codes - set replay_codes [punk::ansi::codetype::sgr_merge_list {*}[lindex $understacks $tail_idx-1]] ;#tail replay codes - } - if {$tail_idx-1 < [llength $understacks_gx]} { - set gx0 [lindex $understacks_gx $tail_idx-1] - if {$gx0 eq [list "gx0_on"]} { - #if it was on, turn gx0 off at the point we stop processing overlay - append outstring "\x1b(B" - } - } - } - if {[string length $overflow_right]} { - #puts stderr "remainder:$overflow_right" - } - #pdict $understacks - - if {[punk::ansi::ta::detect_sgr $outstring]} { - append outstring [punk::ansi::a] ;#without this - we would get for example, trailing backgrounds after rightmost column - - #close off any open gx? - #probably should - and overflow_right reopen? - } - - if {$opt_returnextra} { - #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review - #replay_codes_underlay is the set of codes in effect at the very end of the original underlay - - #review - #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied) - #todo - replay_codes for gx0 mode - - #overflow_idx may change during ansi & character processing - if {$overflow_idx == -1} { - set overflow_right_column "" - } else { - set overflow_right_column [expr {$overflow_idx+1}] - } - set result [dict create\ - result $outstring\ - visualwidth [punk::ansi::printing_length $outstring]\ - instruction $instruction\ - stringlen [string length $outstring]\ - overflow_right_column $overflow_right_column\ - overflow_right $overflow_right\ - unapplied $unapplied\ - unapplied_list $unapplied_list\ - insert_mode $insert_mode\ - autowrap_mode $autowrap_mode\ - insert_lines_above $insert_lines_above\ - insert_lines_below $insert_lines_below\ - cursor_saved_position $cursor_saved_position\ - cursor_saved_attributes $cursor_saved_attributes\ - cursor_column $cursor_column\ - cursor_row $cursor_row\ - opt_overflow $opt_overflow\ - replay_codes $replay_codes\ - replay_codes_underlay $replay_codes_underlay\ - replay_codes_overlay $replay_codes_overlay\ - ] - if {$opt_returnextra == 1} { - return $result - } else { - #human/debug - map special chars to visual glyphs - set viewop VIEW - switch -- $opt_returnextra { - 2 { - #codes and character data - set viewop VIEWCODES ;#ansi colorisation of codes - green for SGR, blue/blue reverse for cursor_save/cursor_restore, cyan for movements, orange for others - } - 3 { - set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ. - } - } - dict set result result [ansistring $viewop -lf 1 -vt 1 [dict get $result result]] - dict set result overflow_right [ansistring VIEW -lf 1 -vt 1 [dict get $result overflow_right]] - dict set result unapplied [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied]] - dict set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied_list]] - dict set result replay_codes [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes]] - dict set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_underlay]] - dict set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_overlay]] - dict set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [dict get $result cursor_saved_attributes]] - return $result - } - } else { - return $outstring - } - #return [join $out ""] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace overtype ---}] -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} - - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#intended for single grapheme - but will work for multiple -#cannot contain ansi or newlines -#(a cache of ansifreestring_width calls - as these are quite regex heavy) -proc overtype::grapheme_width_cached {ch} { - variable grapheme_widths - if {[dict exists $grapheme_widths $ch]} { - return [dict get $grapheme_widths $ch] - } - set width [punk::char::ansifreestring_width $ch] - dict set grapheme_widths $ch $width - return $width -} - - - -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - if {[string first \t $textblock] >= 0} { - if {[info exists punk::console::tabwidth]} { - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - set textblock [textutil::tabify::untabify2 $textblock $tw] - } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] - } - if {[string first \n $textblock] >= 0} { - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] - } else { - set num_le 0 - set width [punk::char::ansifreestring_width $textblock] - } - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - variable cache_is_sgr [dict create] - - #we are likely to be asking the same question of the same ansi codes repeatedly - #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS - #todo - test if still worthwhile after a large cache is built up. (limit cache size?) - proc is_sgr {code} { - variable cache_is_sgr - if {[dict exists $cache_is_sgr $code]} { - return [dict get $cache_is_sgr $code] - } - set answer [punk::ansi::codetype::is_sgr $code] - dict set cache_is_sgr $code $answer - return $answer - } - proc render_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list ;#maintaining as a list allows caller to utilize it without having to re-split - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - #append unapplied [join [lindex $overstacks $idx_over] ""] - #append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]] - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - - #clearer - renders the specific gci forward as unapplied - prefixed with it's merged sgr stack - proc render_this_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - proc render_delchar {i} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - set nxt [llength $o] - if {$i < $nxt} { - set o [lreplace $o $i $i] - set ustacks [lreplace $ustacks $i $i] - set gxstacks [lreplace $gxstacks $i $i] - } else { - - } - } - proc render_erasechar {i count} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - #ECH clears character attributes from erased character positions - #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller should do that mapping and only supply 1 or greater. - if {![string is integer -strict $count] || $count < 1} { - error "render_erasechar count must be integer >= 1" - } - set start $i - set end [expr {$i + $count -1}] - #we restrict ECH to current line - as some terminals do - review - is that the only way it's implemented? - if {$i > [llength $o]-1} { - return - } - if {$end > [llength $o]-1} { - set end [expr {[llength $o]-1}] - } - set num [expr {$end - $start + 1}] - set o [lreplace $o $start $end {*}[lrepeat $num \u0000]] ;#or space? - set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list]]] - set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] - return - } - proc render_setchar {i c } { - upvar outcols o - lset o $i $c - } - #is actually addgrapheme? - proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - - if 0 { - if {$c eq "c"} { - puts "i:$i c:$c sgrstack:[ansistring VIEW $sgrstack]" - puts "understacks:[ansistring VIEW $ustacks]" - upvar overstacks overstacks - puts "overstacks:[ansistring VIEW $overstacks]" - puts "info level 0:[info level 0]" - } - } - - set nxt [llength $o] - if {!$insert_mode} { - if {$i < $nxt} { - #These lists must always be in sync - lset o $i $c - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - lset ustacks $i $sgrstack - lset gxstacks $i $gx0stack - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } else { - #insert of single-width vs double-width when underlying is double-width? - if {$i < $nxt} { - set o [linsert $o $i $c] - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - set ustacks [linsert $ustacks $i $sgrstack] - set gxstacks [linsert $gxstacks $i $gx0stack] - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.6.2 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.6.4.tm b/src/bootsupport/modules/overtype-1.6.5.tm similarity index 98% rename from src/bootsupport/modules/overtype-1.6.4.tm rename to src/bootsupport/modules/overtype-1.6.5.tm index 42876322..143794fb 100644 --- a/src/bootsupport/modules/overtype-1.6.4.tm +++ b/src/bootsupport/modules/overtype-1.6.5.tm @@ -7,7 +7,7 @@ # (C) Julian Noble 2003-2023 # # @@ Meta Begin -# Application overtype 1.6.4 +# Application overtype 1.6.5 # Meta platform tcl # Meta license BSD # @@ Meta End @@ -17,7 +17,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.4] +#[manpage_begin overtype_module_overtype 0 1.6.5] #[copyright "2024"] #[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] #[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] @@ -146,66 +146,12 @@ tcl::namespace::eval overtype { } -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [tcl::dict::get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - proc overtype::string_columns {text} { if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] + #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::ansistrip. Alternatively try punk::ansi::printing_length" + set text [punk::ansi::ansistrip $text] } return [punk::char::ansifreestring_width $text] } @@ -265,7 +211,7 @@ tcl::namespace::eval overtype { variable default_ellipsis_horizontal if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} + error {usage: ?-width ? ?-startcolumn ? ?-transparent [0|1|]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} } lassign [lrange $args end-1 end] underblock overblock set opts [tcl::dict::create\ @@ -1059,7 +1005,7 @@ tcl::namespace::eval overtype { set show_ellipsis 0 } #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - if {[tcl::string::trim [punk::ansi::stripansi $lostdata]] eq ""} { + if {[tcl::string::trim [punk::ansi::ansistrip $lostdata]] eq ""} { set show_ellipsis 0 } } @@ -1837,8 +1783,9 @@ tcl::namespace::eval overtype { set pt [tcl::string::map $cp437_map $pt] } foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. + #an ugly but easy hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. + #todo - test decimal value instead, compare performance switch -- $grapheme { " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - @@ -3460,9 +3407,9 @@ proc overtype::blocksize {textblock} { } set textblock [textutil::tabify::untabify2 $textblock $tw] } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests + #ansistrip on entire block in one go rather than line by line - result should be the same - review - make tests if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] + set textblock [punk::ansi::ansistrip $textblock] } if {[tcl::string::last \n $textblock] >= 0} { set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map {\n {}} $textblock]]}] ;#faster than splitting into single-char list @@ -3677,7 +3624,7 @@ tcl::namespace::eval overtype { ## Ready package provide overtype [tcl::namespace::eval overtype { variable version - set version 1.6.4 + set version 1.6.5 }] return diff --git a/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/bootsupport/modules/punk/ansi-0.1.1.tm index fd14bcae..85cb9f27 100644 --- a/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -265,13 +265,13 @@ tcl::namespace::eval punk::ansi::class { } set opts_width [tcl::dict::get $opts -width] if {$opts_width eq ""} { - return [punk::ansi::stripansiraw [$o_ansistringobj get]] + return [punk::ansi::ansistripraw [$o_ansistringobj get]] } elseif {$opts_width eq "auto"} { lassign [punk::console::get_size] _cols columns _rows rows set displaycols [expr {$columns -4}] ;#review - return [overtype::renderspace -width $displaycols -wrap 1 "" [punk::ansi::stripansiraw [$o_ansistringobj get]]] + return [overtype::renderspace -width $displaycols -wrap 1 "" [punk::ansi::ansistripraw [$o_ansistringobj get]]] } elseif {[tcl::string::is integer -strict $opts_width] && $opts_width > 0} { - return [overtype::renderspace -width $opts_width -wrap 1 "" [punk::ansi::stripansiraw [$o_ansistringobj get]]] + return [overtype::renderspace -width $opts_width -wrap 1 "" [punk::ansi::ansistripraw [$o_ansistringobj get]]] } else { error "viewchars unrecognised value for -width. Try auto or a positive integer" } @@ -420,7 +420,7 @@ tcl::namespace::eval punk::ansi { get_*\ move*\ reset*\ - strip*\ + ansistrip*\ test_decaln\ titleset\ @@ -750,7 +750,7 @@ tcl::namespace::eval punk::ansi { #mqj #m = boxd_lur - #don't call detect_g0 in here. Leave for caller. e.g stripansi uses detect_g0 to decide whether to call this. + #don't call detect_g0 in here. Leave for caller. e.g ansistrip uses detect_g0 to decide whether to call this. set re_g0_open_or_close {\x1b\(0|\x1b\(B} set parts [::punk::ansi::ta::_perlish_split $re_g0_open_or_close $text] @@ -813,14 +813,17 @@ tcl::namespace::eval punk::ansi { proc g0 {text} { return \x1b(0$text\x1b(B } + proc ansistrip_gx {text} { + #e.g "\033(0" - select VT100 graphics for character set G0 + #e.g "\033(B" - reset + #e.g "\033)0" - select VT100 graphics for character set G1 + #e.g "\033)X" - where X is any char other than 0 to reset ?? + + #return [convert_g0 $text] + return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] + } proc stripansi_gx {text} { - #e.g "\033(0" - select VT100 graphics for character set G0 - #e.g "\033(B" - reset - #e.g "\033)0" - select VT100 graphics for character set G1 - #e.g "\033)X" - where X is any char other than 0 to reset ?? - - #return [convert_g0 $text] - return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] + return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] } @@ -1085,7 +1088,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #This is an in depth analysis of the xterm colour set which gives names(*) to all of the 256 colours and describes possible indexing by Hue,Luminance,Saturation #https://www.wowsignal.io/articles/xterm256 - #*The names are wildly-imaginative, often unintuitively so, and multiple (5?) given for each colour - so they are unlikely to be of practical use or any sort of standard. + # *The names are wildly-imaginative, often unintuitively so, and multiple (5?) given for each colour - so they are unlikely to be of practical use or any sort of standard. #e.g who is to know that 'Rabbit Paws', 'Forbidden Thrill' and 'Tarsier' refer to a particular shade of pinky-red? (code 95) #Perhaps it's an indication that colour naming once we get to 256 colours or more is a fool's errand anyway. #The xterm names are boringly unimaginative - and also have some oddities such as: @@ -1872,7 +1875,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t configure -frametype {} $t configure_column 0 -headers [list "[tcl::string::totitle $g] colours"] - $t configure_column 0 -header_colspans [list all] + $t configure_column 0 -header_colspans [list any] $t configure -ansibase_header [a+ {*}$fc web-black Web-white] lappend grouptables [$t print] $t destroy @@ -1919,7 +1922,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t configure -frametype block $t configure_column 0 -headers [list "X11"] - $t configure_column 0 -header_colspans [list all] + $t configure_column 0 -header_colspans [list any] $t configure -ansibase_header [a+ {*}$fc web-black Web-white] lappend comparetables [$t print] $t destroy @@ -1940,7 +1943,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t configure -frametype block $t configure_column 0 -headers [list "Web"] - $t configure_column 0 -header_colspans [list all] + $t configure_column 0 -header_colspans [list any] $t configure -ansibase_header [a+ {*}$fc web-black Web-white] lappend comparetables [$t print] $t destroy @@ -2013,39 +2016,39 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu package require overtype ;# circular dependency - many components require overtype. Here we only need it for nice layout in the a? query proc - so we'll do a soft-dependency by only loading when needed and also wrapping in a try package require textblock - append out [textblock::join $indent [tcl::string::map $strmap $settings_applied]] \n - append out [textblock::join $indent [tcl::string::trim $SGR_colour_map \n]] \n - append out [textblock::join $indent "Example: \[a+ bold red White underline\]text\[a] -> [a+ bold red White underline]text[a]"] \n \n + append out [textblock::join -- $indent [tcl::string::map $strmap $settings_applied]] \n + append out [textblock::join -- $indent [tcl::string::trim $SGR_colour_map \n]] \n + append out [textblock::join -- $indent "Example: \[a+ bold red White underline\]text\[a] -> [a+ bold red White underline]text[a]"] \n \n set bgname "Web-white" set map1 [colourmap1 -bg $bgname -forcecolour $opt_forcecolour] set map1 [overtype::centre -transparent 1 $map1 "[a {*}$fc black $bgname]Standard colours[a]"] set map2 [colourmap2 -bg $bgname -forcecolour $opt_forcecolour] set map2 [overtype::centre -transparent 1 $map2 "[a {*}$fc black $bgname]High-intensity colours[a]"] - append out [textblock::join $indent [textblock::join -- $map1 $map2]] \n + append out [textblock::join -- $indent [textblock::join -- $map1 $map2]] \n append out "[a+ {*}$fc web-white]216 colours of 256 terminal colours (To see names, use: a? term ?pastel? ?rainbow?)[a]" \n - append out [textblock::join $indent [colourblock_216 -forcecolour $opt_forcecolour]] \n + append out [textblock::join -- $indent [colourblock_216 -forcecolour $opt_forcecolour]] \n append out "[a+ {*}$fc web-white]24 Greyscale colours[a]" \n - append out [textblock::join $indent [colourblock_24 -forcecolour $opt_forcecolour]] \n + append out [textblock::join -- $indent [colourblock_24 -forcecolour $opt_forcecolour]] \n append out \n - append out [textblock::join $indent "Example: \[a+ Term-92 term-49\]text\[a] -> [a+ {*}$fc Term-92 term-49]text[a]"] \n - append out [textblock::join $indent "Example: \[a+ Term-lightsteelblue term-gold1\]text\[a] -> [a+ {*}$fc Term-lightsteelblue term-gold1]text[a]"] \n - append out [textblock::join $indent "Example: \[a+ term-lightsteelblue Term-gold1\]text\[a] -> [a+ {*}$fc term-lightsteelblue Term-gold1]text[a]"] \n + append out [textblock::join -- $indent "Example: \[a+ Term-92 term-49\]text\[a] -> [a+ {*}$fc Term-92 term-49]text[a]"] \n + append out [textblock::join -- $indent "Example: \[a+ Term-lightsteelblue term-gold1\]text\[a] -> [a+ {*}$fc Term-lightsteelblue term-gold1]text[a]"] \n + append out [textblock::join -- $indent "Example: \[a+ term-lightsteelblue Term-gold1\]text\[a] -> [a+ {*}$fc term-lightsteelblue Term-gold1]text[a]"] \n append out \n append out "[a+ {*}$fc web-white]16 Million colours[a]" \n #tcl::dict::set WEB_colour_map mediumvioletred 199-21-133 ;# #C71585 - append out [textblock::join $indent "Example: \[a+ rgb-199-21-133\]text\[a] -> [a+ {*}$fc rgb-199-21-133]text[a]"] \n - append out [textblock::join $indent "Example: \[a+ Rgb#C71585\]text\[a] -> [a+ {*}$fc Rgb#C71585]text[a]"] \n - append out [textblock::join $indent "Examine a sequence: a? bold rgb-46-139-87 Rgb#C71585 "] \n + append out [textblock::join -- $indent "Example: \[a+ rgb-199-21-133\]text\[a] -> [a+ {*}$fc rgb-199-21-133]text[a]"] \n + append out [textblock::join -- $indent "Example: \[a+ Rgb#C71585\]text\[a] -> [a+ {*}$fc Rgb#C71585]text[a]"] \n + append out [textblock::join -- $indent "Examine a sequence: a? bold rgb-46-139-87 Rgb#C71585 "] \n append out \n append out "[a+ {*}$fc web-white]Web colours[a]" \n - append out [textblock::join $indent "To see all names use: a? web"] \n - append out [textblock::join $indent "To see specific colour groups use: a? web groupname1 groupname2..."] \n - append out [textblock::join $indent "Valid group names (can be listed in any order): basic pink red orange yellow brown purple blue cyan green white grey"] \n + append out [textblock::join -- $indent "To see all names use: a? web"] \n + append out [textblock::join -- $indent "To see specific colour groups use: a? web groupname1 groupname2..."] \n + append out [textblock::join -- $indent "Valid group names (can be listed in any order): basic pink red orange yellow brown purple blue cyan green white grey"] \n append out \n - append out [textblock::join $indent "Example: \[a+ Web-springgreen web-crimson\]text\[a] -> [a+ {*}$fc Web-springgreen web-coral]text[a]"] \n + append out [textblock::join -- $indent "Example: \[a+ Web-springgreen web-crimson\]text\[a] -> [a+ {*}$fc Web-springgreen web-coral]text[a]"] \n append out \n append out "[a+ {*}$fc web-white]X11 colours[a] - mostly match Web colours" \n - append out [textblock::join $indent "To see differences: a? x11"] \n + append out [textblock::join -- $indent "To see differences: a? x11"] \n if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { append out \n @@ -2261,15 +2264,29 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set sgr_cache [tcl::dict::create] #sgr_cache clear called by punk::console::ansi when set to off - proc sgr_cache {{action ""}} { + proc sgr_cache {args} { + set argd [punk::args::get_dict { + *proc -name punk::ansi::sgr_cache -help "Convenience function to view and optionally clear the ansi character attribute cache (ansi SGR codes) + " + -action -default "" -choices "clear" -help "-action clear will unset the keys in the punk::ansi::sgr_cache dict + This is called automatically when setting 'colour false' in the console" + + -pretty -default 1 -type boolean -help "use 'pdict punk::ansi::sgr_cache */%str,%ansiview' output" + *values -min 0 -max 0 + } $args] + set action [dict get $argd opts -action] + set pretty [dict get $argd opts -pretty] + variable sgr_cache - if {$action ni {"" clear}} { - error "sgr_cache action '$action' not understood. Valid actions: clear" - } if {$action eq "clear"} { set sgr_cache [tcl::dict::create] return "sgr_cache cleared" } + if {$pretty} { + #return [pdict -channel none sgr_cache */%str,%ansiview] + return [pdict -channel none sgr_cache */%rpadstr-"sample",%ansiviewstyle] + } + if {[catch { set termwidth [tcl::dict::get [punk::console::get_size] columns] } errM]} { @@ -2311,7 +2328,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #function name part of cache-key because a and a+ return slightly different results (a has leading reset) variable sgr_cache - set cache_key a+$args ;#ensure cache_key static - we may remove for example 'forcecolour' from args - but it needs to remain part of cache_key + set cache_key "a+ $args" ;#ensure cache_key static - we may remove for example 'forcecolour' from args - but it needs to remain part of cache_key if {[tcl::dict::exists $sgr_cache $cache_key]} { return [tcl::dict::get $sgr_cache $cache_key] } @@ -2670,7 +2687,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #It's important to put the functionname in the cache-key because a and a+ return slightly different results variable sgr_cache - set cache_key a_$args + set cache_key "a $args" if {[tcl::dict::exists $sgr_cache $cache_key]} { return [tcl::dict::get $sgr_cache $cache_key] } @@ -2681,7 +2698,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu variable TERM_colour_map set colour_disabled 0 - #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache clear + #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache -action clear if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { set colour_disabled 1 } @@ -3381,10 +3398,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #arguably - \b and \r are cursor move operations too - so processing them here is not very symmetrical - review #the purpose of backspace (or line cr) in embedded text is unclear. Should it allow some sort of character combining/overstrike as it has sometimes done historically (nroff/less)? e.g a\b` as an alternative combiner or bolding if same char #This should presumably only be done if the over_strike (os) capability is enabled in the terminal. Either way - it presumably won't affect printing width? - set line [punk::ansi::stripansi $line] + set line [punk::ansi::ansistrip $line] #we can't use simple \b processing if we get ansi codes and aren't actually processing them (e.g moves) - set line [punk::char::strip_nonprinting_ascii $line] ;#only strip nonprinting after stripansi - some like BEL are part of ansi + set line [punk::char::strip_nonprinting_ascii $line] ;#only strip nonprinting after ansistrip - some like BEL are part of ansi #backspace 0x08 only erases* printing characters anyway - so presumably order of processing doesn't matter #(* more correctly - moves cursor back) #Note some terminals process backspace before \v - which seems quite wrong @@ -3500,6 +3517,40 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } + #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks + proc ansistrip {text} { + #*** !doctools + #[call [fun ansistrip] [arg text] ] + #[para]Return a string with ansi codes stripped out + #[para]Alternate graphics chars are replaced with modern unicode equivalents (e.g boxdrawing glyphs) + + if {[punk::ansi::ta::detect_g0 $text]} { + set text [convert_g0 $text];#Convert ansi borders to unicode line drawing instead of ascii letters + } + set parts [punk::ansi::ta::split_codes $text] + set out "" + foreach {pt code} $parts { + append out $pt + } + return $out + } + #interp alias {} stripansi {} ::punk::ansi::ansistrip + proc ansistripraw {text} { + #*** !doctools + #[call [fun ansistripraw] [arg text] ] + #[para]Return a string with ansi codes stripped out + #[para]Alternate graphics modes will be stripped rather than converted to unicode - exposing the raw ascii characters as they appear without graphics mode. + #[para]ie instead of a horizontal line you may see: qqqqqq + + set parts [punk::ansi::ta::split_codes $text] + set out "" + foreach {pt code} $parts { + append out $pt + } + return $out + } + #interp alias {} stripansiraw {} ::punk::ansi::ansistripraw + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::ansi ---}] } @@ -4281,16 +4332,16 @@ tcl::namespace::eval punk::ansi::ta { #*** !doctools #[call [fun strip] [arg text]] #[para]Return text stripped of Ansi codes - #[para]This is a tailcall to punk::ansi::stripansi - tailcall stripansi $text + #[para]This is a tailcall to punk::ansi::ansistrip + tailcall ansistrip $text } proc length {text} { #*** !doctools #[call [fun length] [arg text]] #[para]Return the character length after stripping ansi codes - not the printing length - #we can use stripansiraw to avoid g0 conversion - as the length should remain the same - tcl::string::length [stripansiraw $text] + #we can use ansistripraw to avoid g0 conversion - as the length should remain the same + tcl::string::length [ansistripraw $text] } #todo - handle newlines #not in perl ta @@ -5439,11 +5490,8 @@ tcl::namespace::eval punk::ansi::class { } } tcl::namespace::eval punk::ansi { - proc stripansi {text} [string map [list $::punk::ansi::ta::re_ansi_split] { - #*** !doctools - #[call [fun stripansi] [arg text] ] - #[para]Return a string with ansi codes stripped out - #[para]Alternate graphics chars are replaced with modern unicode equivalents (e.g boxdrawing glyphs) + + proc stripansi3 {text} [string map [list $::punk::ansi::ta::re_ansi_split] { #using detect costs us a couple of uS - but saves time on plain text #we should probably leave this for caller - otherwise it ends up being called more than necessary @@ -5459,12 +5507,7 @@ tcl::namespace::eval punk::ansi { punk::ansi::ta::Do_split_at_codes_join $text {} }] - proc stripansiraw {text} [string map [list $::punk::ansi::ta::re_ansi_split] { - #*** !doctools - #[call [fun stripansi] [arg text] ] - #[para]Return a string with ansi codes stripped out - #[para]Alternate graphics modes will be stripped rather than converted to unicode - exposing the raw ascii characters as they appear without graphics mode. - #[para]ie instead of a horizontal line you may see: qqqqqq + proc stripansiraw3 {text} [string map [list $::punk::ansi::ta::re_ansi_split] { #join [::punk::ansi::ta::split_at_codes $text] "" punk::ansi::ta::Do_split_at_codes_join $text {} @@ -5890,7 +5933,7 @@ tcl::namespace::eval punk::ansi::ansistring { #[para]Returns the count of visible graphemes and non-ansi control characters #[para]Incomplete! grapheme clustering support not yet implemented - only diacritics are currently clustered to count as one grapheme. #[para]This will not count strings hidden inside a 'privacy message' or other ansi codes which may have content between their opening escape and their termination sequence. - #[para]This is not quite equivalent to calling string length on the result of stripansi $string due to diacritics and/or grapheme combinations + #[para]This is not quite equivalent to calling string length on the result of ansistrip $string due to diacritics and/or grapheme combinations #[para]Note that this returns the number of characters in the payload (after applying combiners) #It is not always the same as the width of the string as rendered on a terminal due to 2wide Unicode characters and the usual invisible control characters such as \r and \n #[para]To get the width, use punk::ansi::printing_length instead, which is also ansi aware. @@ -5902,17 +5945,17 @@ tcl::namespace::eval punk::ansi::ansistring { set string [regsub -all $re_diacritics $string ""] #we want length to return number of glyphs.. not screen width. Has to be consistent with index function - tcl::string::length [stripansi $string] + tcl::string::length [ansistrip $string] } #included as a test/verification - slightly slower. #grapheme split version may end up being used once it supports unicode grapheme clusters proc count2 {string} { #we want count to return number of glyphs.. not screen width. Has to be consistent with index function - return [llength [punk::char::grapheme_split [stripansi $string]]] + return [llength [punk::char::grapheme_split [ansistrip $string]]] } proc length {string} { - tcl::string::length [stripansi $string] + tcl::string::length [ansistrip $string] } proc _splits_trimleft {sclist} { @@ -6022,9 +6065,9 @@ tcl::namespace::eval punk::ansi::ansistring { #[para]The string could contain non SGR ansi codes - and these will (mostly) be ignored, so shouldn't affect the output. #[para]Some terminals don't hide 'privacy message' and other strings within an ESC X ESC ^ or ESC _ sequence (terminated by ST) #[para]It's arguable some of these are application specific - but this function takes the view that they are probably non-displaying - so index won't see them. - #[para]If the caller wants just the character - they should use a normal string index after calling stripansi, or call stripansi afterwards. - #[para]As any operation using end-+ will need to strip ansi to precalculate the length anyway; the caller should probably just use stripansi and standard string index if the ansi coded output isn't required and they are using and end-based index. - #[para]In fact, any operation where the ansi info isn't required in the output would probably be slightly more efficiently obtained by using stripansi and normal string operations on that. + #[para]If the caller wants just the character - they should use a normal string index after calling ansistrap, or call ansistrip afterwards. + #[para]As any operation using end-+ will need to strip ansi to precalculate the length anyway; the caller should probably just use ansistrip and standard string index if the ansi coded output isn't required and they are using and end-based index. + #[para]In fact, any operation where the ansi info isn't required in the output would probably be slightly more efficiently obtained by using ansistrip and normal string operations on that. #[para]The returned character will (possibly) have a leading ansi escape sequence but no trailing escape sequence - even if the string was taken from a position immediately before a reset or other SGR ansi code #[para]The ansi-code prefix in the returned string is built up by concatenating previous SGR ansi codes seen - but it is optimised to re-start the process if any full SGR reset is encountered. #[para]The code sequence doesn't detect individual properties being turned on and then off again, only full resets; so in some cases the ansi-prefix may not be as short as it could be. diff --git a/src/bootsupport/modules/punk/args-0.1.0.tm b/src/bootsupport/modules/punk/args-0.1.0.tm index e148a2aa..5e270ac8 100644 --- a/src/bootsupport/modules/punk/args-0.1.0.tm +++ b/src/bootsupport/modules/punk/args-0.1.0.tm @@ -267,6 +267,9 @@ tcl::namespace::eval punk::args { #[list_begin definitions] + #todo? -synonym ? (applies to opts only not values) + #e.g -background -synonym -bg -default White + proc Get_argspecs {optionspecs args} { variable argspec_cache variable argspecs @@ -332,7 +335,8 @@ tcl::namespace::eval punk::args { set in_record 0 foreach rawline $linelist { set recordsofar [tcl::string::cat $linebuild $rawline] - if {![tcl::info::complete $recordsofar]} { + #ansi colours can stop info complete from working (contain square brackets) + if {![tcl::info::complete [punk::ansi::ansistrip $recordsofar]]} { #append linebuild [string trimleft $rawline] \n if {$in_record} { if {[tcl::string::length $lastindent]} { @@ -436,6 +440,9 @@ tcl::namespace::eval punk::args { } none - any - ansistring { + } + list { + } default { #todo - disallow unknown types unless prefixed with custom- @@ -494,6 +501,9 @@ tcl::namespace::eval punk::args { } dict - dictionary { set v dict + } + list { + } default { #todo - disallow unknown types unless prefixed with custom- @@ -568,7 +578,9 @@ tcl::namespace::eval punk::args { "" - none { if {$is_opt} { tcl::dict::set spec_merged -type none - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { + tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + } lappend opt_solos $argname } else { #-solo only valid for flags @@ -687,6 +699,8 @@ tcl::namespace::eval punk::args { } proc arg_error {msg spec_dict {badarg ""}} { + # use basic colours here to support terminals without extended colours + #todo - add checks column (e.g -minlen -maxlen) set errmsg $msg if {![catch {package require textblock}]} { if {[catch { @@ -694,18 +708,21 @@ tcl::namespace::eval punk::args { set procname [punk::lib::dict_getdef $spec_dict proc_info -name ""] set prochelp [punk::lib::dict_getdef $spec_dict proc_info -help ""] - set t [textblock::class::table new [a+ web-yellow]Usage[a]] + #set t [textblock::class::table new [a+ web-yellow]Usage[a]] + set t [textblock::class::table new [a+ brightyellow]Usage[a]] set blank_header_col [list ""] if {$procname ne ""} { lappend blank_header_col "" - set procname_display [a+ web-white]$procname[a] + #set procname_display [a+ web-white]$procname[a] + set procname_display [a+ brightwhite]$procname[a] } else { set procname_display "" } if {$prochelp ne ""} { lappend blank_header_col "" - set prochelp_display [a+ web-white]$prochelp[a] + #set prochelp_display [a+ web-white]$prochelp[a] + set prochelp_display [a+ brightwhite]$prochelp[a] } else { set prochelp_display "" } @@ -728,9 +745,12 @@ tcl::namespace::eval punk::args { $t configure_header 2 -values {Arg Type Default Multiple Help} } - set c_default [a+ web-white Web-limegreen] - set c_badarg [a+ web-crimson] - set greencheck [a+ web-limegreen]\u2713[a] + #set c_default [a+ web-white Web-limegreen] + set c_default [a+ brightwhite Brightgreen] + #set c_badarg [a+ web-crimson] + set c_badarg [a+ brightred] + #set greencheck [a+ web-limegreen]\u2713[a] + set greencheck [a+ brightgreen]\u2713[a] foreach arg [dict get $spec_dict opt_names] { set arginfo [dict get $spec_dict arg_info $arg] @@ -779,7 +799,8 @@ tcl::namespace::eval punk::args { } - $t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ web-white] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] + #$t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ web-white] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] + $t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ brightwhite] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] $t configure -maxwidth 80 append errmsg [$t print] $t destroy @@ -798,7 +819,12 @@ tcl::namespace::eval punk::args { #Also,we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg } - + + #todo - a version of get_dict that supports punk::lib::tstr templating + #rename get_dict + #provide ability to look up and reuse definitions from ids etc + # + #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. #only supports -flag val pairs, not solo options @@ -849,7 +875,7 @@ tcl::namespace::eval punk::args { #this would be important in the case where the function to be wrapped has never been called - but the wrapper needs info about the downstream options #we would like to avoid the ugliness of trying to parse a proc body to scrape the specification. #we may still need to do a basic scan of the proc body to determine if it at least contains the string punk::args::get_dict - but that is slightly less odious. - error "unsupported" + error "unsupported number of arguments for punk::args::get_dict" set inopt 0 set k "" set i 0 @@ -887,8 +913,12 @@ tcl::namespace::eval punk::args { #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. #-default value must not be appended to if argname not yet in flagsreceived + + #todo: -minmultiple -maxmultiple ? + set opts $opt_defaults if {[set eopts [lsearch -exact $rawargs "--"]] >= 0} { + lappend flagsreceived -- set values [lrange $rawargs $eopts+1 end] set arglist [lrange $rawargs 0 $eopts-1] set maxidx [expr {[llength $arglist]-1}] @@ -908,7 +938,7 @@ tcl::namespace::eval punk::args { #review - what if user sets first value that happens to match a default? if {$fullopt ni $flagsreceived && [tcl::dict::exists $opt_defaults $fullopt] && ([tcl::dict::get $opt_defaults $fullopt] eq [tcl::dict::get $opts $fullopt])} { #first occurrence of this flag, whilst stored value matches default - tcl::dict::set opts $fullopt $flagval + tcl::dict::set opts $fullopt [list $flagval] } else { tcl::dict::lappend opts $fullopt $flagval } @@ -997,7 +1027,7 @@ tcl::namespace::eval punk::args { #review - what if user sets first value that happens to match a default? if {$fullopt ni $flagsreceived && [tcl::dict::exists $opt_defaults $fullopt] && ([tcl::dict::get $opt_defaults $fullopt] eq [tcl::dict::get $opts $fullopt])} { #first occurrence of this flag, whilst stored value matches default - tcl::dict::set opts $fullopt $flagval + tcl::dict::set opts $fullopt [list $flagval] } else { tcl::dict::lappend opts $fullopt $flagval } @@ -1079,7 +1109,7 @@ tcl::namespace::eval punk::args { if {[tcl::dict::get $arg_info $valname -multiple]} { if {[tcl::dict::exists $val_defaults $valname] && ([tcl::dict::get $val_defaults $valname] eq [tcl::dict::get $values_dict $valname])} { #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname $val + tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list } else { tcl::dict::lappend values_dict $valname $val } @@ -1146,6 +1176,7 @@ tcl::namespace::eval punk::args { } + #todo - truncate/summarize values in error messages #todo - allow defaults outside of choices/ranges @@ -1189,7 +1220,7 @@ tcl::namespace::eval punk::args { package require punk::ansi set vlist_check [list] foreach e $vlist { - lappend vlist_check [punk::ansi::stripansi $e] + lappend vlist_check [punk::ansi::ansistrip $e] } } else { #validate_without_ansi 0 @@ -1205,6 +1236,9 @@ tcl::namespace::eval punk::args { } if {$is_default eq [llength $vlist]} { set is_default 1 + } else { + #important to set 0 here too e.g if only one element of many matches default + set is_default 0 } } #puts "argname:$argname v:$v is_default:$is_default" @@ -1214,6 +1248,32 @@ tcl::namespace::eval punk::args { if {$is_default == 0} { switch -- $type { any {} + list { + foreach e_check $vlist_check { + if {![tcl::string::is list -strict $e_check]} { + arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs $argname + } + if {[tcl::dict::size $thisarg_checks]} { + tcl::dict::for {checkopt checkval} $thisarg_checks { + switch -- $checkopt { + -minlen { + # -1 for disable is as good as zero + if {[llength $e_check] < $checkval} { + arg_error "Option $argname for [Get_caller] requires list with -minlen $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs $argname + } + } + -maxlen { + if {$checkval ne "-1"} { + if {[llength $e_check] > $checkval} { + arg_error "Option $argname for [Get_caller] requires list with -maxlen $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs $argname + } + } + } + } + } + } + } + } string { if {[tcl::dict::size $thisarg_checks]} { foreach e_check $vlist_check { @@ -1295,6 +1355,25 @@ tcl::namespace::eval punk::args { if {[llength $e_check] %2 != 0} { arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs $argname } + if {[tcl::dict::size $thisarg_checks]} { + tcl::dict::for {checkopt checkval} $thisarg_checks { + switch -- $checkopt { + -minlen { + # -1 for disable is as good as zero + if {[tcl::dict::size $e_check] < $checkval} { + arg_error "Option $argname for [Get_caller] requires dict with -minlen $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs $argname + } + } + -maxlen { + if {$checkval ne "-1"} { + if {[tcl::dict::size $e_check] > $checkval} { + arg_error "Option $argname for [Get_caller] requires dict with -maxlen $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs $argname + } + } + } + } + } + } } } alnum - @@ -1369,7 +1448,7 @@ tcl::namespace::eval punk::args { } } if {$is_strip_ansi} { - set stripped_list [lmap e $vlist {punk::ansi::stripansi $e}] ;#no faster or slower, but more concise than foreach + set stripped_list [lmap e $vlist {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach if {[tcl::dict::get $thisarg -multiple]} { if {[tcl::dict::get $thisarg -ARGTYPE] eq "option"} { tcl::dict::set opts $argname $stripped_list diff --git a/src/bootsupport/modules/punk/char-0.1.0.tm b/src/bootsupport/modules/punk/char-0.1.0.tm index e8752c06..ed4b22e4 100644 --- a/src/bootsupport/modules/punk/char-0.1.0.tm +++ b/src/bootsupport/modules/punk/char-0.1.0.tm @@ -1950,7 +1950,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} @@ -2057,7 +2057,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} @@ -2161,7 +2161,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} diff --git a/src/bootsupport/modules/punk/console-0.1.1.tm b/src/bootsupport/modules/punk/console-0.1.1.tm index 832232bd..3c64c7e3 100644 --- a/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/bootsupport/modules/punk/console-0.1.1.tm @@ -51,7 +51,7 @@ namespace eval punk::console { variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run. #-1 still evaluates to true - as the modern assumption for ansi availability is true #only false if ansi_available has been set 0 by test_can_ansi - #support stripansi for legacy windows terminals + #support ansistrip for legacy windows terminals # -- variable ansi_wanted 2 ;#2 for default assumed yes, will be set to -1 for automatically unwanted when ansi unavailable values of 0 or 1 won't be autoset @@ -780,7 +780,7 @@ namespace eval punk::console { #stdout variable ansi_wanted if {$ansi_wanted <= 0} { - puts -nonewline [punk::ansi::stripansiraw [::punk::ansi::a?]] + puts -nonewline [punk::ansi::ansistripraw [::punk::ansi::a?]] } else { tailcall ansi::a? {*}$args } @@ -806,7 +806,7 @@ namespace eval punk::console { proc code_a? {args} { variable ansi_wanted if {$ansi_wanted <= 0} { - return [punk::ansi::stripansi [::punk::ansi::a? {*}$args]] + return [punk::ansi::ansistripraw [::punk::ansi::a? {*}$args]] } else { tailcall ::punk::ansi::a? {*}$args } @@ -833,7 +833,7 @@ namespace eval punk::console { false - no { set ansi_wanted 0 - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear } default { set ansi_wanted 2 @@ -859,7 +859,7 @@ namespace eval punk::console { if {$on} { if {$colour_disabled} { #change of state - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear catch {punk::repl::reset_prompt} set colour_disabled 0 } @@ -867,7 +867,7 @@ namespace eval punk::console { #we don't disable a/a+ entirely - they must still emit underlines/bold/reverse if {!$colour_disabled} { #change of state - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear catch {punk::repl::reset_prompt} set colour_disabled 1 } @@ -1811,7 +1811,6 @@ interp alias {} colour {} punk::console::colour interp alias {} ansi {} punk::console::ansi interp alias {} color {} punk::console::colour interp alias {} a+ {} punk::console::code_a+ -interp alias {} a= {} punk::console::code_a interp alias {} a {} punk::console::code_a interp alias {} a? {} punk::console::code_a? diff --git a/src/bootsupport/modules/punk/fileline-0.1.0.tm b/src/bootsupport/modules/punk/fileline-0.1.0.tm index 837b9821..7e1ee14c 100644 --- a/src/bootsupport/modules/punk/fileline-0.1.0.tm +++ b/src/bootsupport/modules/punk/fileline-0.1.0.tm @@ -318,7 +318,7 @@ namespace eval punk::fileline::class { package require overtype # will require punk::char and punk::ansi - if {"::punk::fileline::ansi::stripansi" ne [info commands ::punk::fileline::ansi::stripansi]} { + if {"::punk::fileline::ansi::ansistrip" ne [info commands ::punk::fileline::ansi::ansistrip]} { namespace eval ::punk::fileline::ansi { namespace import ::punk::ansi::* } @@ -334,7 +334,7 @@ namespace eval punk::fileline::class { } else { set ::punk::fileline::ansi::enabled 0 } - if {"::punk::fileline::stripansi" ne [info commands ::punk::fileline::stripansi]} { + if {"::punk::fileline::ansistrip" ne [info commands ::punk::fileline::ansistrip]} { proc ::punk::fileline::a {args} { if {$::punk::fileline::ansi::enabled} { tailcall ::punk::fileline::ansi::a {*}$args @@ -349,9 +349,9 @@ namespace eval punk::fileline::class { return "" } } - proc ::punk::fileline::stripansi {str} { + proc ::punk::fileline::ansistrip {str} { if {$::punk::fileline::ansi::enabled} { - tailcall ::punk::fileline::ansi::stripansi $str + tailcall ::punk::fileline::ansi::ansistrip $str } else { return $str } @@ -560,7 +560,7 @@ namespace eval punk::fileline::class { set title_line "Line" #todo - use punk::char for unicode support of wide chars etc? set widest_linenum [tcl::mathfunc::max {*}[lmap v [concat [list $title_linenum] $linenums] {string length $v}]] - set widest_marker [tcl::mathfunc::max {*}[lmap v [concat [list $title_marker] $markers] {string length [stripansi $v]}]] + set widest_marker [tcl::mathfunc::max {*}[lmap v [concat [list $title_marker] $markers] {string length [ansistrip $v]}]] set widest_status [expr {max([string length $opt_cmark], [string length $opt_tmark])}] set widest_line [tcl::mathfunc::max {*}[lmap v [concat [list $title_line] $lines] {string length $v}]] foreach row $result_list { @@ -1259,18 +1259,17 @@ namespace eval punk::fileline { #[para]The encoding used is as specified in the -encoding option - or from the Byte Order Mark (bom) at the beginning of the data #[para]For Tcl 8.6 - encodings such as utf-16le may not be available - so the bytes are swapped appropriately depending on the platform byteOrder and encoding 'unicode' is used. #[para]encoding defaults to utf-8 if no -encoding specified and no BOM was found - #[para]Specify -encoding binary to perform no encoding conversion #[para]Whether -encoding was specified or not - by default the BOM characters are not retained in the line-data #[para]If -includebom 1 is specified - the bom will be retained in the stored chunk and the data for line 1, but will undergo the same encoding transformation as the rest of the data #[para]The get_bomid method of the returned object will contain an identifier for any BOM encountered. #[para] e.g utf-8,utf-16be, utf-16le, utf-32be, utf32-le, SCSU, BOCU-1,GB18030, UTF-EBCDIC, utf-1, utf-7 - #[para]If the encoding specified in the BOM isn't recognised by Tcl - the resulting data is likely to remain as the raw bytes (binary translation) + #[para]If the encoding specified in the BOM isn't recognised by Tcl - the resulting data is likely to remain as the raw bytes of whatever encoding that is. #[para]Currently only utf-8, utf-16* and utf-32* are properly supported even though the other BOMs are detected, reported via get_bomid, and stripped from the data. - #[para]GB18030 falls back to cp936/gbk (unless a gb18030 encoding has been installed). Use -encoding binary if this isn't suitable and you need to do your own processing of the raw data. + #[para]GB18030 falls back to cp936/gbk (unless a gb18030 encoding has been installed). Use -encoding iso8859-1 if this isn't suitable and you need to do your own processing of the bytes. set argument_specification { -file -default {} -type existingfile - -translation -default binary + -translation -default iso8859-1 -encoding -default "\uFFFF" -includebom -default 0 *values -min 0 -max 1 @@ -1712,7 +1711,7 @@ namespace eval punk::fileline::ansi { #*** !doctools #[call [fun ansi::a]] #[call [fun ansi::a+]] - #[call [fun ansi::stripansi]] + #[call [fun ansi::ansistrip]] #*** !doctools #[list_end] [comment {--- end definitions namespace punk::fileline::ansi ---}] diff --git a/src/bootsupport/modules/punk/lib-0.1.1.tm b/src/bootsupport/modules/punk/lib-0.1.1.tm index 3d0332b5..3a5764b5 100644 --- a/src/bootsupport/modules/punk/lib-0.1.1.tm +++ b/src/bootsupport/modules/punk/lib-0.1.1.tm @@ -66,34 +66,34 @@ package require Tcl 8.6- # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # oo::class namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::lib::class { - #*** !doctools - #[subsection {Namespace punk::lib::class}] - #[para] class definitions - if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { - #*** !doctools - #[list_begin enumerated] - - # oo::class create interface_sample1 { - # #*** !doctools - # #[enum] CLASS [class interface_sample1] - # #[list_begin definitions] - - # method test {arg1} { - # #*** !doctools - # #[call class::interface_sample1 [method test] [arg arg1]] - # #[para] test method - # puts "test: $arg1" - # } - - # #*** !doctools - # #[list_end] [comment {-- end definitions interface_sample1}] - # } - - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - } -} +#tcl::namespace::eval punk::lib::class { +# #*** !doctools +# #[subsection {Namespace punk::lib::class}] +# #[para] class definitions +# if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { +# #*** !doctools +# #[list_begin enumerated] +# +# # oo::class create interface_sample1 { +# # #*** !doctools +# # #[enum] CLASS [class interface_sample1] +# # #[list_begin definitions] +# +# # method test {arg1} { +# # #*** !doctools +# # #[call class::interface_sample1 [method test] [arg arg1]] +# # #[para] test method +# # puts "test: $arg1" +# # } +# +# # #*** !doctools +# # #[list_end] [comment {-- end definitions interface_sample1}] +# # } +# +# #*** !doctools +# #[list_end] [comment {--- end class enumeration ---}] +# } +#} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::lib::ensemble { @@ -395,105 +395,896 @@ namespace eval punk::lib { } } - proc pdict {args} { + #experiment with equiv of js template literals with ${expression} in templates + #e.g tstr {This is the value of x in calling scope ${$x} !} + #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} + #e.g tstr -allowcommands {This is the value of [lindex $x 0] in calling scope ${[lindex [set x] 0]} !} + proc tstr {args} { + set argd [punk::args::get_dict { + *proc -name punk::lib::tstr -help "A rough equivalent of js template literals" + -allowcommands -default 0 -type none -help "if -allowcommands is true placeholder can contain commands e.g {${plaintext1 [lindex $var 0] plaintext2}}" + -return -default list -choices {dict list string} + *values -min 1 -max 1 + templatestring -help "This argument should be a braced string containing placeholders such as ${$var} e.g {The value is ${$var}} + where $var will be substituted from the calling context + The placeholder itself can contain plaintext portions as well as variables. + It can contain commands in square brackets if -allowcommands is true" + } $args] + set templatestring [dict get $argd values templatestring] + set opt_allowcommands [dict get $argd opts -allowcommands] + set opt_return [dict get $argd opts -return] + set nocommands "-nocommands" + if {$opt_allowcommands == 1} { + set nocommands "" + } + + #set parts [_tstr_split $templatestring] + set parts [_parse_tstr_parts $templatestring] + set textchunks [list] + #set expressions [list] + set params [list] + set idx 0 + foreach {pt expression} $parts { + lappend textchunks $pt + incr idx ;#pt incr + + #ignore last expression + if {$idx == [llength $parts]} { + break + } + #lappend expressions $expression + lappend params [uplevel 1 [list subst {*}$nocommands $expression]] + + incr idx ;#expression incr + } + switch -- $opt_return { + dict { + return [dict create template $textchunks params $params] + } + list { + return [list $textchunks {*}$params] + } + string { + set out "" + foreach pt $textchunks param $params { + append out $pt $param + } + return $out + } + default { + } + } + } + #test single placeholder tstr args where single placeholder must be an int + proc tstr_test_one {args} { + set argd [punk::args::get_dict { + *proc -name tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. + example: + set id 2 + tstr_test_one {*}[Tstr {Select * from table where id = ${$id} and etc... ;}] + } + + *values -min 2 -max 2 + template -type list -minlen 2 -maxlen 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - + but the Tstr method above does this for you, and also passes in the id automatically" + + where -type int -help {Integer param for where clause. Tstr mechanism above will pass the id as the second parameter} + } $args] + set template [dict get $argd values template] + set where [dict get $argd values where] + set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] + return $result + } + proc _parse_tstr_parts {templatestring} { + if {$templatestring eq ""} { + return [list] + } + set chars [split $templatestring ""] + set in_placeholder 0 + set tchars "" + set echars "" + set parts [list] + set i 0 + foreach ch $chars { + if {!$in_placeholder} { + set nextch [lindex $chars [expr {$i+1}]] + if {"$ch$nextch" eq "\$\{"} { + set in_placeholder 2 ;#2 to signify we just entered placeholder + lappend parts $tchars + set tchars "" + } else { + append tchars $ch + } + } else { + if {$ch eq "\}"} { + if {[tcl::info::complete $echars]} { + set in_placeholder 0 + lappend parts $echars + set echars "" + } else { + append echars $ch + } + } else { + if {$in_placeholder == 2} { + #skip opening bracket + set in_placeholder 1 + } else { + append echars $ch + } + } + } + incr i + } + if {$tchars ne ""} { + lappend parts $tchars + } + if {[llength $parts] % 2 == 0} { + #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list + lappend parts "" + } + return $parts + } + #based on punk::ansi::ta::_perlish_split + proc _tstr_split {text} { + if {$text eq ""} { + return {} + } + set list [list] + set start 0 + #ideally re should allow curlies within but we will probably need a custom parser to do it + #(js allows nested string interpolation) + #set re {\$\{[^\}]*\}} + set re {\$\{(?:(?!\$\{).)*\}} + + #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code + + #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW + while {[regexp -start $start -indices -- $re $text match]} { + lassign $match matchStart matchEnd + #puts "->start $start ->match $matchStart $matchEnd" + if {$matchEnd < $matchStart} { + puts "e:$matchEnd < s:$matchStart" + lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] + incr start + if {$start >= [tcl::string::length $text]} { + break + } + continue + } + lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] + set start [expr {$matchEnd+1}] + #? + if {$start >= [tcl::string::length $text]} { + break + } + } + return [lappend list [tcl::string::range $text $start end]] + } + + #get info about punk nestindex key ie type: list,dict,undetermined + proc nestindex_info {args} { set argd [punk::args::get_dict { + -parent -default "" + nestindex + } $args] + set opt_parent [dict get $argd opts -parent] + if {$opt_parent eq ""} { + set parent_type undetermined + } else { + set parent_type [nestindex_info -parent "" $opt_parent] ;#make sure we explicitly set parent of parent to empty so we don't just recurse forever doing nothing + } + + + } + + + proc pdict {args} { + if {[catch {package require punk::ansi} errM]} { + 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] { *proc -name pdict -help {Print dict keys,values to channel (see also showdict)} + *opts -any 1 + #default separator to provide similarity to tcl's parray function - -separator -default " = " + -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 dict variable" - patterns -type string -default * -multiple 1 - } $args] + + 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 + 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 segement 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. + + The pdict function operates on variable names - passing the value to the showdict function which operates on values + } + }] + #puts stderr "$argspec" + set argd [punk::args::get_dict $argspec $args] + set opts [dict get $argd opts] set dvar [dict get $argd values dictvar] set patterns [dict get $argd values patterns] - set dvalue [uplevel 1 [list set $dvar]] + set isarray [uplevel 1 [list array exists $dvar]] + if {$isarray} { + set dvalue [uplevel 1 [list 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 } + + #TODO - much. + #showdict needs to be able to show different branches which share a root path + #e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates) + # - specify ansi colour per pattern so different branches can be highlighted? + # - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc + # - The current version is incomplete but passably usable. + # - Copy proc and attempt rework so we can get back to this as a baseline for functionality proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - set argd [punk::args::get_dict { - *id punk::lib::pdict - *proc -name punk::lib::pdict -help "display dictionary keys and values" + #set sep " [a+ Web-seagreen]=[a] " + if {[catch {package require punk::ansi} errM]} { + set sep " = " + set RST "" + set sep_mismatch " mismatch " + } else { + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + set RST [punk::ansi::a] + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch[punk::ansi::a] " + } + package require punk ;#we need pipeline pattern matching features + package require textblock + + set argd [punk::args::get_dict [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + *id punk::lib::showdict + *proc -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject -return -default "tailtohead" -choices {tailtohead sidebyside} -channel -default none -trimright -default 1 -type boolean -help "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding " - -separator -default " " -help "Separator column between keys and values" - -ansibase_keys -default "" - -ansibase_values -default "" + -separator -default {%sep%} -help "Separator column between keys and values" + -separator_mismatch -default {%sep_mismatch%} -help "Separator to use when patterns mismatch" + -roottype -default "dict" -help "list,dict,string" + -ansibase_keys -default "" -help "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" + -substructure -default {} + -ansibase_values -default "" + -keytemplates -default {${$key}} -type list -help "list of templates for keys at each level" -keysorttype -default "none" -choices {none dictionary ascii integer real} - -keysortdirection -default ascending -choices {ascending descending} + -keysortdirection -default increasing -choices {increasing decreasing} *values -min 1 -max -1 - dictvalue -type dict -help "dict value" - patterns -default * -type string -multiple 1 -help "key or key glob pattern" - } $args] + dictvalue -type list -help "dict or list value" + patterns -default "*" -type string -multiple 1 -help "key or key glob pattern" + }] $args] set opt_sep [dict get $argd opts -separator] + set opt_mismatch_sep [dict get $argd opts -separator_mismatch] set opt_keysorttype [dict get $argd opts -keysorttype] set opt_keysortdirection [dict get $argd opts -keysortdirection] set opt_trimright [dict get $argd opts -trimright] - set opt_ansibase_key [dict get $argd opts -ansibase_keys] - set opt_ansibase_value [dict get $argd opts -ansibase_values] + set opt_keytemplates [dict get $argd opts -keytemplates] + set opt_ansibase_keys [dict get $argd opts -ansibase_keys] + set opt_ansibase_values [dict get $argd opts -ansibase_values] set opt_return [dict get $argd opts -return] + set opt_roottype [dict get $argd opts -roottype] + set opt_structure [dict get $argd opts -substructure] set dval [dict get $argd values dictvalue] set patterns [dict get $argd values patterns] set result "" + #pattern hierarchy + # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest + # * @1 @0,%#,%str - segments + # a b 1 0 %# %str - keys + + set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated + set pattern_next_substructure [dict create] + set pattern_this_structure [dict create] + + # -- --- --- --- + #REVIEW + #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. + #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). + #todo - determine if there is a more consistent rule-based way to do this rather than adhoc + #e.g pdict something * + #we want the keys from the result as individual lines on lhs + #e.g pdict something @@ + #we want on lhs result on rhs + # = v0 + #e.g pdict something @0-2,@4 + #we currently return: + #0 = v0 + #1 = v1 + #2 = v2 + #4 = v4 + #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) + #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. + #this is a tradeoff that could create surprises and make things messy and/or inconsistent. + #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. + #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys + #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment + #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) + # -- --- --- --- + set filtered_keys [list] - foreach p $patterns { - lappend filtered_keys {*}[dict keys $dval $p] - } - if {$opt_keysorttype eq "none"} { - #we can only get duplicate keys if there are multiple patterns supplied - #ignore keysortdirection - doesn't apply - if {[llength $patterns] > 1} { - #order-maintaining (order of keys as they appear in dict) - set filtered_keys [punk::lib::lunique $filtered_keys] + if {$opt_roottype in {dict list string}} { + #puts "getting keys for roottype:$opt_roottype" + if {[llength $dval]} { + set re_numdashnum {^([-+]{0,1}\d+)-([-+]{0,1}\d+)$} + set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} + foreach pattern_nest $patterns { + set keyset [list] + set keyset_structure [list] + + set segments [split $pattern_nest /] + set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns + #we need to use _split_patterns to separate (e.g to protext commas that appear within quotes) + set patterninfo [punk::_split_patterns $levelpatterns] + #puts stderr "showdict-->_split_patterns: $patterninfo" + foreach v_idx $patterninfo { + lassign $v_idx v idx + #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) + set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern + switch -exact -- $p { + * - "" { + if {$opt_roottype eq "list"} { + set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + dict set pattern_this_structure $p list + } elseif {$opt_roottype eq "dict"} { + set keys [dict keys $dval] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } else { + lappend keyset %string + lappend keyset_structure string + dict set pattern_this_structure $p string + } + } + %# { + dict set pattern_this_structure $p string + lappend keyset %# + lappend keyset_structure string + } + # { + dict set pattern_this_structure $p list + lappend keyset # + lappend keyset_structure list + } + ## { + dict set pattern_this_structure $p dict + lappend keyset [list ## query] + lappend keyset_structure dict + } + @* { + puts ---->HERE<---- + dict set pattern_this_structure $p list + set keys [punk::lib::range 0 [llength $dval]-1] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + } + @@ { + #get first k v from dict + dict set pattern_this_structure $p dict + lappend keyset [list @@ query] + lappend keyset_structure dict + } + @*k@* - @*K@* { + #returns keys only + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + @*.@* { + set keys [dict keys $dval] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } + default { + #puts stderr "===p:$p" + #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! + #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful + #@@"key,etc" should allow any non-whitespace key + switch -glob -- $p { + {@k\*@*} - {@K\*@*} { + #value glob return keys + #set search [string range $p 4 end] + #dict for {k v} $dval { + # if {[string match $search $v]} { + # lappend keyset $k + # } + #} + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + @@* { + #exact match key - review - should raise error to match punk pipe behaviour? + set k [string range $p 2 end] + if {[dict exists $dval $k]} { + lappend keyset $k + lappend keyset_structure dict + } + dict set pattern_this_structure $p dict + } + @k@* - @K@* { + set k [string range $p 3 end] + if {[dict exists $dval $k]} { + lappend keyset $k + lappend keyset_structure dict + } + dict set pattern_this_structure $p dict + } + {@\*@*} { + #return list of values + #set k [string range $p 3 end] + #lappend keyset {*}[dict keys $dval $k] + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*.@*} { + set k [string range $p 4 end] + set keys [dict keys $dval $k] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } + {@v\*@*} - {@V\*@*} { + #value-glob return value + #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*v@*} - {@\*V@*} { + #key-glob return value + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*@*} - {@\*v@*} - {@\*V@} { + #key glob return val + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + @??@* { + #exact key match - no error + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + default { + set this_type $opt_roottype + if {[string match @* $p]} { + #list mode - trim optional list specifier @ + set p [string range $p 1 end] + dict set pattern_this_structure $p list + set this_type list + } elseif {[string match %* $p]} { + dict set pattern_this_structure $p string + lappend keyset $p + lappend keyset_structure string + set this_type string + } + if {$this_type eq "list"} { + dict set pattern_this_structure $p list + if {[string is integer -strict $p]} { + lappend keyset $p + lappend keyset_structure list + } elseif {[string match "?*-?*" $p]} { + #could be either - don't change type + #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers + #now we should map _ to "" first + set p [string map {_ {}} $p] + #lassign [textutil::split::splitx $p {\.\.}] a b + if {![regexp $re_idxdashidx $p _match a b]} { + error "unrecognised pattern $p" + } + set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high + #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds + if {${lower_resolve} == -1} { + #lower bound is above upper list range + #match with decreasing indices is still possible + set lower [expr {[llength $dval]-1}] ;#set to max + } elseif {$lower_resolve == -2} { + set lower 0 + } else { + set lower $lower_resolve + } + set upper [punk::lib::lindex_resolve $dval $b] + if {$upper == -2} { + #upper bound is below list range - + if {$lower_resolve >=-1} { + set upper 0 + } else { + continue + } + } elseif {$upper == -1} { + #use max + set upper [expr {[llength $dval]-1}] + #assert - upper >=0 because we have ruled out empty lists + } + #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order + set keys [punk::lib::range $lower $upper] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + } else { + lappend keyset [list @$p query] + lappend keyset_structure list + } + } elseif {$this_type eq "string"} { + dict set pattern_this_structure $p string + } elseif {$this_type eq "dict"} { + #default equivalent to @\*@* + dict set pattern_this_structure $p dict + #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" + set keys [dict keys $dval $p] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + } else { + puts stderr "list: unrecognised pattern $p" + } + } + } + } + } + } + + # -- --- --- --- + #check next pattern-segment for substructure type to use + # -- --- --- --- + set substructure "" + set pnext [lindex $segments 1] + set patterninfo [punk::_split_patterns $levelpatterns] + if {[llength $patterninfo] == 0} { + # // ? -review - what does this mean? for xpath this would mean at any level + set substructure [lindex $pattern_this_structure end] + } elseif {[llength $patterninfo] == 1} { + # single type in segment e.g /@@something/ + switch -exact $pnext { + "" { + set substructure string + } + @*k@* - @*K@* - @*.@* - ## { + set substructure dict + } + # { + set substructure list + } + ## { + set substructure dict + } + %# { + set substructure string + } + * { + #set substructure $opt_roottype + #set substructure [dict get $pattern_this_structure $pattern_nest] + set substructure [lindex $pattern_this_structure end] + } + default { + switch -glob -- $pnext { + @??@* - @?@* - @@* { + #all 4 or 3 len prefixes bounded by @ are dict + set substructure dict + } + default { + if {[string match @* $pnext]} { + set substructure list + } elseif {[string match %* $pnext]} { + set substructure string + } else { + #set substructure $opt_roottype + #set substructure [dict get $pattern_this_structure $pattern_nest] + set substructure [lindex $pattern_this_structure end] + } + } + } + } + } + } else { + #e.g /@0,%str,.../ + #doesn't matter what the individual types are - we have a list result + set substructure list + } + #puts "--pattern_nest: $pattern_nest substructure: $substructure" + dict set pattern_next_substructure $pattern_nest $substructure + # -- --- --- --- + + if {$opt_keysorttype ne "none"} { + set int_keyset 1 + foreach k $keyset { + if {![string is integer -strict $k]} { + set int_keyset 0 + break + } + } + if {$int_keyset} { + set sortindices [lsort -indices -integer $keyset] + #set keyset [lsort -integer $keyset] + } else { + #set keyset [lsort -$opt_keysorttype $keyset] + set sortindices [lsort -indices -$opt_keysorttype $keyset] + } + set keyset [lmap i $sortindices {lindex $keyset $i}] + set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] + } + + foreach k $keyset { + lappend pattern_key_index $pattern_nest + } + + lappend filtered_keys {*}$keyset + lappend all_keyset_structure {*}$keyset_structure + + #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" + } } + #puts stderr "[dict get $pattern_this_structure $pattern_nest] keys: $filtered_keys" } else { - set filtered_keys [lsort -unique -$opt_keysorttype $opt_keysortdirection $filtered_keys] + puts stdout "unrecognised roottype: $opt_roottype" + return $dval } if {[llength $filtered_keys]} { #both keys and values could have newline characters. #simple use of 'format' won't cut it for more complex dict keys/values #use block::width or our columns won't align in some cases - set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] - set RST [a] switch -- $opt_return { "tailtohead" { #last line of key is side by side (possibly with separator) with first line of value #This is more intelligible when terminal wrapping occurs - and is closer to what happens with parray multiline keys and values #we still pad the key to max width so that the separator appears in the same column - which in the case of wide keys could cause that to wrap for all entries - foreach key $filtered_keys { - lassign [textblock::size $key] _kw kwidth _kh kheight - lassign [textblock::size [dict get $dval $key]] _vw vwidth _vh vheight - set totalheight [expr {$kheight + $vheight -1}] - set blanks_above [string repeat \n [expr {$kheight -1}]] - set blanks_below [string repeat \n [expr {$vheight -1}]] - set sepwidth [textblock::width $opt_sep] - #append result [textblock::pad $opt_ansibase_key$key$RST -width $maxl] $opt_sep $opt_ansibase_value[dict get $dval $key]$RST \n - set kblock [textblock::pad $opt_ansibase_key$key$RST$blanks_below -width $maxl] - set sblock [textblock::pad $blanks_above$opt_sep$blanks_below -width $sepwidth] - set vblock $blanks_above$opt_ansibase_value[dict get $dval $key]$RST - #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace - append result [textblock::join_basic $kblock $sblock $vblock] \n + + set kt [lindex $opt_keytemplates 0] + if {$kt eq ""} { + set kt {${$key}} + } + #set display_keys [lmap k $filtered_keys {tcl::string::map [list %k% $k] $kt}] + set display_keys [lmap key $filtered_keys {tstr -ret string -allowcommands $kt}] + set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] + + set kidx 0 + set last_hidekey 0 + foreach keydisplay $display_keys key $filtered_keys { + set thisval "?" + set hidekey 0 + set pattern_nest [lindex $pattern_key_index $kidx] + set pattern_nest_list [split $pattern_nest /] + #set this_type [dict get $pattern_this_structure $pattern_nest] + #set this_type [dict get $pattern_this_structure $key] + set this_type [lindex $all_keyset_structure $kidx] + #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" + + set is_match 1 ;#whether to display the normal separator or bad-match separator + switch -- $this_type { + dict { + #todo? - slower lsearch if -dupes 1 flag set so we can display duplicate 'keys' if var not a proper dict but rather a dict-shaped list that we want to display as a dict + # - default highlight dupes (ansi underline?) + if {[lindex $key 1] eq "query"} { + set qry [lindex $key 0] + % thisval.= $qry= $dval + } else { + set thisval [tcl::dict::get $dval $key] + } + + #set substructure [lrange $opt_structure 1 end] + + set nextpatterns [list] + #which pattern nest applies to this branch + set nextsub [dict get $pattern_next_substructure $pattern_nest] + if {[llength $pattern_nest_list]} { + set nest [lrange $pattern_nest_list 1 end] + lappend nextpatterns {*}[join $nest /] + } + set nextopts [dict get $argd opts] + + + set subansibasekeys [lrange $opt_ansibase_keys 1 end] + set nextkeytemplates [lrange $opt_keytemplates 1 end] + #dict set nextopts -substructure $nextsub + dict set nextopts -keytemplates $nextkeytemplates + dict set nextopts -ansibase_keys $subansibasekeys + dict set nextopts -roottype $nextsub + dict set nextopts -channel none + #puts stderr "showdict {*}$nextopts $thisval [lindex $args end]" + + if {[llength $nextpatterns]} { + if {[catch { + set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] + } errMsg]} { + #puts stderr ">>> nextpatterns:'$nextpatterns' nextopts:'$nextopts'" + set is_match 0 + } + } + } + list { + if {[string is integer -strict $key]} { + set thisval [lindex $dval $key] + } else { + if {[lindex $key 1] eq "query"} { + set qry [lindex $key 0] + } else { + set qry $key + } + % thisval.= $qry= $dval + } + + set nextpatterns [list] + #which pattern nest applies to this branch + set nextsub [dict get $pattern_next_substructure $pattern_nest] + if {[llength $pattern_nest_list]} { + set nest [lrange $pattern_nest_list 1 end] + lappend nextpatterns {*}[join $nest /] + } + set nextopts [dict get $argd opts] + + dict set nextopts -roottype $nextsub + dict set nextopts -channel none + + #if {![llength $nextpatterns]} { + # set nextpatterns * + #} + if {[llength $nextpatterns]} { + if {[catch { + set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] + } errMsg]} { + set is_match 0 + } + } + } + string { + set hidekey 1 + if {$key eq "%string"} { + set hidekey 1 + set thisval $dval + } elseif {$key eq "%ansiview"} { + set thisval [ansistring VIEW -lf 1 $dval] + } elseif {$key eq "%ansiviewstyle"} { + set thisval [ansistring VIEWSTYLE -lf 1 $dval] + } elseif {[string match *lpad-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + $extra}] + set thisval [textblock::pad $dval -which left -width $width] + } elseif {[string match *lpadstr-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] + set thisval [textblock::pad $dval -which left -width $width -padchar $extra] + } elseif {[string match *rpad-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + $extra}] + set thisval [textblock::pad $dval -which right -width $width] + } elseif {[string match *rpadstr-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] + set thisval [textblock::pad $dval -which right -width $width -padchar $extra] + } else { + if {[lindex $key 1] eq "query"} { + set qry [lindex $key 0] + } else { + set qry $key + } + set thisval $dval + if {[string index $key 0] ne "%"} { + set key %$key + } + % thisval.= $key= $thisval + } + + set nextpatterns [list] + #which pattern nest applies to this branch + set nextsub [dict get $pattern_next_substructure $pattern_nest] + if {[llength $pattern_nest_list]} { + set nest [lrange $pattern_nest_list 1 end] + lappend nextpatterns {*}[join $nest /] + } + #set nextopts [dict get $argd opts] + dict set nextopts -roottype $nextsub + dict set nextopts -channel none + + if {[llength $nextpatterns]} { + set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns] + } + + } + } + if {$this_type eq "string" && $hidekey} { + lassign [textblock::size $thisval] _vw vwidth _vh vheight + #set blanks_above [string repeat \n [expr {$kheight -1}]] + set vblock $opt_ansibase_values$thisval$RST + #append result [textblock::join_basic -- $vblock] + #review - we wouldn't need this space if we had a literal %sp %sp-x ?? + append result " $vblock" + } else { + set ansibase_key [lindex $opt_ansibase_keys 0] + + lassign [textblock::size $keydisplay] _kw kwidth _kh kheight + lassign [textblock::size $thisval] _vw vwidth _vh vheight + + set totalheight [expr {$kheight + $vheight -1}] + set blanks_above [string repeat \n [expr {$kheight -1}]] + set blanks_below [string repeat \n [expr {$vheight -1}]] + + if {$is_match} { + set use_sep $opt_sep + } else { + set use_sep $opt_mismatch_sep + } + + + set sepwidth [textblock::width $use_sep] + set kblock [textblock::pad $ansibase_key$keydisplay$RST$blanks_below -width $maxl] + set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] + set vblock $blanks_above$opt_ansibase_values$thisval$RST + #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace + if {$last_hidekey} { + append result \n + } + append result [textblock::join_basic -- $kblock $sblock $vblock] \n + } + set last_hidekey $hidekey + incr kidx } } "sidebyside" { + #todo #This is nice for multiline keys and values of reasonable length, will produce unintuitive results when line-wrapping occurs. #use ansibase_key etc to make the output more comprehensible in that situation. #This is why it is not the default. (review - terminal width detection and wrapping?) + set maxl [::tcl::mathfunc::max {*}[lmap v $filtered_keys {textblock::width $v}]] foreach key $filtered_keys { + set kt [lindex $opt_keytemplates 0] + if {$kt eq ""} { + set kt "%k%" + } + set keydisplay $opt_ansibase_keys[string map [list %k% $key] $kt]$RST #append result [format "%-*s = %s" $maxl $key [dict get $dval $key]] \n #differing height blocks (ie ragged) so we need a full textblock::join rather than join_basic - append result [textblock::join -- [textblock::pad $opt_ansibase_key$key$RST -width $maxl] $opt_sep "$opt_ansibase_value[dict get $dval $key]$RST"] \n + append result [textblock::join -- [textblock::pad $keydisplay -width $maxl] $opt_sep "$opt_ansibase_values[dict get $dval $key]$RST"] \n } } } @@ -765,19 +1556,23 @@ namespace eval punk::lib { #[para]This means the proc may be called with something like $x+2 end-$y etc #[para]Sometimes the actual integer index is desired. #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. - #[para]lindex_resolve will parse the index expression and return -1 if the supplied index expression is out of bounds for the supplied list. + #[para]lindex_resolve will parse the index expression and return: + #[para] a) -2 if the supplied index expression is below the lower bound for the supplied list. (< 0) + #[para] b) -1 if the supplied index expression is above the upper bound for the supplied list. (> end) #[para]Otherwise it will return an integer corresponding to the position in the list. + #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out or bounds indices, or in the case of lrange, return results anyway. #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which we will resolve (hopefully safely) with expr - if {![llength $list]} { - return -1 - } + #if {![llength $list]} { + # #review + # return ??? + #} set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i if {$index < 0} { - return -1 + return -2 } elseif {$index >= [llength $list]} { return -1 } else { @@ -794,16 +1589,28 @@ namespace eval punk::lib { return -1 } } else { - set offset 0 + #end + set index [expr {[llength $list]-1}] + if {$index < 0} { + #special case - end with empty list - treat end like a positive number out of bounds + return -1 + } else { + return $index + } } - #by now, if op = + then offset = 0 so we only need to handle the minus case if {$offset == 0} { set index [expr {[llength $list]-1}] + if {$index < 0} { + return -1 ;#special case + } else { + return $index + } } else { + #by now, if op = + then offset = 0 so we only need to handle the minus case set index [expr {([llength $list]-1) - $offset}] } if {$index < 0} { - return -1 + return -2 } else { return $index } @@ -823,16 +1630,25 @@ namespace eval punk::lib { } else { error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" } - if {$index < 0 || $index >= [llength $list]} {return -1} + if {$index < 0} { + return -2 + } elseif {$index >= [llength $list]} { + return -1 + } return $index } } } proc lindex_resolve2 {list index} { - set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ but that's likely unavailable here. - for {set i 0} {$i < [llength $list]} {incr i} { - lappend indices $i - } + #set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ but that's likely unavailable here. + #for {set i 0} {$i < [llength $list]} {incr i} { + # lappend indices $i + #} + if {[llength $list]} { + set indices [punk::lib::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. + } else { + set indices [list] + } set idx [lindex $indices $index] if {$idx eq ""} { return -1 diff --git a/src/bootsupport/modules/punk/mix/base-0.1.tm b/src/bootsupport/modules/punk/mix/base-0.1.tm index 6eec4d8d..8a4456d1 100644 --- a/src/bootsupport/modules/punk/mix/base-0.1.tm +++ b/src/bootsupport/modules/punk/mix/base-0.1.tm @@ -351,8 +351,14 @@ namespace eval punk::mix::base { continue } set testfolder [file join $candidate src $sub] - set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm] - if {[llength $tmfiles]} { + #ensure that if src/modules exists - it is always included even if empty + if {[string tolower $sub] eq "modules"} { + lappend tm_folders $testfolder + continue + } + #set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm] + #set podfolders [glob -nocomplain -dir $testfolder -type d -tail #modpod-*] + if {[llength [glob -nocomplain -dir $testfolder -type f -tail *.tm]] || [llength [glob -nocomplain -dir $testfolder -type d -tail #modpod-*]]} { lappend tm_folders $testfolder } } @@ -428,9 +434,10 @@ namespace eval punk::mix::base { } #crc::cksum is extremely slow in tcllib as at 2023 e.g 20x slower (no c implementation?) + # - try builtin zlib crc instead? #sha1 is performant - and this is not being used in a cryptographic or adversarial context - so performance and practical unlikelihood of accidental collisions should be the main consideration. #adler32 is fastest for some larger files of a few MB but slower on small files (possibly due to Tcl-based file load?) - #sha1 as at 2023 seems a good default + #sha1 as at 2023 seems a reasonable default proc cksum_algorithms {} { variable sha3_implementation #sha2 is an alias for sha256 @@ -459,10 +466,16 @@ namespace eval punk::mix::base { #adler32 via file-slurp proc cksum_adler32_file {filename} { package require zlib; #should be builtin anyway - set data [punk::mix::util::fcat -translation binary $filename] + set data [punk::mix::util::fcat -translation binary -encoding iso8859-1 $filename] #set data [fileutil::cat -translation binary $filename] ;#no significant performance diff on windows - and doesn't handle win-illegal names zlib adler32 $data } + #zlib crc vie file-slurp + proc cksum_crc_file {filename} { + package require zlib + set data [punk::mix::util::fcat -translation binary -encoding iso8859-1 $filename] + zlib crc $data + } #required to be able to accept relative paths @@ -614,6 +627,9 @@ namespace eval punk::mix::base { package require cksum ;#tcllib set cksum_command [list crc::cksum -format 0x%X -file] } + crc { + set cksum_command [list cksum_crc_file] + } adler32 { set cksum_command [list cksum_adler32_file] } diff --git a/src/modules/punk/mix/cli-0.3.tm b/src/bootsupport/modules/punk/mix/cli-0.3.1.tm similarity index 62% rename from src/modules/punk/mix/cli-0.3.tm rename to src/bootsupport/modules/punk/mix/cli-0.3.1.tm index db21a253..5843789f 100644 --- a/src/modules/punk/mix/cli-0.3.tm +++ b/src/bootsupport/modules/punk/mix/cli-0.3.1.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::mix::cli 0.3 +# Application punk::mix::cli 0.3.1 # Meta platform tcl # Meta license # @@ Meta End @@ -18,6 +18,7 @@ ## Requirements ##e.g package require frobz package require punk::repo +package require punk::ansi package require punkcheck ;#checksum and/or timestamp records @@ -202,7 +203,8 @@ namespace eval punk::mix::cli { proc module_types {} { #first in list is default for unspecified -type when creating new module - return [list plain tarjar zipkit] + #return [list plain tarjar zipkit] + return [list plain tarjar zip] } proc validate_modulename {modulename args} { @@ -401,7 +403,7 @@ namespace eval punk::mix::cli { proc build_modules_from_source_to_base {srcdir basedir args} { - set antidir [list "#*" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders we don't want to search in. + set antidir [list "#*" "_build" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders (at any level) we don't want to search in or copy. set defaults [list\ -installer punk::mix::cli::build_modules_from_source_to_base\ -call-depth-internal 0\ @@ -409,6 +411,7 @@ namespace eval punk::mix::cli { -subdirlist {}\ -punkcheck_eventobj "\uFFFF"\ -glob *.tm\ + -podglob #modpod-*\ ] set opts [dict merge $defaults $args] @@ -420,6 +423,7 @@ namespace eval punk::mix::cli { set subdirlist [dict get $opts -subdirlist] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set fileglob [dict get $opts -glob] + set podglob [dict get $opts -podglob] if {![string match "*.tm" $fileglob]} { error "build_modules_from_source_to_base -glob '$fileglob' doesn't seem to target tcl modules." } @@ -475,99 +479,344 @@ namespace eval punk::mix::cli { #---------------------------------------- - + set process_modules [dict create] + #put pods first in processing order + set src_pods [glob -nocomplain -dir $current_source_dir -type d -tail $podglob] + foreach podpath $src_pods { + dict set process_modules $podpath [dict create -type pod] + } set src_modules [glob -nocomplain -dir $current_source_dir -type f -tail $fileglob] + foreach modulepath $src_modules { + dict set process_modules $modulepath [dict create -type file] + } set did_skip 0 ;#flag for stdout/stderr formatting only - foreach m $src_modules { + dict for {modpath modinfo} $process_modules { + set modtype [dict get $modinfo -type] + set is_interesting 0 if {[string match "foobar" $current_source_dir]} { set is_interesting 1 } if {$is_interesting} { - puts "build_modules_from_source_to_base >>> module $current_source_dir/$m" + puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" } - set fileparts [split [file rootname $m] -] - set tmfile_versionsegment [lindex $fileparts end] - if {$tmfile_versionsegment eq $magicversion} { - #rebuild the .tm from the #tarjar - set basename [join [lrange $fileparts 0 end-1] -] - set versionfile $current_source_dir/$basename-buildversion.txt - set versionfiledata "" - if {![file exists $versionfile]} { - puts stderr "\nWARNING: Missing buildversion text file: $versionfile" - puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" - set module_build_version "0.1" - } else { - set fd [open $versionfile r] - set versionfiledata [read $fd]; close $fd - set ln0 [lindex [split $versionfiledata \n] 0] - set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] - if {![util::is_valid_tm_version $ln0]} { - puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" - exit 3 + set fileparts [split [file rootname $modpath] -] + #set tmfile_versionsegment [lindex $fileparts end] + lassign [split_modulename_version $modpath] basename tmfile_versionsegment + if {$tmfile_versionsegment eq ""} { + #split_modulename_version version part will be empty if not valid tcl version + #last segment doesn't look even slightly versiony - fail. + puts stderr "ERROR: Unable to confirm file $current_source_dir/$modpath is a reasonably versioned .tm module - ABORTING." + exit 1 + } + switch -- $modtype { + pod { + #basename still contains leading #modpod- + if {[string match #modpod-* $basename]} { + set basename [string range $basename 8 end] + } else { + error "build_modules_from_source_to_base, pod, unexpected basename $basename" ;#shouldn't be possible with default podglob - review - why is podglob configurable? + } + set versionfile $current_source_dir/$basename-buildversion.txt ;#needs to be added in targetset_addsource to trigger rebuild if changed (only when magicversion in use) + if {$tmfile_versionsegment eq $magicversion} { + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + } else { + set module_build_version $tmfile_versionsegment } - set module_build_version $ln0 - } - - if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { - #TODO + set buildfolder $current_source_dir/_build file mkdir $buildfolder + # -- --- + set config [dict create\ + -glob *\ + -max_depth 100\ + ] + # -max_depth -1 for no limit + set build_installername pods_in_$current_source_dir + set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + $build_installer set_source_target $current_source_dir/$modpath $buildfolder + set build_event [$build_installer start_event $config] + # -- --- + set podtree_copy $buildfolder/#modpod-$basename-$module_build_version + set modulefile $buildfolder/$basename-$module_build_version.tm + + + $build_event targetset_init INSTALL $podtree_copy + $build_event targetset_addsource $current_source_dir/$modpath + if {$tmfile_versionsegment eq $magicversion} { + $build_event targetset_addsource $versionfile + } + if {\ + [llength [dict get [$build_event targetset_source_changes] changed]]\ + || [llength [$build_event get_targets_exist]] < [llength [$build_event get_targets]]\ + } { + $build_event targetset_started + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + + set delete_failed 0 + if {[file exists $buildfolder/]} { + puts stderr "deleting existing _build copy at $podtree_copy" + if {[catch { + file delete -force $podtree_copy + } errMsg]} { + puts stderr "[punk::ansi::a+ red]deletion of _build copy at $podtree_copy failed: $errMsg[punk::ansi::a]" + set delete_failed 1 + } + } + if {!$delete_failed} { + puts stdout "copying.." + puts stdout "$current_source_dir/$modpath" + puts stdout "to:" + puts stdout "$podtree_copy" + file copy $current_source_dir/$modpath $podtree_copy + if {$tmfile_versionsegment eq $magicversion} { + set tmfile $buildfolder/#modpod-$basename-$module_build_version/$basename-$magicversion.tm + if {[file exists $tmfile]} { + set newname $buildfolder/#modpod-$basename-$module_build_version/$basename-$module_build_version.tm + file rename $tmfile $newname + set tmfile $newname + } + set fd [open $tmfile r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $tmfile w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + } + #delete and regenerate zip and modpod stubbed zip + set had_error 0 + set notes [list] + if {[catch { + file delete $buildfolder/$basename-$module_build_version.zip + } err] } { + set had_error 1 + lappend notes "zip_delete_failed" + } + if {[catch { + file delete $buildfolder/$basename-$module_build_version.tm + } err]} { + set had_error 1 + lappend notes "tm_delete_failed" + } + #create ordinary zip file without using external executable + package require punk::zip + set zipfile $buildfolder/$basename-$module_build_version.zip ;#ordinary zip file (deflate) + + if 0 { + #use -base $buildfolder so that -directory is included in the archive - the modpod stub relies on this - and extraction would be potentially messy otherwise + punk::zip::mkzip -base $buildfolder -directory $buildfolder/#modpod-$basename-$module_build_version -- $zipfile * + #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files + } + #zipfs mkzip does exactly what we need anyway in this case + set wd [pwd] + cd $buildfolder + puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" + zipfs mkzip $zipfile #modpod-$basename-$module_build_version + cd $wd + + package require modpod + modpod::lib::make_zip_modpod $zipfile $modulefile + + + if {$had_error} { + $build_event targetset_end FAILED -note [join $notes ,] + } else { + # -- ---------- + $build_event targetset_end OK + # -- ---------- + } + } else { + $build_event targetset_end FAILED -note "could not delete $podtree_copy" + } - if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { - } else { - - } - #REVIEW - should be in same structure/depth as $target_module_dir in _build? - set tmfile $basedir/_build/$basename-$module_build_version.tm - file mkdir $basedir/_build - file delete -force $basedir/_build/#tarjar-$basename-$module_build_version - file delete -force $tmfile - - - file copy -force $current_source_dir/#tarjar-$basename-$magicversion $basedir/_build/#tarjar-$basename-$module_build_version - # - #bsdtar doesn't seem to work.. or I haven't worked out the right options? - #exec tar -cvf $basedir/_build/$basename-$module_build_version.tm $basedir/_build/#tarjar-$basename-$module_build_version - package require tar - tar::create $tmfile $basedir/_build/#tarjar-$basename-$module_build_version - if {![file exists $tmfile]} { - puts stdout "ERROR: Failed to build tarjar file $tmfile" - exit 4 - } - #copy the file? - #set target $target_module_dir/$basename-$module_build_version.tm - #file copy -force $tmfile $target - - lappend module_list $tmfile - } else { - #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. - if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { - puts stderr "\nWarning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $build_event targetset_end SKIPPED } + $build_event destroy + $build_installer destroy - #------------------------------ - # - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm - $event targetset_addsource $versionfile - $event targetset_addsource $current_source_dir/$m + $event targetset_addsource $modulefile + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + $event targetset_started + # -- --- --- --- --- --- + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + lappend module_list $modulefile + file copy -force $modulefile $target_module_dir + puts stderr "Copied zip modpod module $modulefile to $target_module_dir" + # -- --- --- --- --- --- + $event targetset_end OK -note "zip modpod" + } else { + puts -nonewline stderr "." + set did_skip 1 + if {$is_interesting} { + puts stderr "$modulefile [$event targetset_source_changes]" + } + $event targetset_end SKIPPED + } + } + tarjar { + #basename may still contain #tarjar- + #to be obsoleted - update modpod to (optionally) use vfs::tar + } + file { + set m $modpath + if {$tmfile_versionsegment eq $magicversion} { + #set basename [join [lrange $fileparts 0 end-1] -] + set versionfile $current_source_dir/$basename-buildversion.txt + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { + #rebuild the .tm from the #tarjar + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { + + } else { + + } + #REVIEW - should be in same structure/depth as $target_module_dir in _build? + + #TODO + set buildfolder $current_sourcedir/_build + file mkdir $buildfolder + + set tmfile $buildfolder/$basename-$module_build_version.tm + file delete -force $buildfolder/#tarjar-$basename-$module_build_version + file delete -force $tmfile + + + file copy -force $current_source_dir/#tarjar-$basename-$magicversion $buildfolder/#tarjar-$basename-$module_build_version + # + #bsdtar doesn't seem to work.. or I haven't worked out the right options? + #exec tar -cvf $buildfolder/$basename-$module_build_version.tm $buildfolder/#tarjar-$basename-$module_build_version + package require tar + tar::create $tmfile $buildfolder/#tarjar-$basename-$module_build_version + if {![file exists $tmfile]} { + puts stdout "ERROR: failed to build tarjar file $tmfile" + exit 4 + } + #copy the file? + #set target $target_module_dir/$basename-$module_build_version.tm + #file copy -force $tmfile $target + + lappend module_list $tmfile + } else { + #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. + if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { + puts stderr "\nWarning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" + } + + #------------------------------ + # + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] + $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm + $event targetset_addsource $versionfile + $event targetset_addsource $current_source_dir/$m + + #set changed_list [list] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] + #set changed_list [dict get $changed_unchanged changed] + + + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + #set file_record [punkcheck::installfile_started_install $basedir $file_record] + $event targetset_started + # -- --- --- --- --- --- + set target $target_module_dir/$basename-$module_build_version.tm + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" + set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $target w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + #file copy -force $srcdir/$m $target + lappend module_list $target + # -- --- --- --- --- --- + #set file_record [punkcheck::installfile_finished_install $basedir $file_record] + $event targetset_end OK + } else { + if {$is_interesting} { + puts stdout "skipping module $current_source_dir/$m - no change in sources detected" + } + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $event targetset_end SKIPPED + } + + #------------------------------ + } + + continue + } + ##------------------------------ + ## + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] #set changed_list [list] ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] ## -- --- --- --- --- --- #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] #set changed_list [dict get $changed_unchanged changed] - - + #---------- + $event targetset_init INSTALL $target_module_dir/$m + $event targetset_addsource $current_source_dir/$m if {\ [llength [dict get [$event targetset_source_changes] changed]]\ || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ @@ -576,85 +825,27 @@ namespace eval punk::mix::cli { #set file_record [punkcheck::installfile_started_install $basedir $file_record] $event targetset_started # -- --- --- --- --- --- - set target $target_module_dir/$basename-$module_build_version.tm if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} - puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" - set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd - set data [string map [list $magicversion $module_build_version] $data] - set fdout [open $target w] - fconfigure $fdout -translation binary - puts -nonewline $fdout $data - close $fdout - #file copy -force $srcdir/$m $target - lappend module_list $target + lappend module_list $current_source_dir/$m + file copy -force $current_source_dir/$m $target_module_dir + puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" # -- --- --- --- --- --- #set file_record [punkcheck::installfile_finished_install $basedir $file_record] - $event targetset_end OK + $event targetset_end OK -note "already versioned module" } else { - if {$is_interesting} { - puts stdout "skipping module $current_source_dir/$m - no change in sources detected" - } puts -nonewline stderr "." set did_skip 1 + if {$is_interesting} { + puts stderr "$current_source_dir/$m [$event targetset_source_changes]" + } #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $event targetset_end SKIPPED } - - #------------------------------ - } - - continue - } - - - if {![util::is_valid_tm_version $tmfile_versionsegment]} { - #last segment doesn't look even slightly versiony - fail. - puts stderr "ERROR: Unable to confirm file $current_source_dir/$m is a reasonably versioned .tm module - ABORTING." - exit 1 } + } ;#end dict for {modpath modinfo} $process_modules - ##------------------------------ - ## - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] - #set changed_list [list] - ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- - #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] - #set changed_list [dict get $changed_unchanged changed] - - #---------- - $event targetset_init INSTALL $target_module_dir/$m - $event targetset_addsource $current_source_dir/$m - if {\ - [llength [dict get [$event targetset_source_changes] changed]]\ - || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ - } { - - #set file_record [punkcheck::installfile_started_install $basedir $file_record] - $event targetset_started - # -- --- --- --- --- --- - if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} - lappend module_list $current_source_dir/$m - file copy -force $current_source_dir/$m $target_module_dir - puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" - # -- --- --- --- --- --- - #set file_record [punkcheck::installfile_finished_install $basedir $file_record] - $event targetset_end OK -note "already versioned module" - } else { - puts -nonewline stderr "." - set did_skip 1 - if {$is_interesting} { - puts stderr "$current_source_dir/$m [$event targetset_source_changes]" - } - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] - $event targetset_end SKIPPED - } - } if {$CALLDEPTH >= $max_depth} { set subdirs [list] } else { @@ -680,6 +871,7 @@ namespace eval punk::mix::cli { -subdirlist [list {*}$subdirlist $d]\ -punkcheck_eventobj $event\ -glob $fileglob\ + -podglob $podglob\ ] } if {$did_skip} { @@ -931,6 +1123,6 @@ namespace eval punk::mix::cli { ## Ready package provide punk::mix::cli [namespace eval punk::mix::cli { variable version - set version 0.3 + set version 0.3.1 }] return diff --git a/src/bootsupport/modules/punk/mix/cli-0.3.tm b/src/bootsupport/modules/punk/mix/cli-0.3.tm index db21a253..263ccc96 100644 --- a/src/bootsupport/modules/punk/mix/cli-0.3.tm +++ b/src/bootsupport/modules/punk/mix/cli-0.3.tm @@ -18,6 +18,7 @@ ## Requirements ##e.g package require frobz package require punk::repo +package require punk::ansi package require punkcheck ;#checksum and/or timestamp records @@ -202,7 +203,8 @@ namespace eval punk::mix::cli { proc module_types {} { #first in list is default for unspecified -type when creating new module - return [list plain tarjar zipkit] + #return [list plain tarjar zipkit] + return [list plain tarjar zip] } proc validate_modulename {modulename args} { @@ -401,7 +403,7 @@ namespace eval punk::mix::cli { proc build_modules_from_source_to_base {srcdir basedir args} { - set antidir [list "#*" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders we don't want to search in. + set antidir [list "#*" "_build" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders (at any level) we don't want to search in or copy. set defaults [list\ -installer punk::mix::cli::build_modules_from_source_to_base\ -call-depth-internal 0\ @@ -409,6 +411,7 @@ namespace eval punk::mix::cli { -subdirlist {}\ -punkcheck_eventobj "\uFFFF"\ -glob *.tm\ + -podglob #modpod-*\ ] set opts [dict merge $defaults $args] @@ -420,6 +423,7 @@ namespace eval punk::mix::cli { set subdirlist [dict get $opts -subdirlist] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set fileglob [dict get $opts -glob] + set podglob [dict get $opts -podglob] if {![string match "*.tm" $fileglob]} { error "build_modules_from_source_to_base -glob '$fileglob' doesn't seem to target tcl modules." } @@ -475,99 +479,344 @@ namespace eval punk::mix::cli { #---------------------------------------- - + set process_modules [dict create] + #put pods first in processing order + set src_pods [glob -nocomplain -dir $current_source_dir -type d -tail $podglob] + foreach podpath $src_pods { + dict set process_modules $podpath [dict create -type pod] + } set src_modules [glob -nocomplain -dir $current_source_dir -type f -tail $fileglob] + foreach modulepath $src_modules { + dict set process_modules $modulepath [dict create -type file] + } set did_skip 0 ;#flag for stdout/stderr formatting only - foreach m $src_modules { + dict for {modpath modinfo} $process_modules { + set modtype [dict get $modinfo -type] + set is_interesting 0 if {[string match "foobar" $current_source_dir]} { set is_interesting 1 } if {$is_interesting} { - puts "build_modules_from_source_to_base >>> module $current_source_dir/$m" + puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" } - set fileparts [split [file rootname $m] -] - set tmfile_versionsegment [lindex $fileparts end] - if {$tmfile_versionsegment eq $magicversion} { - #rebuild the .tm from the #tarjar - set basename [join [lrange $fileparts 0 end-1] -] - set versionfile $current_source_dir/$basename-buildversion.txt - set versionfiledata "" - if {![file exists $versionfile]} { - puts stderr "\nWARNING: Missing buildversion text file: $versionfile" - puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" - set module_build_version "0.1" - } else { - set fd [open $versionfile r] - set versionfiledata [read $fd]; close $fd - set ln0 [lindex [split $versionfiledata \n] 0] - set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] - if {![util::is_valid_tm_version $ln0]} { - puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" - exit 3 + set fileparts [split [file rootname $modpath] -] + #set tmfile_versionsegment [lindex $fileparts end] + lassign [split_modulename_version $modpath] basename tmfile_versionsegment + if {$tmfile_versionsegment eq ""} { + #split_modulename_version version part will be empty if not valid tcl version + #last segment doesn't look even slightly versiony - fail. + puts stderr "ERROR: Unable to confirm file $current_source_dir/$modpath is a reasonably versioned .tm module - ABORTING." + exit 1 + } + switch -- $modtype { + pod { + #basename still contains leading #modpod- + if {[string match #modpod-* $basename]} { + set basename [string range $basename 8 end] + } else { + error "build_modules_from_source_to_base, pod, unexpected basename $basename" ;#shouldn't be possible with default podglob - review - why is podglob configurable? + } + set versionfile $current_source_dir/$basename-buildversion.txt ;#needs to be added in targetset_addsource to trigger rebuild if changed (only when magicversion in use) + if {$tmfile_versionsegment eq $magicversion} { + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + } else { + set module_build_version $tmfile_versionsegment } - set module_build_version $ln0 - } - - if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { - #TODO + set buildfolder $current_source_dir/_build file mkdir $buildfolder + # -- --- + set config [dict create\ + -glob *\ + -max_depth 100\ + ] + # -max_depth -1 for no limit + set build_installername pods_in_$current_source_dir + set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + $build_installer set_source_target $current_source_dir/$modpath $buildfolder + set build_event [$build_installer start_event $config] + # -- --- + set podtree_copy $buildfolder/#modpod-$basename-$module_build_version + set modulefile $buildfolder/$basename-$module_build_version.tm + + + $build_event targetset_init INSTALL $podtree_copy + $build_event targetset_addsource $current_source_dir/$modpath + if {$tmfile_versionsegment eq $magicversion} { + $build_event targetset_addsource $versionfile + } + if {\ + [llength [dict get [$build_event targetset_source_changes] changed]]\ + || [llength [$build_event get_targets_exist]] < [llength [$build_event get_targets]]\ + } { + $build_event targetset_started + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + + set delete_failed 0 + if {[file exists $buildfolder/]} { + puts stderr "deleting existing _build copy at $podtree_copy" + if {[catch { + file delete -force $podtree_copy + } errMsg]} { + puts stderr "[punk::ansi::a+ red]deletion of _build copy at $podtree_copy failed: $errMsg[punk::ansi::a]" + set delete_failed 1 + } + } + if {!$delete_failed} { + puts stdout "copying.." + puts stdout "$current_source_dir/$modpath" + puts stdout "to:" + puts stdout "$podtree_copy" + file copy $current_source_dir/$modpath $podtree_copy + if {$tmfile_versionsegment eq $magicversion} { + set tmfile $buildfolder/#modpod-$basename-$module_build_version/$basename-$magicversion.tm + if {[file exists $tmfile]} { + set newname $buildfolder/#modpod-$basename-$module_build_version/$basename-$module_build_version.tm + file rename $tmfile $newname + set tmfile $newname + } + set fd [open $tmfile r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $tmfile w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + } + #delete and regenerate zip and modpod stubbed zip + set had_error 0 + set notes [list] + if {[catch { + file delete $buildfolder/$basename-$module_build_version.zip + } err] } { + set had_error 1 + lappend notes "zip_delete_failed" + } + if {[catch { + file delete $buildfolder/$basename-$module_build_version.tm + } err]} { + set had_error 1 + lappend notes "tm_delete_failed" + } + #create ordinary zip file without using external executable + package require punk::zip + set zipfile $buildfolder/$basename-$module_build_version.zip ;#ordinary zip file (deflate) + + if 0 { + #use -base $buildfolder so that -directory is included in the archive - the modpod stub relies on this - and extraction would be potentially messy otherwise + punk::zip::mkzip -base $buildfolder -directory $buildfolder/#modpod-$basename-$module_build_version -- $zipfile * + #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files + } + #zipfs mkzip does exactly what we need anyway in this case + set wd [pwd] + cd $buildfolder + puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" + zipfs mkzip $zipfile #modpod-$basename-$module_build_version + cd $wd + + package require modpod + modpod::lib::make_zip_modpod $zipfile $modulefile + + + if {$had_error} { + $build_event targetset_end FAILED -note [join $notes ,] + } else { + # -- ---------- + $build_event targetset_end OK + # -- ---------- + } + } else { + $build_event targetset_end FAILED -note "could not delete $podtree_copy" + } - if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { - } else { - - } - #REVIEW - should be in same structure/depth as $target_module_dir in _build? - set tmfile $basedir/_build/$basename-$module_build_version.tm - file mkdir $basedir/_build - file delete -force $basedir/_build/#tarjar-$basename-$module_build_version - file delete -force $tmfile - - - file copy -force $current_source_dir/#tarjar-$basename-$magicversion $basedir/_build/#tarjar-$basename-$module_build_version - # - #bsdtar doesn't seem to work.. or I haven't worked out the right options? - #exec tar -cvf $basedir/_build/$basename-$module_build_version.tm $basedir/_build/#tarjar-$basename-$module_build_version - package require tar - tar::create $tmfile $basedir/_build/#tarjar-$basename-$module_build_version - if {![file exists $tmfile]} { - puts stdout "ERROR: Failed to build tarjar file $tmfile" - exit 4 - } - #copy the file? - #set target $target_module_dir/$basename-$module_build_version.tm - #file copy -force $tmfile $target - - lappend module_list $tmfile - } else { - #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. - if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { - puts stderr "\nWarning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $build_event targetset_end SKIPPED } + $build_event destroy + $build_installer destroy - #------------------------------ - # - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm - $event targetset_addsource $versionfile - $event targetset_addsource $current_source_dir/$m + $event targetset_addsource $modulefile + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + $event targetset_started + # -- --- --- --- --- --- + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + lappend module_list $modulefile + file copy -force $modulefile $target_module_dir + puts stderr "Copied zip modpod module $modulefile to $target_module_dir" + # -- --- --- --- --- --- + $event targetset_end OK -note "zip modpod" + } else { + puts -nonewline stderr "." + set did_skip 1 + if {$is_interesting} { + puts stderr "$modulefile [$event targetset_source_changes]" + } + $event targetset_end SKIPPED + } + } + tarjar { + #basename may still contain #tarjar- + #to be obsoleted - update modpod to (optionally) use vfs::tar + } + file { + set m $modpath + if {$tmfile_versionsegment eq $magicversion} { + #set basename [join [lrange $fileparts 0 end-1] -] + set versionfile $current_source_dir/$basename-buildversion.txt + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { + #rebuild the .tm from the #tarjar + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { + + } else { + + } + #REVIEW - should be in same structure/depth as $target_module_dir in _build? + + #TODO + set buildfolder $current_sourcedir/_build + file mkdir $buildfolder + + set tmfile $buildfolder/$basename-$module_build_version.tm + file delete -force $buildfolder/#tarjar-$basename-$module_build_version + file delete -force $tmfile + + + file copy -force $current_source_dir/#tarjar-$basename-$magicversion $buildfolder/#tarjar-$basename-$module_build_version + # + #bsdtar doesn't seem to work.. or I haven't worked out the right options? + #exec tar -cvf $buildfolder/$basename-$module_build_version.tm $buildfolder/#tarjar-$basename-$module_build_version + package require tar + tar::create $tmfile $buildfolder/#tarjar-$basename-$module_build_version + if {![file exists $tmfile]} { + puts stdout "ERROR: failed to build tarjar file $tmfile" + exit 4 + } + #copy the file? + #set target $target_module_dir/$basename-$module_build_version.tm + #file copy -force $tmfile $target + + lappend module_list $tmfile + } else { + #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. + if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { + puts stderr "\nWarning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" + } + + #------------------------------ + # + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] + $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm + $event targetset_addsource $versionfile + $event targetset_addsource $current_source_dir/$m + + #set changed_list [list] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] + #set changed_list [dict get $changed_unchanged changed] + + + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + #set file_record [punkcheck::installfile_started_install $basedir $file_record] + $event targetset_started + # -- --- --- --- --- --- + set target $target_module_dir/$basename-$module_build_version.tm + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" + set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $target w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + #file copy -force $srcdir/$m $target + lappend module_list $target + # -- --- --- --- --- --- + #set file_record [punkcheck::installfile_finished_install $basedir $file_record] + $event targetset_end OK + } else { + if {$is_interesting} { + puts stdout "skipping module $current_source_dir/$m - no change in sources detected" + } + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $event targetset_end SKIPPED + } + + #------------------------------ + } + + continue + } + ##------------------------------ + ## + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] #set changed_list [list] ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] ## -- --- --- --- --- --- #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] #set changed_list [dict get $changed_unchanged changed] - - + #---------- + $event targetset_init INSTALL $target_module_dir/$m + $event targetset_addsource $current_source_dir/$m if {\ [llength [dict get [$event targetset_source_changes] changed]]\ || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ @@ -576,85 +825,27 @@ namespace eval punk::mix::cli { #set file_record [punkcheck::installfile_started_install $basedir $file_record] $event targetset_started # -- --- --- --- --- --- - set target $target_module_dir/$basename-$module_build_version.tm if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} - puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" - set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd - set data [string map [list $magicversion $module_build_version] $data] - set fdout [open $target w] - fconfigure $fdout -translation binary - puts -nonewline $fdout $data - close $fdout - #file copy -force $srcdir/$m $target - lappend module_list $target + lappend module_list $current_source_dir/$m + file copy -force $current_source_dir/$m $target_module_dir + puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" # -- --- --- --- --- --- #set file_record [punkcheck::installfile_finished_install $basedir $file_record] - $event targetset_end OK + $event targetset_end OK -note "already versioned module" } else { - if {$is_interesting} { - puts stdout "skipping module $current_source_dir/$m - no change in sources detected" - } puts -nonewline stderr "." set did_skip 1 + if {$is_interesting} { + puts stderr "$current_source_dir/$m [$event targetset_source_changes]" + } #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] $event targetset_end SKIPPED } - - #------------------------------ - } - - continue - } - - - if {![util::is_valid_tm_version $tmfile_versionsegment]} { - #last segment doesn't look even slightly versiony - fail. - puts stderr "ERROR: Unable to confirm file $current_source_dir/$m is a reasonably versioned .tm module - ABORTING." - exit 1 } + } ;#end dict for {modpath modinfo} $process_modules - ##------------------------------ - ## - #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] - #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] - #set changed_list [list] - ## -- --- --- --- --- --- - #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] - #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] - ## -- --- --- --- --- --- - #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] - #set changed_list [dict get $changed_unchanged changed] - - #---------- - $event targetset_init INSTALL $target_module_dir/$m - $event targetset_addsource $current_source_dir/$m - if {\ - [llength [dict get [$event targetset_source_changes] changed]]\ - || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ - } { - - #set file_record [punkcheck::installfile_started_install $basedir $file_record] - $event targetset_started - # -- --- --- --- --- --- - if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} - lappend module_list $current_source_dir/$m - file copy -force $current_source_dir/$m $target_module_dir - puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" - # -- --- --- --- --- --- - #set file_record [punkcheck::installfile_finished_install $basedir $file_record] - $event targetset_end OK -note "already versioned module" - } else { - puts -nonewline stderr "." - set did_skip 1 - if {$is_interesting} { - puts stderr "$current_source_dir/$m [$event targetset_source_changes]" - } - #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] - $event targetset_end SKIPPED - } - } if {$CALLDEPTH >= $max_depth} { set subdirs [list] } else { @@ -680,6 +871,7 @@ namespace eval punk::mix::cli { -subdirlist [list {*}$subdirlist $d]\ -punkcheck_eventobj $event\ -glob $fileglob\ + -podglob $podglob\ ] } if {$did_skip} { diff --git a/src/bootsupport/modules/punk/mix/commandset/debug-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/debug-0.1.0.tm index 8ed735c1..c6c83b69 100644 --- a/src/bootsupport/modules/punk/mix/commandset/debug-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/debug-0.1.0.tm @@ -31,7 +31,7 @@ namespace eval punk::mix::commandset::debug { set out "" puts stdout "find_repos output:" set pathinfo [punk::repo::find_repos [pwd]] - pdict $pathinfo + pdict pathinfo set projectdir [dict get $pathinfo closest] set modulefolders [lib::find_source_module_paths $projectdir] @@ -39,7 +39,7 @@ namespace eval punk::mix::commandset::debug { set template_base_dict [punk::mix::base::lib::get_template_basefolders] puts stdout "get_template_basefolders output:" - pdict $template_base_dict + pdict template_base_dict */* return } 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 56aa8158..9955c53b 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 @@ -159,7 +159,7 @@ namespace eval punk::mix::commandset::module { #set opts [dict merge $defaults $args] #todo - review compatibility between -template and -type - #-type is the wrapping technology e.g 'plain' for none or tarjar/zipkit etc (consider also snappy/snappy-tcl) + #-type is the wrapping technology e.g 'plain' for none or tarjar or zip (modpod) etc (consider also snappy/snappy-tcl) #-template may be a folder - but only if the selected -type suports it @@ -293,6 +293,7 @@ namespace eval punk::mix::commandset::module { } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_quiet [dict get $opts -quiet] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -378,13 +379,39 @@ namespace eval punk::mix::commandset::module { } set template_filedata [string map $strmap $template_filedata] - set modulefile $modulefolder/${moduletail}-$infile_version.tm - if {[file exists $modulefile]} { - set errmsg "module.new error: module file $modulefile already exists - aborting" - if {[string match "*$magicversion*" $modulefile]} { - append errmsg \n "If you are attempting to create a module file with a specific version in the source-file name - you will need to use a template that doesn't contain the string '$magicversion' e.g the provided template moduleexactversion-0.0.1.tm" + set tmfile $modulefolder/${moduletail}-$infile_version.tm + set podfile $modulefolder/#modpod-$moduletail-$infile_version/$moduletail-$infile_version.tm + set has_tm [file exists $tmfile] + set has_pod [file exists $podfile] + if {$has_tm && $has_pos} { + #invalid configuration - bomb out + error "module.new error: Invalid target configuration found. module folder has both a .tm file $tmfile and a modpod file $podfile. Please delete one of them before trying again." + } + if {$opt_type eq "plain"} { + set modulefile $tmfile + } else { + set modulefile $podfile + } + if {$has_tm || $has_pod} { + if {!$opt_force} { + if {$has_tm} { + set errmsg "module.new error: module file $tmfile already exists - aborting" + } else { + set errmsg "module.new error: module file $podfile already exists - aborting" + } + if {[string match "*$magicversion*" $tmfile]} { + append errmsg \n "If you are attempting to create a module file with a specific version in the source-file name - you will need to use a template that doesn't contain the string '$magicversion' e.g the provided template moduleexactversion-0.0.1.tm" + } + error $errmsg + } else { + #review - prompt here vs caller? + #we are committed to overwriting/replacing if there was a pre-existing module of same version + if {$has_pod} { + file delete -force [file dirname $podfile] + } elseif {$has_tm} { + file delete -force $tmfile + } } - error $errmsg } @@ -407,13 +434,20 @@ namespace eval punk::mix::commandset::module { } } - set existing_versions [glob -nocomplain -dir $modulefolder -tails ${moduletail}-*.tm] + set existing_tm_versions [glob -nocomplain -dir $modulefolder -tails ${moduletail}-*.tm] #it shouldn't be possible to overmatch with the glob - because '-' is not valid in a Tcl module name + set existing_pod_versions [glob -nocomplain -dir $modulefolder -tails #modpod-$moduletail-*] + set existing_versions [concat $existing_tm_versions $existing_pod_versions] + if {[llength $existing_versions]} { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lappend name_version_pairs [punk::mix::cli::lib::split_modulename_version $existing] ;# .tm is stripped and ignored + lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + if {[string match #modpod-* $namepart]} { + set namepart [string range $namepart 8 end] + } + lappend name_version_pairs [list $namepart $version] } set name_version_pairs [lsort -command {package vcompare} -index 1 $name_version_pairs] ;#while plain lsort will often work with versions - it can get order wrong with some - so use package vcompare if {[lindex $name_version_pairs end] ne [list $moduletail $infile_version]} { @@ -436,6 +470,8 @@ namespace eval punk::mix::commandset::module { if {!$opt_quiet} { puts stdout "Creating $modulefile from template $moduletemplate" } + file mkdir [file dirname $modulefile] + set fd [open $modulefile w] fconfigure $fd -translation binary puts -nonewline $fd $template_filedata diff --git a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index 862bbf00..9cac531c 100644 --- a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -320,6 +320,8 @@ namespace eval punk::mix::commandset::project { puts stderr "-force 1 or -update 1 not specified - aborting" return } + #review + set fossil_repo_file $repodb_folder/$projectname.fossil } if {$fossil_repo_file eq ""} { @@ -415,12 +417,30 @@ namespace eval punk::mix::commandset::project { if {[file exists $projectdir/src/modules]} { foreach m $opt_modules { - if {![file exists $projectdir/src/modules/$m-[punk::mix::util::magic_tm_version].tm]} { + #check if mod-ver.tm file or #modpod-mod-ver folder exist + set tmfile $projectdir/src/modules/$m-[punk::mix::util::magic_tm_version].tm + set podfile $projectdir/src/modules/#modpod-$m-[punk::mix::util::magic_tm_version]/$m-[punk::mix::util::magic_tm_version].tm + + set has_tm [file exists $tmfile] + set has_pod [file exists $podfile] + #puts stderr "=====> has_tm: $has_tm has_pod: $has_pod" + if {!$has_tm && !$has_pod} { #todo - option for -module_template - and check existence at top? or change opt_modules to be a list of dicts with configuration info -template -type etc - punk::mix::commandset::module::new $m -project $projectname -type $opt_type + punk::mix::commandset::module::new -project $projectname -type $opt_type $m } else { + #we should rarely if ever want to force any src/modules to be overwritten if {$opt_force} { - punk::mix::commandset::module::new $m -project $projectname -type $opt_type -force 1 + if {$has_pod} { + set answer [util::askuser "OVERWRITE the src/modules file $podfile ?? (generally not desirable) Y|N"] + set overwrite_type zip + } else { + set answer [util::askuser "OVERWRITE the src/modules file $tmfile ?? (generally not desirable) Y|N"] + set overwrite_type $opt_type + } + if {[string tolower $answer] eq "y"} { + #REVIEW - all pods zip - for now + punk::mix::commandset::module::new -project $projectname -type $overwrite_type -force 1 $m + } } } } diff --git a/src/bootsupport/modules/punk/mix/templates/layouts/project/src/make.tcl b/src/bootsupport/modules/punk/mix/templates/layouts/project/src/make.tcl index c53315e9..20b0c29f 100644 --- a/src/bootsupport/modules/punk/mix/templates/layouts/project/src/make.tcl +++ b/src/bootsupport/modules/punk/mix/templates/layouts/project/src/make.tcl @@ -13,7 +13,7 @@ namespace eval ::punkmake { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /?] - variable known_commands [list project get-project-info shell bootsupport] + variable known_commands [list project get-project-info shell vendor bootsupport] } if {"::try" ni [info commands ::try]} { puts stderr "Tcl interpreter possibly too old - 'try' command not found - aborting" @@ -134,6 +134,8 @@ proc punkmake_gethelp {args} { append h " - the optional -k flag will terminate processes running as the executable being built (if applicable)" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n \n + append h " $scriptname vendor" \n + append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n append h " $scriptname get-project-info" \n append h " - show the name and base folder of the project to be built" \n append h "" \n @@ -251,6 +253,100 @@ if {$::punkmake::command eq "shell"} { exit 1 } +if {$::punkmake::command eq "vendor"} { + puts "projectroot: $projectroot" + puts "script: [info script]" + #puts "-- [tcl::tm::list] --" + puts stdout "Updating vendor modules" + proc vendor_localupdate {projectroot} { + set local_modules [list] + set git_modules [list] + set fossil_modules [list] + #todo vendor/lib ? + set vendor_config $projectroot/src/vendormodules/include_modules.config + if {[file exists $vendor_config]} { + set targetroot $projectroot/src/vendormodules/modules + source $vendor_config ;#populate $local_modules $git_modules $fossil_modules with project-specific list + if {![llength $local_modules]} { + puts stderr "No local vendor modules configured for updating (config file: $vendor_config)" + } else { + if {[catch { + #---------- + set vendor_installer [punkcheck::installtrack new make.tcl $projectroot/src/vendormodules/.punkcheck] + $vendor_installer set_source_target $projectroot $projectroot/src/vendormodules + set installation_event [$vendor_installer start_event {-make_step vendor}] + #---------- + } errM]} { + puts stderr "Unable to use punkcheck for vendor update. Error: $errM" + set installation_event "" + } + foreach {relpath module} $local_modules { + set module [string trim $module :] + set module_subpath [string map {:: /} [namespace qualifiers $module]] + set srclocation [file join $projectroot $relpath $module_subpath] + #puts stdout "$relpath $module $module_subpath $srclocation" + set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] + #lsort won't sort version numbers properly e.g with -dictionary 0.1.1 comes before 0.1 + if {![llength $pkgmatches]} { + puts stderr "Missing source for vendor module $module - not found in $srclocation" + continue + } + set latestfile [lindex $pkgmatches 0] + set latestver [lindex [split [file rootname $latestfile] -] 1] + foreach m $pkgmatches { + lassign [split [file rootname $m] -] _pkg ver + #puts "comparing $ver vs $latestver" + if {[package vcompare $ver $latestver] == 1} { + set latestver $ver + set latestfile $m + } + } + set srcfile [file join $srclocation $latestfile] + set tgtfile [file join $targetroot $module_subpath $latestfile] + if {$installation_event ne ""} { + #---------- + $installation_event targetset_init INSTALL $tgtfile + $installation_event targetset_addsource $srcfile + #---------- + if {\ + [llength [dict get [$installation_event targetset_source_changes] changed]]\ + || [llength [$installation_event get_targets_exist]] < [llength [$installation_event get_targets]]\ + } { + file mkdir [file dirname $tgtfile] ;#ensure containing folder for target exists + $installation_event targetset_started + # -- --- --- --- --- --- + puts "VENDOR update: $srcfile -> $tgtfile" + if {[catch { + file copy -force $srcfile $tgtfile + } errM]} { + $installation_event targetset_end FAILED + } else { + $installation_event targetset_end OK + } + # -- --- --- --- --- --- + } else { + puts -nonewline stderr "." + $installation_event targetset_end SKIPPED + } + $installation_event end + } else { + file copy -force $srcfile $tgtfile + } + } + + } + } else { + puts stderr "No config at $vendor_config - nothing configured to update" + } + } + + + puts stdout " vendor package update done " + flush stderr + flush stdout + ::exit 0 +} + if {$::punkmake::command eq "bootsupport"} { puts "projectroot: $projectroot" puts "script: [info script]" @@ -275,7 +371,7 @@ if {$::punkmake::command eq "bootsupport"} { set boot_event [$boot_installer start_event {-make_step bootsupport}] #---------- } errM]} { - puts stderr "Unable to use punkcheck for bootsupport error: $errM" + puts stderr "Unable to use punkcheck for bootsupport. Error: $errM" set boot_event "" } @@ -441,7 +537,7 @@ if {[file exists $sourcefolder/vendorlib]} { if {[file exists $sourcefolder/vendormodules]} { #install .tm *and other files* puts stdout "VENDORMODULES: copying from $sourcefolder/vendormodules to $target_modules_base (if source file changed)" - set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md}] + set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md include_modules.config}] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } else { puts stderr "VENDORMODULES: No src/vendormodules folder found." diff --git a/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl b/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl new file mode 100644 index 00000000..99320f87 --- /dev/null +++ b/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl @@ -0,0 +1,53 @@ +apply {code { + set scriptpath [file normalize [info script]] + if {[string match "#modpod-loadscript*.tcl" [file tail $scriptpath]]} { + #jump up an extra dir level if we are within a #modpod-loadscript file. + set mypath [file dirname [file dirname $scriptpath]] + #expect to be in folder #modpod-- + #Now we need to test if we are in a mounted folder vs an extracted folder + set container [file dirname $mypath] + if {[string match "#mounted-modpod-*" $container]} { + set mypath [file dirname $container] + } + set modver [string range [file tail [file dirname $scriptpath]] 8 end] ;# the containing folder is named #modpod-- + } else { + set mypath [file dirname $scriptpath] + set modver [file root [file tail [info script]]] + } + set mysegs [file split $mypath] + set overhang [list] + foreach libpath [tcl::tm::list] { + set libsegs [file split $libpath] ;#split and rejoin with '/' because sometimes module paths may have mixed \ & / + if {[file join $mysegs /] eq [file join [lrange $libsegs 0 [llength $mysegs]] /]} { + #mypath is below libpath + set overhang [lrange $mysegs [llength $libsegs]+1 end] + break + } + } + lassign [split $modver -] moduletail version + set ns [join [concat $overhang $moduletail] ::] + #if {![catch {package require modpod}]} { + # ::modpod::disconnect [info script] + #} + package provide $ns $version + namespace eval $ns $code +} ::} { + # + # Module procs here, where current namespace is that of the module. + # Package version can, if needed, be accessed as [uplevel 1 {set version}] + # Last element of module name: [uplevel 1 {set moduletail}] + # Full module name: [uplevel 1 {set ns}] + + # + # + # + + # + # + # + + # + # + # + +} diff --git a/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z b/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z new file mode 100644 index 00000000..a8f7b05a --- /dev/null +++ b/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z @@ -0,0 +1,2 @@ +#Do not remove the trailing ctrl-z character from this file + \ No newline at end of file diff --git a/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip b/src/bootsupport/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..665234dec0d8e52074344321c6b9813e78b18ed9 GIT binary patch literal 1275 zcmWIWW@Zs#00D&^uOh$k`3i4BQfmCTus%}|oQE_H|o_+vS69>b~ZLcCS zZ!j;O&&0s6osEG(7q=#uSvmQMDaFY}nFS?!CCNFpAqVqr8}RHEe_>zXyMZbC_L}Vn zwX5!AGMAmNNl;Y_)AnAcqW-q&&uPG1=Nxnl~O^07{emEb@ zpg-r8Q~SckuWp%4$+6v0f0ZM&H*d6DxK7w5_rZp52Q@dhiZy3E$hjDGGqQ4}?x`t% zo@#N_PW#EE#d|t%*}8sD`@dq}Yi~Q=XOvp7{DHudq6L}mR$Xs7F9^N~&6vX2vQxrk z>S~t=pRMN?FFcXHVd=~LnXbVm-fvDgZhUzm=jGIy{T&aiCrmW`6XB4-(PG+TFflcG zYf8{l#YIoLCraB@`);3ZzNT_EmnBo?`QTqCuEhL}6}P+_DXk`^ws`I49Gk9b%h#Ug zxSuJSF!Rf6!;?n!&2MtoPUW}Uu(2<|WWm)~Epv|8(1mMPDZX5*={H+DH76%|m!tB* zi+e-UZm?Ly&pj;5`NlRm$J=r16U9;?r78a(7@O=+&bTV<`Gb3Y;?yOlrPNP0mdyOS zE^C3i1oImm-}=Z`e~yLEI%8(S8giXSI(*lf(s|Q9#jM!&`%=GQ_O+vHLMz-CD#g|B zZA;v{Or^Z6q)Y0;6Z z0io;Xwfbip+Hkb<9cA7m=-$7#h$ZuK|70~oli2X7A@f3cIt^k^clIB;X}LRi((EIu zdg`Ue&mSz7VtgC0yyeG@x$|Tq-?&y!9|M?LqJ#cQI@js92+iZX-+!lx>@uc*s;Ifn9Cr;}JYIvPJtM47EagyQD zq9bSKoY_;-`ef1~nY&u8PkN3lI`U-F6{XfKi~htcm~ -Description: %description% -Rights: BSD -URL: %url% -Available: -Architecture: tcl -Subject: +Identifier: %package% +Version: %version% +Title: %title% +Creator: %name% <%email%> +Description: %description% +Rights: BSD +URL: %url% +Available: +Architecture: tcl +Subject: diff --git a/src/bootsupport/modules/punk/mix/templates/utility/a b/tcltest.bat b/src/bootsupport/modules/punk/mix/templates/utility/a b/tcltest.bat deleted file mode 100644 index 4f798a83..00000000 --- a/src/bootsupport/modules/punk/mix/templates/utility/a b/tcltest.bat +++ /dev/null @@ -1,7 +0,0 @@ -::lindex tcl;#\ -@call tclsh "%~dp0%~n0.bat" %* & goto :eof -# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl -puts stdout "script: [info script]" -puts stdout "argv: $::argc" -puts stdout "args: '$::argv'" - diff --git a/src/bootsupport/modules/punk/mix/templates/utility/tclbatheader.txt b/src/bootsupport/modules/punk/mix/templates/utility/tclbatheader.txt index b2e0367f..88326d54 100644 --- a/src/bootsupport/modules/punk/mix/templates/utility/tclbatheader.txt +++ b/src/bootsupport/modules/punk/mix/templates/utility/tclbatheader.txt @@ -1,3 +1,3 @@ -::lindex tcl;#\ -@call tclsh "%~dp0%~n0.bat" %* & goto :eof -# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl +::lindex tcl;#\ +@call tclsh "%~dp0%~n0.bat" %* & goto :eof +# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl diff --git a/src/bootsupport/modules/punk/mix/templates/utility/tclbattest.bat b/src/bootsupport/modules/punk/mix/templates/utility/tclbattest.bat index 396aea56..fd2e9511 100644 --- a/src/bootsupport/modules/punk/mix/templates/utility/tclbattest.bat +++ b/src/bootsupport/modules/punk/mix/templates/utility/tclbattest.bat @@ -1,8 +1,8 @@ -::lindex tcl;#\ -@call tclsh "%~dp0%~n0.bat" %* & goto :eof -# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl -puts stdout "exe: [info nameof]" -puts stdout "scr: [info script]" -puts stdout "argc: $::argc" -puts stdout "argv: '$::argv'" - +::lindex tcl;#\ +@call tclsh "%~dp0%~n0.bat" %* & goto :eof +# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl +puts stdout "exe: [info nameof]" +puts stdout "scr: [info script]" +puts stdout "argc: $::argc" +puts stdout "argv: '$::argv'" + diff --git a/src/bootsupport/modules/punk/mix/templates/utility/tclbattest2.bat b/src/bootsupport/modules/punk/mix/templates/utility/tclbattest2.bat index fbf2fcd0..4765515a 100644 --- a/src/bootsupport/modules/punk/mix/templates/utility/tclbattest2.bat +++ b/src/bootsupport/modules/punk/mix/templates/utility/tclbattest2.bat @@ -1,19 +1,19 @@ -::set - { -@goto start -# -- tcl bat -:start -@echo off -set script=%0 -echo %* -if exist %script%.bat set script=%script%.bat -tclsh %script% %* -goto end of BAT file -};unset - ;# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl - -puts stdout "exe: [info nameof]" -puts stdout "scr: [info script]" -puts stdout "argc: $::argc" -puts stdout "argv: '$::argv'" - -# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl\ -:end of BAT file +::set - { +@goto start +# -- tcl bat +:start +@echo off +set script=%0 +echo %* +if exist %script%.bat set script=%script%.bat +tclsh %script% %* +goto end of BAT file +};unset - ;# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl + +puts stdout "exe: [info nameof]" +puts stdout "scr: [info script]" +puts stdout "argc: $::argc" +puts stdout "argv: '$::argv'" + +# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl\ +:end of BAT file diff --git a/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/bootsupport/modules/punk/ns-0.1.0.tm index 10a8d9a5..70f924d7 100644 --- a/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -1166,24 +1166,6 @@ tcl::namespace::eval punk::ns { lappend allooclasses $cmd } } - if {[catch { - if {$cmd eq ""} { - #empty command was previously marked as "::" - too confusing - nslist updated to properly display empty string. - set nsorigin [namespace origin ${location}::] - } elseif {[string match :* $cmd]} { - set nsorigin [nseval $location "::namespace origin $cmd"] - } else { - set nsorigin [namespace origin [nsjoin $location $cmd]] - } - } errM]} { - puts stderr "get_ns_dicts failed to determine origin of command '$cmd' adding to 'undetermined'" - puts stderr "error message: $errM" - lappend allundetermined $cmd - } else { - if {[nsprefix $nsorigin] ne $location} { - lappend allimported $cmd - } - } } default { if {$ctype eq "imported"} { @@ -1242,6 +1224,25 @@ tcl::namespace::eval punk::ns { } } + #JMN + if {[catch { + if {$cmd eq ""} { + #empty command was previously marked as "::" - too confusing - nslist updated to properly display empty string. + set nsorigin [namespace origin ${location}::] + } elseif {[string match :* $cmd]} { + set nsorigin [nseval $location "::namespace origin $cmd"] + } else { + set nsorigin [namespace origin [nsjoin $location $cmd]] + } + } errM]} { + puts stderr "get_ns_dicts failed to determine origin of command '$cmd' adding to 'undetermined'" + puts stderr "error message: $errM" + lappend allundetermined $cmd + } else { + if {[nsprefix $nsorigin] ne $location} { + lappend allimported $cmd + } + } } if {$glob ne "*"} { set childtailmatches [lsearch -all -inline $childtails $glob] diff --git a/src/bootsupport/modules/punk/path-0.1.0.tm b/src/bootsupport/modules/punk/path-0.1.0.tm index 4abba695..933ef860 100644 --- a/src/bootsupport/modules/punk/path-0.1.0.tm +++ b/src/bootsupport/modules/punk/path-0.1.0.tm @@ -217,7 +217,8 @@ namespace eval punk::path { -directory -default "\uFFFF" -call-depth-internal -default 0 -type integer -antiglob_paths -default {} - *values -min 0 -max -1 -type string + *values -min 0 -max -1 -optional 1 -type string + tailglobs -multiple 1 } $args] lassign [dict values $argd] opts values set tailglobs [dict values $values] diff --git a/src/bootsupport/modules/punk/repo-0.1.1.tm b/src/bootsupport/modules/punk/repo-0.1.1.tm index 4b1e2f4f..ee2384b4 100644 --- a/src/bootsupport/modules/punk/repo-0.1.1.tm +++ b/src/bootsupport/modules/punk/repo-0.1.1.tm @@ -1447,6 +1447,7 @@ namespace eval punk::repo { #Must accept empty prefix - which is effectively noop. #MUCH faster version for absolute path prefix (pre-normalized) + #review - will error on file join if lrange returns empty list ie if prefix longer than path proc path_strip_alreadynormalized_prefixdepth {path prefix} { if {$prefix eq ""} { return $path @@ -1488,11 +1489,11 @@ namespace eval punk::repo { interp alias {} git_revision {} ::punk::repo::git_revision - interp alias {} gs {} git status -sb - interp alias {} gr {} ::punk::repo::git_revision - interp alias {} gl {} git log --oneline --decorate ;#decorate so stdout consistent with what we see on console - interp alias {} glast {} git log -1 HEAD --stat - interp alias {} gconf {} git config --global -l + interp alias {} gs {} shellrun::runconsole git status -sb + interp alias {} gr {} ::punk::repo::git_revision + interp alias {} gl {} shellrun::runconsole git log --oneline --decorate ;#decorate so stdout consistent with what we see on console + interp alias {} glast {} shellrun::runconsole git log -1 HEAD --stat + interp alias {} gconf {} shellrun::runconsole git config --global -l } namespace eval punk::repo::lib { diff --git a/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/bootsupport/modules/punkcheck-0.1.0.tm index 56d42b23..5d4f5c27 100644 --- a/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -37,7 +37,7 @@ namespace eval punkcheck { start_installer_event installfile_* #antiglob_dir & antiglob_file entries match the pattern at any level - should not contain path separators - variable default_antiglob_dir_core [list "#*" "_aside" ".git" ".fossil*"] + variable default_antiglob_dir_core [list "#*" "_aside" "_build" ".git" ".fossil*"] variable default_antiglob_file_core "" proc uuid {} { set has_twapi 0 @@ -1196,7 +1196,7 @@ namespace eval punkcheck { #and may be less error prone than doing slightly more opaue path manipulations at each recursion level to determine where we started #For consistency - we'll use the same mechanism in various recursive directory walking procedures such as this one. set CALLDEPTH [dict get $opts -call-depth-internal] ;#added for extra debug/sanity checking - clearer test for initial function call ie CALLDPEPTH = 0 - set max_depth [dict get $opts -max_depth] + set max_depth [dict get $opts -max_depth] ;# -1 for no limit set subdirlist [dict get $opts -subdirlist] ;# generally should be same length as CALLDEPTH - but user could prefill set fileglob [dict get $opts -glob] set createdir [dict get $opts -createdir] ;#defaults to zero to help avoid mistakes with initial target dir - required target subdirs are created regardless of this setting @@ -1598,7 +1598,7 @@ namespace eval punkcheck { } - if {$CALLDEPTH >= $max_depth} { + if {$max_depth != -1 && $CALLDEPTH >= $max_depth} { #don't process any more subdirs set subdirs [list] } else { diff --git a/src/bootsupport/modules/textblock-0.1.1.tm b/src/bootsupport/modules/textblock-0.1.1.tm index 055fdfcd..04e219bb 100644 --- a/src/bootsupport/modules/textblock-0.1.1.tm +++ b/src/bootsupport/modules/textblock-0.1.1.tm @@ -29,6 +29,7 @@ package require textutil tcl::namespace::eval textblock { #review - what about ansi off in punk::console? tcl::namespace::import ::punk::ansi::a ::punk::ansi::a+ + tcl::namespace::export block frame frame_cache framedef frametypes gcross height width widthtopline join join_basic list_as_table pad testblock tcl::namespace::eval class { variable opts_table_defaults @@ -836,7 +837,7 @@ tcl::namespace::eval textblock { set args_got_header_colspans 1 #check columns to left to make sure each new colspan for this column makes sense in the overall context #user may have to adjust colspans in order left to right to avoid these check errors - #note that 'all' represents span all up to the next non-zero defined colspan. + #note that 'any' represents span all up to the next non-zero defined colspan. set cspans [my header_colspans] set h 0 if {[llength $v] > [tcl::dict::size $cspans]} { @@ -846,34 +847,34 @@ tcl::namespace::eval textblock { if {$cidx == 0} { if {[tcl::string::is integer -strict $s]} { if {$s < 1} { - error "configure_column $cidx -header_colspans bad first value '$s' for header '$h' . First column cannot have span less than 1. use 'all' or a positive integer" + error "configure_column $cidx -header_colspans bad first value '$s' for header '$h' . First column cannot have span less than 1. use 'any' or a positive integer" } } else { - if {$s ne "all" && $s ne ""} { - error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'all'" + if {$s ne "any" && $s ne ""} { + error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'any'" } } } else { #if {![tcl::string::is integer -strict $s]} { - # if {$s ne "all" && $s ne ""} { - # error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'all'" + # if {$s ne "any" && $s ne ""} { + # error "configure_column $cidx -header_colspans unrecognised value '$s' for header '$h' - must be a positive integer or the keyword 'any'" # } #} else { set header_spans [tcl::dict::get $cspans $h] set remaining [lindex $header_spans 0] - if {$remaining ne "all"} { + if {$remaining ne "any"} { incr remaining -1 } #look at spans defined for previous cols #we are presuming previous column entries are valid - and only validating if our new entry is ok under that assumption for {set c 0} {$c < $cidx} {incr c} { set span [lindex $header_spans $c] - if {$span eq "all"} { - set remaining "all" + if {$span eq "any"} { + set remaining "any" } else { - if {$remaining eq "all"} { + if {$remaining eq "any"} { if {$span ne "0"} { - #a previous column has ended the 'all' span + #a previous column has ended the 'any' span set remaining [expr {$span -1}] } } else { @@ -886,8 +887,8 @@ tcl::namespace::eval textblock { } } } - if {$remaining eq "all"} { - #any int >0 ok - what about 'all' immediately following all? + if {$remaining eq "any"} { + #any int >0 ok - what about 'any' immediately following any? } else { if {$remaining > 0} { if {$s ne "0" && $s ne ""} { @@ -895,7 +896,7 @@ tcl::namespace::eval textblock { } } else { if {$s == 0} { - error "configure_column $cidx -header_colspans bad span $s for header '$h'. No span in place - need >=1 or 'all'" + error "configure_column $cidx -header_colspans bad span $s for header '$h'. No span in place - need >=1 or 'any'" } } } @@ -1020,10 +1021,11 @@ tcl::namespace::eval textblock { #return a dict keyed on header index with values representing colspans #e.g - # 0 {all 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} + # 0 {any 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} # method header_colspans {} { - set num_headers [my header_count_calc] + #set num_headers [my header_count_calc] + set num_headers [my header_count] set colspans_by_header [tcl::dict::create] tcl::dict::for {cidx cdef} $o_columndefs { set headerlist [tcl::dict::get $cdef -headers] @@ -1033,17 +1035,17 @@ tcl::namespace::eval textblock { set defined_span [lindex $colspans_for_column $h] set i 0 set spanremaining [lindex $headerspans 0] - if {$spanremaining ne "all"} { + if {$spanremaining ne "any"} { if {$spanremaining eq ""} { set spanremaining 1 } incr spanremaining -1 } foreach s $headerspans { - if {$s eq "all"} { - set spanremaining "all" + if {$s eq "any"} { + set spanremaining "any" } elseif {$s == 0} { - if {$spanremaining ne "all"} { + if {$spanremaining ne "any"} { incr spanremaining -1 } } else { @@ -1055,7 +1057,7 @@ tcl::namespace::eval textblock { if {$spanremaining eq "0"} { lappend headerspans 1 } else { - #"all" or an integer + #"any" or an integer lappend headerspans 0 } } else { @@ -1067,6 +1069,39 @@ tcl::namespace::eval textblock { return $colspans_by_header } + #e.g + # 0 {any 0 0 0} 1 {1 1 1 1} 2 {2 0 any 1} 3 {any 0 0 1} + #convert to + # 0 {4 0 0 0} 1 {1 1 1 1} 2 {2 0 1 1} 3 {3 0 0 1} + method header_colspans_numeric {} { + set hcolspans [my header_colspans] + if {![tcl::dict::size $hcolspans]} { + return + } + set numcols [llength [tcl::dict::get $hcolspans 0]] ;#assert: all are the same + tcl::dict::for {h spans} $hcolspans { + set c 0 ;#column index + foreach s $spans { + if {$s eq "any"} { + set spanlen 1 + for {set i [expr {$c+1}]} {$i < $numcols} {incr i} { + #next 'any' or non-zero ends an 'any' span + if {[lindex $spans $i] ne "0"} { + break + } + incr spanlen + } + #overwrite the 'any' with it's actual span + set modified_spans [dict get $hcolspans $h] + lset modified_spans $c $spanlen + dict set hcolspans $h $modified_spans + } + incr c + } + } + return $hcolspans + } + #should be configure_headerrow ? method configure_header {index_expression args} { #the header data being configured or returned here is mostly derived from the column defs and if necessary written to the column defs. @@ -1103,6 +1138,10 @@ tcl::namespace::eval textblock { #set val [tcl::dict::get $o_rowdefs $ridx $k] set infodict [tcl::dict::create] + #todo + # -blockalignments and -textalignments lists + # must match number of values if not empty? - e.g -blockalignments {left right "" centre left ""} + #if there is a value it overrides alignments specified on the column switch -- $k { -values { set header_row_items [list] @@ -1190,54 +1229,54 @@ tcl::namespace::eval textblock { if {[llength $v]} { set firstspan [lindex $v 0] set first_is_ok 0 - if {$firstspan eq "all"} { + if {$firstspan eq "any"} { set first_is_ok 1 } elseif {[tcl::string::is integer -strict $firstspan] && $firstspan > 0 && $firstspan <= $numcols} { set first_is_ok 1 } if {!$first_is_ok} { - error "textblock::table::configure_header -colspans first value '$firstspan' must be integer > 0 & <= $numcols or the string \"all\"" + error "textblock::table::configure_header -colspans first value '$firstspan' must be integer > 0 & <= $numcols or the string \"any\"" } #we don't mind if there are less colspans specified than columns.. the tail can be deduced from the leading ones specified (review) set remaining $firstspan - if {$remaining ne "all"} { + if {$remaining ne "any"} { incr remaining -1 } set spanview $v set sidx 1 - #because we allow 'all' - be careful when doing < or > comparisons - as we are mixing integer and string comparisons if we don't test for 'all' first + #because we allow 'any' - be careful when doing < or > comparisons - as we are mixing integer and string comparisons if we don't test for 'any' first foreach span [lrange $v 1 end] { - if {$remaining eq "all"} { - if {$span eq "all"} { - set remaining "all" + if {$remaining eq "any"} { + if {$span eq "any"} { + set remaining "any" } elseif {$span > 0} { - #ok to reset to higher val immediately or after an all and any number of following zeros + #ok to reset to higher val immediately or after an any and any number of following zeros if {$span > ($numcols - $sidx)} { lset spanview $sidx [a+ web-red]$span[a] - error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"all\".[a] $spanview" + error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"any\".[a] $spanview" } set remaining $span incr remaining -1 } else { - #zero following an all - leave remaining as all + #zero following an any - leave remaining as any } } else { if {$span eq "0"} { if {$remaining eq "0"} { lset spanview $sidx [a+ web-red]$span[a] - error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining. Require positive or \"all\" value.[a] $spanview" + error "textblock::table::configure_header -colspans sequence incorrect at span '$span' remaining is $remaining. Require positive or \"any\" value.[a] $spanview" } else { incr remaining -1 } } else { if {$remaining eq "0"} { - #ok for new span value of all or > 0 - if {$span ne "all" && $span > ($numcols - $sidx)} { + #ok for new span value of any or > 0 + if {$span ne "any" && $span > ($numcols - $sidx)} { lset spanview $sidx [a+ web-red]$span[a] - error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"all\".[a] $spanview" + error "textblock::table::configure_header -colspans sequence incorrect at span '$span'. Require span <= [expr {$numcols-$sidx}] or \"any\".[a] $spanview" } set remaining $span - if {$remaining ne "all"} { + if {$remaining ne "any"} { incr remaining -1 } } else { @@ -1760,8 +1799,8 @@ tcl::namespace::eval textblock { set hdrmap [tcl::dict::get $hmap only${opt_posn}] set topseps_h [tcl::dict::get $sep_elements_horizontal top$opt_posn] - set topseps_v [tcl::dict::get $sep_elements_vertical top$opt_posn] set midseps_h [tcl::dict::get $sep_elements_horizontal middle$opt_posn] + set topseps_v [tcl::dict::get $sep_elements_vertical top$opt_posn] set midseps_v [tcl::dict::get $sep_elements_vertical middle$opt_posn] set botseps_v [tcl::dict::get $sep_elements_vertical bottom$opt_posn] set onlyseps_v [tcl::dict::get $sep_elements_vertical only$opt_posn] @@ -1795,16 +1834,19 @@ tcl::namespace::eval textblock { #set hcolwidth [my column_width_configured $cidx] set hcell_line_blank [tcl::string::repeat " " $hcolwidth] - set all_colspans [my header_colspans] + set all_colspans [my header_colspans_numeric] + #put our framedef calls together + set fdef_header [textblock::framedef $ftype_header] + set framedef_leftbox [textblock::framedef -joins left $ftype_header] + set framedef_headerdown_same [textblock::framedef -joins {down} $ftype_header] + set framedef_headerdown_body [textblock::framedef -joins [list down-$fname_body] $ftype_header] #default span_extend_map - used as base to customise with specific joins - set fdef_header [textblock::framedef $ftype_header] set span_extend_map [tcl::dict::create \ vll " "\ tlc [tcl::dict::get $fdef_header hlt]\ blc [tcl::dict::get $fdef_header hlb]\ ] - set framedef_leftbox [textblock::framedef $ftype_header -joins left] #used for colspan-zero header frames @@ -1851,7 +1893,10 @@ tcl::namespace::eval textblock { } #puts ">>> headerspans: $headerspans cidx: $cidx" - if {$this_span eq "all" || $this_span > 0} { + #if {$this_span eq "any" || $this_span > 0} {} + #changed to processing only numeric colspans + + if {$this_span > 0} { set startmap [tcl::dict::get $hmap $rowpos${opt_posn}] #look at spans in header below to determine joins required at blc if {$show_seps_v} { @@ -1882,7 +1927,7 @@ tcl::namespace::eval textblock { # -boxlimits $hlims -boxmap $startmap -joins $header_joins $hval\ # ] - if {$this_span eq "1"} { + if {$this_span == 1} { #write the actual value now set cellcontents $hval } else { @@ -1894,13 +1939,20 @@ tcl::namespace::eval textblock { -boxlimits $hlims -boxmap $startmap -joins $header_joins $cellcontents\ ] - if {$this_span ne "1"} { + if {$this_span != 1} { #puts "===>\n$header_cell_startspan\n<===" set spanned_parts [list $header_cell_startspan] - #assert this_span == "all" or >1 ie a header that spans other columns + #assert this_span == "any" or >1 ie a header that spans other columns #therefore more parts to append #set remaining_cols [lrange [tcl::dict::keys $o_columndefs] $cidx end] set remaining_spans [lrange $headerspans $cidx+1 end] + set spanval [join $remaining_spans ""] ;#so we can test for all zeros + set spans_to_rhs 0 + if {[expr {$spanval}] == 0} { + #puts stderr "SPANS TO RHS" + set spans_to_rhs 1 + } + #puts ">> remaining_spans: $remaining_spans" set spancol [expr {$cidx + 1}] set h_lines [lrepeat $rowh ""] @@ -1944,13 +1996,11 @@ tcl::namespace::eval textblock { if {[llength $next_spanlist]} { set spanbelow [lindex $next_spanlist $spancol] if {$spanbelow != 0} { - set downbox [textblock::framedef $ftype_header -joins {down}] - tcl::dict::set this_span_map blc [tcl::dict::get $downbox hlbj] ;#horizontal line bottom with down join - to same frametype + tcl::dict::set this_span_map blc [tcl::dict::get $framedef_headerdown_same hlbj] ;#horizontal line bottom with down join - to same frametype } } else { #join to body - set downbox [textblock::framedef $ftype_header -joins [list down-$fname_body]] - tcl::dict::set this_span_map blc [tcl::dict::get $downbox hlbj] ;#horizontal line bottom with down join - from header frametype to body frametype + tcl::dict::set this_span_map blc [tcl::dict::get $framedef_headerdown_body hlbj] ;#horizontal line bottom with down join - from header frametype to body frametype } } @@ -1980,17 +2030,38 @@ tcl::namespace::eval textblock { #spanned_parts are all built with textblock::frame - therefore uniform-width lines - can use join_basic set spanned_frame [textblock::join_basic -- {*}$spanned_parts] - if {$hrow == 0} { - set hlims $header_boxlimits_toprow + if {$spans_to_rhs} { + if {$cidx == 0} { + set fake_posn solo + } else { + set fake_posn right + } + set x_limj [my Get_boxlimits_and_joins $fake_posn $fname_body] + if {$hrow == 0} { + set x_boxlimits_toprow [tcl::dict::get $x_limj boxlimits_top] + set hlims [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $x_boxlimits_toprow] + } else { + set x_boxlimits_position [tcl::dict::get $x_limj boxlimits] + set hlims [struct::set intersect [tcl::dict::get $o_opts_table_effective -framelimits_header] $x_boxlimits_position] + } } else { - set hlims $header_boxlimits + if {$hrow == 0} { + set hlims $header_boxlimits_toprow + } else { + set hlims $header_boxlimits + } } if {!$show_seps_v} { set hlims [struct::set difference $hlims $headerseps_v] } if {![tcl::dict::get $o_opts_table -show_edge]} { - #use the edge_parts corresponding to the column being written to ie use opt_posn - set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts $rowpos$opt_posn] ] + if {$spans_to_rhs} { + #assert fake_posn has been set above based on cidx and spans_to_rhs + set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts ${rowpos}$fake_posn] ] + } else { + #use the edge_parts corresponding to the column being written to ie use opt_posn + set hlims [struct::set difference $hlims [tcl::dict::get $::textblock::class::header_edge_parts $rowpos$opt_posn] ] + } } set spacemap [list hl " " vl " " tlc " " blc " " trc " " brc " "] ;#transparent overlay elements @@ -2005,7 +2076,21 @@ tcl::namespace::eval textblock { #set spanned_frame [overtype::renderspace -experimental test_mode -transparent 1 $spanned_frame $hblock] #spanned values default left - todo make configurable + + #TODO + #consider that currently blockaligning for spanned columns must always use the blockalign value from the starting column of the span + #we could conceivably have an override that could be set somehow with configure_header for customization of alignments of individual spans or span sizes? + #this seems like a likely requirement. The first spanned column may well have different alignment requirements than the span. + #(e.g if first spanned col happens to be numeric it probably warrants right textalign (if not blockalign) but we don't necessarily want the spanning header or even a non-spanning header to be right aligned) + set spanned_frame [overtype::block -blockalign $col_blockalign -overflow 1 -transparent 1 $spanned_frame $hblock] + #POTENTIAL BUG (fixed with spans_to_rhs above) + #when -blockalign right and colspan extends to rhs - last char of longest of that spanlength will overlap right edge (if show_edge 1) + #we need to shift 1 to the left when doing our overtype with blockalign right + #we normally do this by using the hlims based on position - effectively we need a rhs position set of hlims for anything that colspans to the right edge + #(even though the column position may be left or inner) + + } else { #this_span == 1 @@ -2301,11 +2386,9 @@ tcl::namespace::eval textblock { error "table::get_column_cells_by_index no such index $index_expression. Valid range is $range" } #assert cidx is integer >=0 + set num_header_rows [my header_count] set cdef [tcl::dict::get $o_columndefs $cidx] set headerlist [tcl::dict::get $cdef -headers] - set num_header_rows [my header_count] - - set ansibase_body [tcl::dict::get $o_opts_table -ansibase_body] set ansibase_col [tcl::dict::get $cdef -ansibase] set textalign [tcl::dict::get $cdef -textalign] switch -- $textalign { @@ -2316,20 +2399,23 @@ tcl::namespace::eval textblock { } } + set ansibase_body [tcl::dict::get $o_opts_table -ansibase_body] set ansibase_header [tcl::dict::get $o_opts_table -ansibase_header] #set header_underlay $ansibase_header$cell_line_blank #set hdrwidth [my column_width_configured $cidx] - set all_colspans [my header_colspans] - + #set all_colspans [my header_colspans] + #we need to replace the 'any' entries with their actual span lengths before passing any -colspan values to column_datawidth - hence use header_colspans_numeric + set all_colspans [my header_colspans_numeric] + #JMN #store configured widths so we don't look up for each header line - set configured_widths [list] - foreach c [tcl::dict::keys $o_columndefs] { - #lappend configured_widths [my column_width $c] - #we don't just want the width of the column in the body - or the headers will get truncated - lappend configured_widths [my column_width_configured $c] - } + #set configured_widths [list] + #foreach c [tcl::dict::keys $o_columndefs] { + # #lappend configured_widths [my column_width $c] + # #we don't just want the width of the column in the body - or the headers will get truncated + # lappend configured_widths [my column_width_configured $c] + #} set output [tcl::dict::create] tcl::dict::set output headers [list] @@ -2342,7 +2428,7 @@ tcl::namespace::eval textblock { set this_span [lindex $headerrow_colspans $cidx] #set this_hdrwidth [lindex $configured_widths $cidx] - set this_hdrwidth [my column_datawidth $cidx -headers 1 -colspan $this_span] ;#widest of headers in this col with same span - allows textalign to work with blockalign + set this_hdrwidth [my column_datawidth $cidx -headers 1 -colspan $this_span -cached 1] ;#widest of headers in this col with same span - allows textalign to work with blockalign set hcell_line_blank [tcl::string::repeat " " $this_hdrwidth] set hcell_lines [lrepeat $header_maxdataheight $hcell_line_blank] @@ -2704,7 +2790,7 @@ tcl::namespace::eval textblock { set width_max [expr {max($test_width,$width_max)}] continue } - if {$spanc eq "all" || $spanc > 1} { + if {$spanc eq "any" || $spanc > 1} { set spanned [list] ;#spanned is other columns spanned - not including this one set cnext [expr {$cidx +1}] set spanlength [lindex $colspans $cnext] @@ -2773,10 +2859,12 @@ tcl::namespace::eval textblock { set opts [tcl::dict::create\ -headers 0\ -footers 0\ - -colspan *\ + -colspan unspecified\ -data 1\ -cached 1\ ] + #NOTE: -colspan any is not the same as * + # #-colspan is relevant to header/footer data only foreach {k v} $args { switch -- $k { @@ -2789,6 +2877,17 @@ tcl::namespace::eval textblock { } } set opt_colspan [tcl::dict::get $opts -colspan] + switch -- $opt_colspan { + * - unspecified {} + any { + error "method column_datawidth invalid -colspan '$opt_colspan' must be * or an integer >= 0 (use header_colspans_numeric to get actual spans)" + } + default { + if {![string is integer -strict $opt_colspan]} { + error "method column_datawidth invalid -colspan '$opt_colspan' must be * or an integer >= 0" + } + } + } set cidx [lindex [tcl::dict::keys $o_columndefs] $index_expression] @@ -2801,26 +2900,26 @@ tcl::namespace::eval textblock { set bwidest 0 set fwidest 0 if {[tcl::dict::get $opts -headers]} { - if {$opt_colspan eq "*"} { + if {$opt_colspan in {* unspecified}} { set hwidest [tcl::dict::get $o_columnstates $cidx maxwidthheaderseen] } else { + #this is not cached + # -- --- --- --- set colheaders [tcl::dict::get $o_columndefs $cidx -headers] - set all_colspans_by_header [my header_colspans] + set all_colspans_by_header [my header_colspans_numeric] set hlist [list] tcl::dict::for {hrow cspans} $all_colspans_by_header { set s [lindex $cspans $cidx] - #todo - map 'all' entries to a number? - #we should build a version of header_colspans that does this if {$s eq $opt_colspan} { lappend hlist [lindex $colheaders $hrow] } } - #set widest1 [tcl::mathfunc::max {*}[lmap v $cmds {tcl::string::length $v}]] if {[llength $hlist]} { set hwidest [tcl::mathfunc::max {*}[lmap v $hlist {textblock::width $v}]] } else { set hwidest 0 } + # -- --- --- --- } } if {[tcl::dict::get $opts -data]} { @@ -2835,8 +2934,28 @@ tcl::namespace::eval textblock { #assert cidx is >=0 integer in valid range of keys for o_columndefs set values [list] + set hwidest 0 if {[tcl::dict::get $opts -headers]} { - lappend values {*}[tcl::dict::get $o_columndefs $cidx -headers] + if {$opt_colspan in {* unspecified}} { + lappend values {*}[tcl::dict::get $o_columndefs $cidx -headers] + } else { + # -- --- --- --- + set colheaders [tcl::dict::get $o_columndefs $cidx -headers] + set all_colspans_by_header [my header_colspans_numeric] + set hlist [list] + tcl::dict::for {hrow cspans} $all_colspans_by_header { + set s [lindex $cspans $cidx] + if {$s eq $opt_colspan} { + lappend hlist [lindex $colheaders $hrow] + } + } + if {[llength $hlist]} { + set hwidest [tcl::mathfunc::max {*}[lmap v $hlist {textblock::width $v}]] + } else { + set hwidest 0 + } + # -- --- --- --- + } } if {[tcl::dict::get $opts -data]} { if {[tcl::dict::exists $o_columndata $cidx]} { @@ -2847,9 +2966,10 @@ tcl::namespace::eval textblock { lappend values {*}[tcl::dict::get $o_columndefs $cidx -footers] } if {[llength $values]} { - set widest [tcl::mathfunc::max {*}[lmap v $values {textblock::width $v}]] + set valwidest [tcl::mathfunc::max {*}[lmap v $values {textblock::width $v}]] + set widest [expr {max($valwidest,$hwidest)}] } else { - set widest 0 + set widest $hwidest } return $widest } @@ -3143,24 +3263,43 @@ tcl::namespace::eval textblock { set colspans_for_column [tcl::dict::get $o_columndefs $cidx -header_colspans] set spaninfo [list] set numcols [tcl::dict::size $o_columndefs] - #note that 'all' can occur in positions other than column 0 - meaning all remaining + #note that 'any' can occur in positions other than column 0 - meaning any remaining until next non-zero span tcl::dict::for {hrow rawspans} $spans_by_header { set thiscol_spanval [lindex $rawspans $cidx] - if {$thiscol_spanval eq "all" || $thiscol_spanval > 0} { + if {$thiscol_spanval eq "any" || $thiscol_spanval > 0} { set spanstartcol $cidx ;#own column - if {$thiscol_spanval eq "all"} { - set spanlen [expr {$numcols - $cidx}] + if {$thiscol_spanval eq "any"} { + #scan right to first non-zero to get actual length of 'any' span + #REVIEW! + set spanlen 1 + for {set i [expr {$cidx+1}]} {$i < $numcols} {incr i} { + #abort at next any or number or empty string + if {[lindex $rawspans $i] ne "0"} { + break + } + incr spanlen + } + #set spanlen [expr {$numcols - $cidx}] } else { set spanlen $thiscol_spanval } } else { - #look left til we see an all or a non-zero value + #look left til we see an any or a non-zero value for {set i $cidx} {$i > -1} {incr i -1} { set s [lindex $rawspans $i] - if {$s eq "all" || $s > 0} { + if {$s eq "any" || $s > 0} { set spanstartcol $i - if {$s eq "all"} { - set spanlen [expr {$numcols - $i}] + if {$s eq "any"} { + #REVIEW! + #set spanlen [expr {$numcols - $i}] + set spanlen 1 + #now scan right to see how long the 'any' actually is + for {set j [expr {$i+1}]} {$j < $numcols} {incr j} { + if {[lindex $rawspans $j] ne "0"} { + break + } + incr spanlen + } } else { set spanlen $s } @@ -3295,7 +3434,7 @@ tcl::namespace::eval textblock { set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent $TSUB $table[unset table] $nextcol] #JMN - #set nextcol [textblock::join [textblock::block $padwidth $height "\uFFFF"] $nextcol] + #set nextcol [textblock::join -- [textblock::block $padwidth $height "\uFFFF"] $nextcol] #set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent \uFFFF $table $nextcol] } incr padwidth $bodywidth @@ -3303,7 +3442,7 @@ tcl::namespace::eval textblock { } if {[llength $cols]} { - #return [textblock::join {*}$blocks] + #return [textblock::join -- {*}$blocks] if {[tcl::dict::get $o_opts_table -show_edge]} { #title is considered part of the edge ? set offset 1 ;#make configurable? @@ -3399,11 +3538,11 @@ tcl::namespace::eval textblock { } else { set table [overtype::renderspace -startcolumn [expr {$padwidth + 1}] -overflow 1 -experimental test_mode -transparent $TSUB $table $nextcol] - #set nextcol [textblock::join [textblock::block $padwidth $height $TSUB] $nextcol] + #set nextcol [textblock::join -- [textblock::block $padwidth $height $TSUB] $nextcol] #set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent $TSUB $table[unset table] $nextcol] #JMN - #set nextcol [textblock::join [textblock::block $padwidth $height "\uFFFF"] $nextcol] + #set nextcol [textblock::join -- [textblock::block $padwidth $height "\uFFFF"] $nextcol] #set table [overtype::renderspace -overflow 1 -experimental test_mode -transparent \uFFFF $table $nextcol] } incr padwidth $bodywidth @@ -3411,7 +3550,7 @@ tcl::namespace::eval textblock { } if {[llength $cols]} { - #return [textblock::join {*}$blocks] + #return [textblock::join -- {*}$blocks] if {[tcl::dict::get $o_opts_table -show_edge]} { #title is considered part of the edge ? set offset 1 ;#make configurable? @@ -3517,7 +3656,7 @@ tcl::namespace::eval textblock { set header_build [overtype::renderspace -startcolumn [expr {$padwidth + 1}] -overflow 1 -experimental test_mode -transparent $TSUB $header_build[unset header_build] $nextcol_header[unset nextcol_header]] } lappend body_blocks $nextcol_body - #set body_build [textblock::join $body_build[unset body_build] $nextcol_body] + #set body_build [textblock::join -- $body_build[unset body_build] $nextcol_body] } incr padwidth $bodywidth incr colposn @@ -3595,7 +3734,6 @@ tcl::namespace::eval textblock { #Note: A textblock does not necessarily have lines the same length - either in number of characters or print-width # tcl::namespace::eval textblock { - tcl::namespace::export block width tcl::namespace::eval cd { #todo - save and restore existing tcl::namespace::export in case macros::cd has default exports in future tcl::namespace::eval ::term::ansi::code::macros::cd {tcl::namespace::export *} @@ -3605,18 +3743,29 @@ tcl::namespace::eval textblock { proc spantest {} { set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] $t configure_column 0 -headers [list span3 "1-span4\n2-span4 second line" span5/5 "span-all etc blah 123 hmmmmm" span2] - $t configure_column 0 -header_colspans {3 4 5 all 2} + $t configure_column 0 -header_colspans {3 4 5 any 2} $t configure_column 2 -headers {"" "" "" "" c2span2_etc} $t configure_column 2 -header_colspans {0 0 0 0 2} $t configure -show_header 1 -ansiborder_header [a+ cyan] return $t } + proc spantest1 {} { + set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] + $t configure_column 0 -headers [list "span3-any longer than other span any" "1-span4\n2-span4 second line" "span-any short aligned?" "span5/5 longest etc blah 123" span2] + $t configure_column 0 -header_colspans {any 4 any 5 2} + $t configure_column 2 -headers {"" "" "" "" c2span2_etc} + $t configure_column 2 -header_colspans {0 0 0 0 2} + $t configure_column 3 -header_colspans {1 0 0 0 0} + $t configure -show_header 1 -ansiborder_header [a+ cyan] + $t configure_column 0 -blockalign right ;#trigger KNOWN BUG overwrite of right edge by last char of spanning col (in this case fullspan from left - but also happens for any span extending to rhs) + return $t + } #more complex colspans proc spantest2 {} { set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] $t configure_column 0 -headers {c0span3 c0span4 c0span1 "c0span-all etc blah 123 hmmmmm" c0span2} - $t configure_column 0 -header_colspans {3 4 1 all 2} + $t configure_column 0 -header_colspans {3 4 1 any 2} $t configure_column 1 -header_colspans {0 0 2 0 0} $t configure_column 2 -headers {"" "" "" "" c2span2} $t configure_column 2 -header_colspans {0 0 0 0 2} @@ -3625,9 +3774,9 @@ tcl::namespace::eval textblock { return $t } proc spantest3 {} { - set t [list_as_table -columns 5 -return tableobjec {a b c d e aa bb cc dd ee X Y}] + set t [list_as_table -columns 5 -return tableobject {a b c d e aa bb cc dd ee X Y}] $t configure_column 0 -headers {c0span3 c0span4 c0span1 "c0span-all etc blah 123 hmmmmm" "c0span2 etc blah" c0span1} - $t configure_column 0 -header_colspans {3 4 1 all 2 1} + $t configure_column 0 -header_colspans {3 4 1 any 2 1} $t configure_column 1 -header_colspans {0 0 4 0 0 1} $t configure_column 1 -headers {"" "" "c1span4" "" "" "c1nospan"} $t configure_column 2 -headers {"" "" "" "" "" c2span2} @@ -3650,6 +3799,7 @@ tcl::namespace::eval textblock { -choices {table tableobject}\ -help "default choice 'table' returns the displayable table output" -compact -default 1 -type boolean -help "compact true defaults: -show_vseps 0 -show_header 0 -show_edge 0" + -frame -default 1 -type boolean -show_vseps -default "" -type boolean -show_header -default "" -type boolean -show_edge -default "" -type boolean @@ -3690,21 +3840,24 @@ tcl::namespace::eval textblock { } set cat_reactive_nonmetal [list H C N O F P S Cl Se Br I] - set ansi [a+ {*}$fc web-black Web-lightgreen] + #set ansi [a+ {*}$fc web-black Web-lightgreen] + set ansi [a+ {*}$fc black Term-113] set val [list ansi $ansi cat reactive_nonmetal] foreach e $cat_reactive_nonmetal { tcl::dict::set ecat $e $val } set cat [list Li Na K Rb Cs Fr] - set ansi [a+ {*}$fc web-black Web-Khaki] + #set ansi [a+ {*}$fc web-black Web-Khaki] + set ansi [a+ {*}$fc black Term-lightgoldenrod2] set val [list ansi $ansi cat alkali_metals] foreach e $cat { tcl::dict::set ecat $e $val } set cat [list Sc Ti V Cr Mn Fe Co Ni Cu Zn Y Zr Nb Mo Tc Ru Rh Pd Ag Cd Hf Ta W Re Os Ir Pt Au Hg Rf Db Sg Bh Hs] - set ansi [a+ {*}$fc web-black Web-lightsalmon] + #set ansi [a+ {*}$fc web-black Web-lightsalmon] + set ansi [a+ {*}$fc black Term-orange1] set val [list ansi $ansi cat transition_metals] foreach e $cat { tcl::dict::set ecat $e $val @@ -3718,7 +3871,8 @@ tcl::namespace::eval textblock { } set cat [list B Si Ge As Sb Te At] - set ansi [a+ {*}$fc web-black Web-turquoise] + #set ansi [a+ {*}$fc web-black Web-turquoise] + set ansi [a+ {*}$fc black Brightcyan] set val [list ansi $ansi cat metalloids] foreach e $cat { tcl::dict::set ecat $e $val @@ -3739,7 +3893,8 @@ tcl::namespace::eval textblock { } set cat [list La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu] - set ansi [a+ {*}$fc web-black Web-tan] + #set ansi [a+ {*}$fc web-black Web-tan] + set ansi [a+ {*}$fc black Term-tan] set val [list ansi $ansi cat lanthanoids] foreach e $cat { tcl::dict::set ecat $e $val @@ -3794,14 +3949,22 @@ tcl::namespace::eval textblock { $t configure \ -frametype_header light\ - -ansiborder_header [a+ {*}$fc web-white]\ - -ansibase_header [a+ {*}$fc Web-black]\ - -ansibase_body [a+ {*}$fc Web-black]\ - -ansiborder_body [a+ {*}$fc web-black]\ + -ansiborder_header [a+ {*}$fc brightwhite]\ + -ansibase_header [a+ {*}$fc Black]\ + -ansibase_body [a+ {*}$fc Black]\ + -ansiborder_body [a+ {*}$fc black]\ -frametype block + #-ansiborder_header [a+ {*}$fc web-white]\ + if {$opt_return eq "table"} { - set output [textblock::frame -ansiborder [a+ {*}$fc Web-black web-cornflowerblue] -type heavy -title "[a+ {*}$fc Web-black] Periodic Table " [$t print]] + if {[dict get $opts -frame]} { + #set output [textblock::frame -ansiborder [a+ {*}$fc Black web-cornflowerblue] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] + #set output [textblock::frame -ansiborder [a+ {*}$fc Black term-deepskyblue2] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] + set output [textblock::frame -ansiborder [a+ {*}$fc Black cyan] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] + } else { + set output [$t print] + } $t destroy return $output } @@ -4106,8 +4269,8 @@ tcl::namespace::eval textblock { set textblock [textutil::tabify::untabify2 $textblock $tw] } if {[punk::ansi::ta::detect $textblock]} { - #stripansiraw slightly faster than stripansi - and won't affect width (avoid detect_g0/conversions) - set textblock [punk::ansi::stripansiraw $textblock] + #ansistripraw slightly faster than ansistrip - and won't affect width (avoid detect_g0/conversions) + set textblock [punk::ansi::ansistripraw $textblock] } if {[tcl::string::last \n $textblock] >= 0} { return [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] @@ -4123,7 +4286,7 @@ tcl::namespace::eval textblock { set tl $textblock } if {[punk::ansi::ta::detect $tl]} { - set tl [punk::ansi::stripansiraw $tl] + set tl [punk::ansi::ansistripraw $tl] } return [punk::char::ansifreestring_width $tl] } @@ -4158,9 +4321,9 @@ tcl::namespace::eval textblock { } set textblock [textutil::tabify::untabify2 $textblock $tw] } - #stripansiraw on entire block in one go rather than line by line - result should be the same - review - make tests + #ansistripraw on entire block in one go rather than line by line - result should be the same - review - make tests if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansiraw $textblock] + set textblock [punk::ansi::ansistripraw $textblock] } if {[tcl::string::last \n $textblock] >= 0} { #set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]] @@ -4189,16 +4352,16 @@ tcl::namespace::eval textblock { } set block [textutil::tabify::untabify2 $block $tw] if {[tcl::string::last \n $block] >= 0} { - return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [stripansi $v]}]] + return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [ansistrip $v]}]] } if {[catch {llength $block}]} { - return [::punk::char::string_width [stripansi $block]] + return [::punk::char::string_width [ansistrip $block]] } if {[llength $block] == 0} { #could be just a whitespace string return [tcl::string::length $block] } - return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [stripansi $v]}]] + return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [ansistrip $v]}]] } #we shouldn't make textblock depend on the punk pipeline system @@ -4279,9 +4442,21 @@ tcl::namespace::eval textblock { set lines [list] + set padcharsize [punk::ansi::printing_length $padchar] + set pad_has_ansi [punk::ansi::ta::detect $padchar] if {$block eq ""} { #we need to treat as a line - return [tcl::string::repeat $padchar $width] + set repeats [expr {int(ceil($width / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + #TODO + #review - what happens when padchar has ansi, or the width would split a double-wide unicode char? + #we shouldn't be using string range if there is ansi - (overtype? ansistring range?) + #we should use overtype with suitable replacement char (space?) for chopped double-wides + if {!$pad_has_ansi} { + return [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $width-1] + } else { + set base [tcl::string::repeat " " $width] + return [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } } #review - tcl format can only pad with zeros or spaces? @@ -4321,6 +4496,7 @@ tcl::namespace::eval textblock { } set line_chunks [list] set line_len 0 + set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad foreach {pt ansi} $parts { if {$pt ne ""} { set has_nl [expr {[tcl::string::last \n $pt]>=0}] @@ -4335,12 +4511,26 @@ tcl::namespace::eval textblock { foreach pl $partlines { lappend line_chunks $pl #incr line_len [punk::char::ansifreestring_width $pl] - incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak + incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW if {$p != $last} { #do padding set missing [expr {$width - $line_len}] if {$missing > 0} { - set pad [tcl::string::repeat $padchar $missing] + #commonly in a block - many lines will have the same pad - cache based on missing + + #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] + } else { + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } + dict set pad_cache $missing $pad + } switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad @@ -4397,7 +4587,18 @@ tcl::namespace::eval textblock { #pad last line set missing [expr {$width - $line_len}] if {$missing > 0} { - set pad [tcl::string::repeat $padchar $missing] + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] + } else { + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } + } + #set pad [tcl::string::repeat $padchar $missing] switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad @@ -4667,6 +4868,7 @@ tcl::namespace::eval textblock { # Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner #" set argd [punk::args::get_dict { + -- -type none -optional 0 -help "end of options marker -- is mandatory because joined blocks may easily conflict with flags" -ansiresets -type any -default auto blocks -type any -multiple 1 } $args] @@ -4726,13 +4928,22 @@ tcl::namespace::eval textblock { -ansiresets { if {[lindex $args 2] eq "--"} { set blocks [lrange $args 3 end] + set ansiresets [lindex $args 1] } else { - set blocks [lrange $args 2 end] + error "end of opts marker -- is mandatory." } - set ansiresets [lindex $args 1] } default { - set blocks $args + if {[catch {tcl::prefix::match {-ansiresets} [lindex $args 0]} fullopt]} { + error "first flag must be -ansiresets or end of opts marker --" + } else { + if {[lindex $args 2] eq "--"} { + set blocks [lrange $args 3 end] + set ansiresets [lindex $args 1] + } else { + error "end of opts marker -- is mandatory" + } + } } } @@ -4836,11 +5047,12 @@ tcl::namespace::eval textblock { proc example3 {{text "test\netc\nmore text"}} { package require patternpunk - .= textblock::join [punk::lib::list_as_lines -- [list 1 2 3 4 5 6 7]] [>punk . lhs] |> .=>1 textblock::join " " |> .=>1 textblock::join $text |> .=>1 textblock::join [>punk . rhs] |> .=>1 textblock::join [punk::lib::list_as_lines -- [lrepeat 7 " | "]] + .= textblock::join -- [punk::lib::list_as_lines -- [list 1 2 3 4 5 6 7]] [>punk . lhs] |> .=>1 textblock::join -- " " |> .=>1 textblock::join -- $text |> .=>1 textblock::join -- [>punk . rhs] |> .=>1 textblock::join -- [punk::lib::list_as_lines -- [lrepeat 7 " | "]] } proc example2 {{text "test\netc\nmore text"}} { package require patternpunk .= textblock::join\ + --\ [punk::lib::list_as_lines -- [list 1 2 3 4 5 6 7 8]]\ [>punk . lhs]\ " "\ @@ -4900,67 +5112,96 @@ tcl::namespace::eval textblock { } variable frametypes - set frametypes [list light heavy arc double block block1 ascii altg] + set frametypes [list light heavy arc double block block1 block2 ascii altg] #class::table needs to be able to determine valid frametypes proc frametypes {} { variable frametypes return $frametypes } proc frametype {f} { - variable frametypes - set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] #set custom_keys [list hl hlt hlb vl vll vlr tlc trc blc brc] - if {$f ni $frametypes} { - set is_custom_dict_ok 1 - if {[llength $f] %2 == 0} { - #custom dict may leave out keys - but cannot have unknown keys - foreach {k v} $f { - switch -- $k { - hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} - default { - #k not in custom_keys - set is_custom_dict_ok 0 - break + switch -- $f { + light - heavy - arc - double - block - block1 - block2 - ascii - altg { + return [tcl::dict::create category predefined type $f] + } + default { + set is_custom_dict_ok 1 + if {[llength $f] %2 == 0} { + #custom dict may leave out keys - but cannot have unknown keys + foreach {k v} $f { + switch -- $k { + hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {} + default { + #k not in custom_keys + set is_custom_dict_ok 0 + break + } } } + } else { + set is_custom_dict_ok 0 } - } else { - set is_custom_dict_ok 0 - } - if {!$is_custom_dict_ok} { - error "frame option -type must be one of known types: $frametypes or a dictionary with any of keys hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" + if {!$is_custom_dict_ok} { + error "frame option -type must be one of known types: $textblock::frametypes or a dictionary with any of keys hl,hlt,hlb,vl,vll,vlr,tlc,trc,blc,brc" + } + set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] + set custom_frame [tcl::dict::merge $default_custom $f] + return [tcl::dict::create category custom type $custom_frame] } - set custom_frame [tcl::dict::merge $default_custom $f] - return [tcl::dict::create category custom type $custom_frame] - } else { - return [tcl::dict::create category predefined type $f] } } variable framedef_cache [tcl::dict::create] - proc framedef {f args} { + proc framedef {args} { #unicode box drawing only provides enough characters for seamless joining of unicode boxes light and heavy. #e.g with characters such as \u2539 Box Drawings Right Light and Left Up Heavy. #the double glyphs in box drawing can do a limited set of joins to light lines - but not enough for seamless table layouts. #the arc set can't even join to itself e.g with curved equivalents of T-like shapes + + #we use the simplest cache_key possible - performance sensitive as called multiple times in table building. variable framedef_cache - set cache_key [concat $f $args] + set cache_key $args if {[tcl::dict::exists $framedef_cache $cache_key]} { return [tcl::dict::get $framedef_cache $cache_key] } + set argopts [lrange $args 0 end-1] + set f [lindex $args end] + + #here we avoid the punk::args usage on the happy path, even though punk::args is fairly fast, in favour of an even faster literal switch on the happy path + #this means we have some duplication in where our flags/opts are defined: here in opts, and in spec below to give nicer error output without affecting performance. + #It also means we can't specify checks on the option types etc set opts [tcl::dict::create\ -joins ""\ -boxonly 0\ ] - foreach {k v} $args { + set bad_option 0 + foreach {k v} $argopts { switch -- $k { -joins - -boxonly { tcl::dict::set opts $k $v } default { - error "framedef unknown option '$k'. Known options [tcl::dict::keys $opts]" + set bad_option + break } } } + if {[llength $args] % 2 == 0 || $bad_option} { + #no framedef supplied, or unrecognised opt seen + set spec [string map [list $::textblock::frametypes] { + *proc -name textblock::framedef + -joins -default "" -help "List of join directions, any of: up down left right + or those combined with another frametype e.g left-heavy down-light" + -boxonly -default 0 -help "-boxonly true restricts results to the corner,vertical and horizontal box elements + It excludes the extra top and side join elements htlj,hlbj,vllj,vlrj" + *values -min 1 -max 1 + frametype -help "name from the predefined frametypes: + or an adhoc + }] + append spec \n "frametype -help \"A predefined \"" + punk::args::get_dict $spec $args + return + } + set joins [tcl::dict::get $opts -joins] set boxonly [tcl::dict::get $opts -boxonly] @@ -5986,6 +6227,7 @@ tcl::namespace::eval textblock { } } block1 { + #box drawing as far as we can go without extra blocks from legacy computing unicode block - which is unfortunately not commonly supported set hlt \u2581 ;# lower one eighth block set hlb \u2594 ;# upper one eighth block set vll \u258f ;# left one eighth block @@ -6002,17 +6244,19 @@ tcl::namespace::eval textblock { set vlrj $vlr } - blockxx { + block2 { + #the resultant table will have text appear towards top of each box + #with 'legacy' computing unicode block - hopefully these become more widely supported - as they are useful and fill in some gaps set hlt \u2594 ;# upper one eighth block set hlb \u2581 ;# lower one eighth block - set vll \u2595 ;# right one eighth block - set vlr \u258f ;# left one eighth block + set vlr \u2595 ;# right one eighth block + set vll \u258f ;# left one eighth block - set tlc \u2595 ;# right one eighth block - set trc \u258f ;# left one eighth block + set tlc \U1fb7d ;#legacy block + set trc \U1fb7e ;#legacy block - set blc \u2595 ;# right one eighth block - set brc \u258f ;# left one eighth block + set blc \U1fb7c ;#legacy block + set brc \U1fb7f ;#legacy block #horizontal and vertical bar joins set hltj $hlt @@ -6039,36 +6283,36 @@ tcl::namespace::eval textblock { set vlrj $vlr } default { - set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] - set custom_frame [tcl::dict::merge $default_custom $f] - tcl::dict::with custom_frame {} ;#extract keys as vars - - if {[tcl::dict::exists $custom_frame hlt]} { - set hlt [tcl::dict::get $custom_frame hlt] - } else { - set hlt $hl + set default_custom [tcl::dict::create hl " " vl " " tlc " " trc " " blc " " brc " "] ;#only default the general types - these form defaults for more specific types if they're missing + if {[llength $f] % 2 != 0} { + #todo - retrieve usage from punk::args + error "textblock::frametype frametype '$f' is not one of the predefined frametypes: $textblock::frametypes and does not appear to be a dictionary for a custom frametype" } - if {[tcl::dict::exists $custom_frame hlb]} { - set hlb [tcl::dict::get $custom_frame hlb] - } else { - set hlb $hl - } - - if {[tcl::dict::exists $custom_frame vll]} { - set vll [tcl::dict::get $custom_frame vll] - } else { - set vll $vl + #unknown order of keys specified by user - validate before creating vars as we need more general elements to be available as defaults + dict for {k v} $f { + switch -- $k { + hl - vl - tlc - trc - blc - brc - hlt - hlb - vll - vlr - hltj - hlbj - vllj - vlrj {} + default { + error "textblock::frametype '$f' has unknown element '$k'" + } + } } - if {[tcl::dict::exists $custom_frame vlr]} { - set vlr [tcl::dict::get $custom_frame vlr] - } else { - set vlr $vl + #verified keys - safe to extract as vars + set custom_frame [tcl::dict::merge $default_custom $f] + tcl::dict::with custom_frame {} ;#extract keys as vars + #longer j vars must be after their more specific counterparts in the list being processed by foreach + foreach t {hlt hlb vll vlr hltj hlbj vllj vlrj} { + if {[tcl::dict::exists $custom_frame $t]} { + set $t [tcl::dict::get $custom_frame $t] + } else { + #set more explicit type to it's more general counterpart if it's missing + #e.g hlt -> hl + #e.g hltj -> hlt + set $t [set [string range $t 0 end-1]] + } } - #horizontal and vertical bar joins - set hltj $hlt - set hlbj $hlb - set vllj $vll - set vlrj $vlr + #assert vars hl vl tlc trc blc brc hlt hlb vll vlr hltj hlbj vllj vlrj are all set + #horizontal and vertical bar joins - key/variable ends with 'j' } } if {$boxonly} { @@ -6096,33 +6340,44 @@ tcl::namespace::eval textblock { variable frame_cache set frame_cache [tcl::dict::create] - proc frame_cache {{action ""}} { + proc frame_cache {args} { + set argd [punk::args::get_dict { + -action -default "" -choices {clear} -help "Clear the textblock::frame_cache dictionary" + -pretty -default 1 -help "Use 'pdict textblock::frame_cache */*' for prettier output" + *values -min 0 -max 0 + } $args] + set action [dict get $argd opts -action] + if {$action ni [list clear ""]} { error "frame_cache action '$action' not understood. Valid actions: clear" } variable frame_cache - set out "" - if {[catch { - set termwidth [tcl::dict::get [punk::console::get_size] columns] - }]} { - set termwidth 80 - } + if {[dict get $argd opts -pretty]} { + set out [pdict -chan none frame_cache */*] + } else { + set out "" + if {[catch { + set termwidth [tcl::dict::get [punk::console::get_size] columns] + }]} { + set termwidth 80 + } - tcl::dict::for {k v} $frame_cache { - lassign $v _f frame _used used - set fwidth [textblock::widthtopline $frame] - #review - are cached frames uniform width lines? - #set fwidth [textblock::width $frame] - set frameinfo "$k used:$used " - set allinone_width [expr {[tcl::string::length $frameinfo] + $fwidth}] - if {$allinone_width >= $termwidth} { - #split across 2 lines - append out "$frameinfo\n" - append out $frame \n - } else { - append out [textblock::join -- $frameinfo $frame]\n + tcl::dict::for {k v} $frame_cache { + lassign $v _f frame _used used + set fwidth [textblock::widthtopline $frame] + #review - are cached frames uniform width lines? + #set fwidth [textblock::width $frame] + set frameinfo "$k used:$used " + set allinone_width [expr {[tcl::string::length $frameinfo] + $fwidth}] + if {$allinone_width >= $termwidth} { + #split across 2 lines + append out "$frameinfo\n" + append out $frame \n + } else { + append out [textblock::join -- $frameinfo $frame]\n + } + append out \n ;# frames used to build tables often have joins - keep a line in between for clarity } - append out \n ;# frames used to build tables often have joins - keep a line in between for clarity } if {$action eq "clear"} { set frame_cache [tcl::dict::create] @@ -6270,7 +6525,7 @@ tcl::namespace::eval textblock { } } switch -- $target { - "" - light - heavy - ascii - altg - arc - double - custom - block - block1 {} + "" - light - heavy - ascii - altg - arc - double - custom - block - block1 - block2 {} default { set is_joins_ok 0 break @@ -6473,7 +6728,7 @@ tcl::namespace::eval textblock { set vll_width 1 ;#default for all except custom (printing width) set vlr_width 1 - set framedef [textblock::framedef $framedef -joins $opt_joins] + set framedef [textblock::framedef -joins $opt_joins $framedef] tcl::dict::with framedef {} ;#extract vll,hlt,tlc etc vars #puts "---> $opt_boxmap" @@ -6846,15 +7101,24 @@ tcl::namespace::eval textblock { return $fs } } - proc gcross {{size 1} args} { + proc gcross {args} { + set argd [punk::args::get_dict { + -max_cross_size -default 0 -type integer -help "Largest size cross to use to make up the block + Only cross sizes that divide the size of the overall block will be used. + e.g if the 'size' chosen is 19 (a prime number) - only 1 or the full size of 19 can be used as the crosses to make up the block. + Whereas for a block size of 24, -max_cross_size of 1,2,3,4,6,8,12 or 24 will work. (all divisors) + If a number chosen for -max_cross_size isn't a divisor, the largest divisor below the chosen value will be used. + " + *values -min 1 + size -default 1 -type integer + } $args] + set size [dict get $argd values size] + set opts [dict get $argd opts] + if {$size == 0} { return "" } - set defaults [list\ - -max_cross_size 0 - ] - set opts [tcl::dict::merge $defaults $args] set opt_max_cross_size [tcl::dict::get $opts -max_cross_size] #set fit_size [punk::lib::greatestOddFactor $size] @@ -6932,14 +7196,14 @@ tcl::namespace::eval textblock { #Test we can join two coloured blocks proc test_colour {} { - set b1 [a= red]1\n2\n3[a=] - set b2 [a= green]a\nb\nc[a=] - set result [textblock::join $b1 $b2] + set b1 [a red]1\n2\n3[a] + set b2 [a green]a\nb\nc[a] + set result [textblock::join -- $b1 $b2] puts $result #return [list $b1 $b2 $result] return [ansistring VIEW $result] } - tcl::namespace::import ::punk::ansi::stripansi + tcl::namespace::import ::punk::ansi::ansistrip } diff --git a/src/bootsupport/modules_tcl8/include_modules.config b/src/bootsupport/modules_tcl8/include_modules.config new file mode 100644 index 00000000..05ce61ae --- /dev/null +++ b/src/bootsupport/modules_tcl8/include_modules.config @@ -0,0 +1,9 @@ + +#bootsupport modules can be pulled in from within other areas of src or from the built module folders of the project +#They must be already built, so generally shouldn't come directly from src/modules. + +#each entry - base module +set bootsupport_modules [list\ + modules_tcl8 thread\ +] + diff --git a/src/make.tcl b/src/make.tcl index f4eef65f..e25031ed 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -13,7 +13,7 @@ namespace eval ::punkmake { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /?] - variable known_commands [list project get-project-info shell bootsupport] + variable known_commands [list project get-project-info shell vendorupdate bootsupport] } if {"::try" ni [info commands ::try]} { puts stderr "Tcl interpreter possibly too old - 'try' command not found - aborting" @@ -134,6 +134,8 @@ proc punkmake_gethelp {args} { append h " - the optional -k flag will terminate processes running as the executable being built (if applicable)" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n \n + append h " $scriptname vendorupdate" \n + append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n append h " $scriptname get-project-info" \n append h " - show the name and base folder of the project to be built" \n append h "" \n @@ -251,124 +253,225 @@ if {$::punkmake::command eq "shell"} { exit 1 } -if {$::punkmake::command eq "bootsupport"} { +if {$::punkmake::command eq "vendorupdate"} { puts "projectroot: $projectroot" puts "script: [info script]" #puts "-- [tcl::tm::list] --" - puts stdout "Updating bootsupport from local files" - - proc bootsupport_localupdate {projectroot} { - set bootsupport_modules [list] - set bootsupport_module_folders [list] - set bootsupport_config $projectroot/src/bootsupport/include_modules.config ;# - if {[file exists $bootsupport_config]} { - set targetroot $projectroot/src/bootsupport/modules - source $bootsupport_config ;#populate $bootsupport_modules with project-specific list - if {![llength $bootsupport_modules]} { - puts stderr "No local bootsupport modules configured for updating" - } else { - - if {[catch { - #---------- - set boot_installer [punkcheck::installtrack new make.tcl $projectroot/src/bootsupport/.punkcheck] - $boot_installer set_source_target $projectroot $projectroot/src/bootsupport - set boot_event [$boot_installer start_event {-make_step bootsupport}] - #---------- - } errM]} { - puts stderr "Unable to use punkcheck for bootsupport error: $errM" - set boot_event "" + puts stdout "Updating vendor modules in src folder" + + proc vendor_localupdate {projectroot} { + set local_modules [list] + set git_modules [list] + set fossil_modules [list] + set sourcefolder $projectroot/src + #todo vendor/lib + set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] + + set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] + lappend vendormodulefolders vendormodules + foreach vf $vendormodulefolders { + if {[file exists $sourcefolder/$vf]} { + lassign [split $vf _] _vm tclx + if {$tclx ne ""} { + set which _$tclx + } else { + set which "" } - foreach {relpath module} $bootsupport_modules { - set module [string trim $module :] - set module_subpath [string map [list :: /] [namespace qualifiers $module]] - set srclocation [file join $projectroot $relpath $module_subpath] - #puts stdout "$relpath $module $module_subpath $srclocation" - set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] - #lsort won't sort version numbers properly e.g with -dictionary 0.1.1 comes before 0.1 - if {![llength $pkgmatches]} { - puts stderr "Missing source for bootsupport module $module - not found in $srclocation" - continue - } - set latestfile [lindex $pkgmatches 0] - set latestver [lindex [split [file rootname $latestfile] -] 1] - foreach m $pkgmatches { - lassign [split [file rootname $m] -] _pkg ver - #puts "comparing $ver vs $latestver" - if {[package vcompare $ver $latestver] == 1} { - set latestver $ver - set latestfile $m - } - } - set srcfile [file join $srclocation $latestfile] - set tgtfile [file join $targetroot $module_subpath $latestfile] - if {$boot_event ne ""} { - #---------- - $boot_event targetset_init INSTALL $tgtfile - $boot_event targetset_addsource $srcfile - #---------- - if {\ - [llength [dict get [$boot_event targetset_source_changes] changed]]\ - || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ - } { - file mkdir [file dirname $tgtfile] ;#ensure containing folder for target exists - $boot_event targetset_started - # -- --- --- --- --- --- - puts "BOOTSUPPORT update: $srcfile -> $tgtfile" - if {[catch { - file copy -force $srcfile $tgtfile - } errM]} { - $boot_event targetset_end FAILED + set vendor_config $sourcefolder/vendormodules$which/include_modules.config + if {[file exists $vendor_config]} { + set targetroot $sourcefolder/vendormodules$which + source $vendor_config ;#populate $local_modules $git_modules $fossil_modules with project-specific list + if {![llength $local_modules]} { + puts stderr "src/vendormodules$which No local vendor modules configured for updating (config file: $vendor_config)" + } else { + if {[catch { + #---------- + set vendor_installer [punkcheck::installtrack new make.tcl $sourcefolder/vendormodules$which/.punkcheck] + $vendor_installer set_source_target $projectroot $sourcefolder/vendormodules$which + set installation_event [$vendor_installer start_event {-make_step vendorupdate}] + #---------- + } errM]} { + puts stderr "Unable to use punkcheck for vendormodules$which update. Error: $errM" + set installation_event "" + } + foreach {relpath module} $local_modules { + set module [string trim $module :] + set module_subpath [string map {:: /} [namespace qualifiers $module]] + set srclocation [file join $projectroot $relpath $module_subpath] + #puts stdout "$relpath $module $module_subpath $srclocation" + set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] + #lsort won't sort version numbers properly e.g with -dictionary 0.1.1 comes before 0.1 + if {![llength $pkgmatches]} { + puts stderr "Missing source for vendor module $module - not found in $srclocation" + continue + } + set latestfile [lindex $pkgmatches 0] + set latestver [lindex [split [file rootname $latestfile] -] 1] + foreach m $pkgmatches { + lassign [split [file rootname $m] -] _pkg ver + #puts "comparing $ver vs $latestver" + if {[package vcompare $ver $latestver] == 1} { + set latestver $ver + set latestfile $m + } + } + set srcfile [file join $srclocation $latestfile] + set tgtfile [file join $targetroot $module_subpath $latestfile] + if {$installation_event ne ""} { + #---------- + $installation_event targetset_init INSTALL $tgtfile + $installation_event targetset_addsource $srcfile + #---------- + if {\ + [llength [dict get [$installation_event targetset_source_changes] changed]]\ + || [llength [$installation_event get_targets_exist]] < [llength [$installation_event get_targets]]\ + } { + file mkdir [file dirname $tgtfile] ;#ensure containing folder for target exists + $installation_event targetset_started + # -- --- --- --- --- --- + puts "VENDORMODULES$which update: $srcfile -> $tgtfile" + if {[catch { + file copy -force $srcfile $tgtfile + } errM]} { + $installation_event targetset_end FAILED + } else { + $installation_event targetset_end OK + } + # -- --- --- --- --- --- + } else { + puts -nonewline stderr "." + $installation_event targetset_end SKIPPED + } + $installation_event end } else { - $boot_event targetset_end OK + file copy -force $srcfile $tgtfile } - # -- --- --- --- --- --- - } else { - puts -nonewline stderr "." - $boot_event targetset_end SKIPPED } - $boot_event end - } else { - file copy -force $srcfile $tgtfile + } - } - if {$boot_event ne ""} { - puts \n - $boot_event destroy - $boot_installer destroy + } else { + puts stderr "No config at $vendor_config - nothing configured to update" } } + } + } - if {[llength $bootsupport_module_folders] % 2 != 0} { - #todo - change include_modules.config structure to be line based? we have no way of verifying paired entries because we accept a flat list - puts stderr "WARNING - Skipping bootsupport_module_folders - list should be a list of base subpath pairs" - } else { - foreach {base subfolder} $bootsupport_module_folders { - #user should be careful not to include recursive/cyclic structures e.g module that has a folder which contains other modules from this project - #It will probably work somewhat.. but may make updates confusing.. or worse - start making deeper and deeper copies - set src [file join $projectroot $base $subfolder] - if {![file isdirectory $src]} { - puts stderr "bootsupport folder not found: $src" - continue - } + vendor_localupdate $projectroot + + puts stdout " vendor package update done " + flush stderr + flush stdout + ::exit 0 +} - #subfolder is the common relative path - so don't include the base in the target path - set tgt [file join $targetroot $subfolder] - file mkdir $tgt +if {$::punkmake::command eq "bootsupport"} { + puts "projectroot: $projectroot" + puts "script: [info script]" + #puts "-- [tcl::tm::list] --" + puts stdout "Updating bootsupport from local files" - puts stdout "BOOTSUPPORT non_tm_files $src - copying to $tgt (if source file changed)" - set overwrite "installedsourcechanged-targets" - set resultdict [punkcheck::install_non_tm_files $src $tgt -installer make.tcl -overwrite $overwrite -punkcheck_folder $projectroot/src/bootsupport] - puts stdout [punkcheck::summarize_install_resultdict $resultdict] + proc bootsupport_localupdate {projectroot} { + set bootsupport_modules [list] ;#variable populated by include_modules.config file - review + + set bootmodulefolders [glob -nocomplain -dir $sourcefolder/bootsupport -type d -tails modules_tcl*] + lappend bootmodulefolder modules + foreach bm $bootmodulefolders { + if {[file exists $sourcefolder/bootsupport/$bm]} { + lassign [split $bm _] _bm tclx + if {$tclx ne ""} { + set which _$tclx + } else { + set which "" } - } + set bootsupport_config $projectroot/src/bootsupport/modules$which/include_modules.config ;# + if {[file exists $bootsupport_config]} { + set targetroot $projectroot/src/bootsupport/modules$which + source $bootsupport_config ;#populate $bootsupport_modules with project-specific list + if {![llength $bootsupport_modules]} { + puts stderr "bootsupport/modules$which - No local bootsupport modules configured for updating" + } else { + if {[catch { + #---------- + set boot_installer [punkcheck::installtrack new make.tcl $projectroot/src/bootsupport/.punkcheck] + $boot_installer set_source_target $projectroot $projectroot/src/bootsupport + set boot_event [$boot_installer start_event {-make_step bootsupport}] + #---------- + } errM]} { + puts stderr "Unable to use punkcheck for bootsupport error: $errM" + set boot_event "" + } + + foreach {relpath module} $bootsupport_modules { + set module [string trim $module :] + set module_subpath [string map [list :: /] [namespace qualifiers $module]] + set srclocation [file join $projectroot $relpath $module_subpath] + #puts stdout "$relpath $module $module_subpath $srclocation" + set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] + #lsort won't sort version numbers properly e.g with -dictionary 0.1.1 comes before 0.1 + if {![llength $pkgmatches]} { + puts stderr "Missing source for bootsupport module $module - not found in $srclocation" + continue + } + set latestfile [lindex $pkgmatches 0] + set latestver [lindex [split [file rootname $latestfile] -] 1] + foreach m $pkgmatches { + lassign [split [file rootname $m] -] _pkg ver + #puts "comparing $ver vs $latestver" + if {[package vcompare $ver $latestver] == 1} { + set latestver $ver + set latestfile $m + } + } + set srcfile [file join $srclocation $latestfile] + set tgtfile [file join $targetroot $module_subpath $latestfile] + if {$boot_event ne ""} { + #---------- + $boot_event targetset_init INSTALL $tgtfile + $boot_event targetset_addsource $srcfile + #---------- + if {\ + [llength [dict get [$boot_event targetset_source_changes] changed]]\ + || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ + } { + file mkdir [file dirname $tgtfile] ;#ensure containing folder for target exists + $boot_event targetset_started + # -- --- --- --- --- --- + puts "BOOTSUPPORT module$which update: $srcfile -> $tgtfile" + if {[catch { + file copy -force $srcfile $tgtfile + } errM]} { + $boot_event targetset_end FAILED + } else { + $boot_event targetset_end OK + } + # -- --- --- --- --- --- + } else { + puts -nonewline stderr "." + $boot_event targetset_end SKIPPED + } + $boot_event end + } else { + file copy -force $srcfile $tgtfile + } + } + if {$boot_event ne ""} { + puts \n + $boot_event destroy + $boot_installer destroy + } + } + + } + } } } bootsupport_localupdate $projectroot - #/modules/punk/mix/templates/layouts only applies if the project has it's own copy of the punk/mix modules. Generally this should only apply to the punkshell project itself. + #if this project has custom project layouts, and there is a bootsupport folder - update their bootsupport + set layout_bases [list\ $sourcefolder/project_layouts/custom/_project\ ] @@ -381,14 +484,26 @@ if {$::punkmake::command eq "bootsupport"} { set antipaths [list\ README.md\ ] - set sourcemodules $projectroot/src/bootsupport/modules - set targetroot [file join $project_layout_base $layoutname/src/bootsupport/modules] - file mkdir $targetroot - - puts stdout "BOOTSUPPORT layouts/$layoutname: copying from $sourcemodules to $targetroot (if source file changed)" - set resultdict [punkcheck::install $sourcemodules $targetroot -overwrite installedsourcechanged-targets -antiglob_paths $antipaths] - puts stdout [punkcheck::summarize_install_resultdict $resultdict] - flush stdout + set boot_module_folders [glob -nocomplain -dir $projectroot/src/bootsupport/modules_tcl*] + lappend bootsupport_module_folders "modules" + foreach bm $bootsupport_module_folders { + if {[file exists $projectroot/src/bootsupport/$bm]} { + lassign [split $bm _] _bm tclx + if {$tclx ne ""} { + set which _$tclx + } else { + set which "" + } + set sourcemodules $projectroot/src/bootsupport/modules$which + set targetroot [file join $project_layout_base $layoutname/src/bootsupport/modules$which] + file mkdir $targetroot + + puts stdout "BOOTSUPPORT$which layouts/$layoutname: copying from $sourcemodules to $targetroot (if source file changed)" + set resultdict [punkcheck::install $sourcemodules $targetroot -overwrite installedsourcechanged-targets -antiglob_paths $antipaths] + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + flush stdout + } + } } } } else { @@ -412,39 +527,66 @@ if {$::punkmake::command ne "project"} { exit 1 } -file mkdir $projectroot/lib ;#needs to exist - -#only a single consolidated /modules folder used for target -set target_modules_base $projectroot/modules -file mkdir $target_modules_base #external libs and modules first - and any supporting files - no 'building' required -if {[file exists $sourcefolder/vendorlib]} { - #exclude README.md from source folder - but only the root one - #-antiglob_paths takes relative patterns e.g - # */test.txt will only match test.txt exactly one level deep. - # */*/*.foo will match any path ending in .foo that is exactly 2 levels deep. - # **/test.txt will match at any level below the root (but not in the root) - set antipaths [list\ - README.md\ - ] - puts stdout "VENDORLIB: copying from $sourcefolder/vendorlib to $projectroot/lib (if source file changed)" - set resultdict [punkcheck::install $sourcefolder/vendorlib $projectroot/lib -overwrite installedsourcechanged-targets -antiglob_paths $antipaths] - puts stdout [punkcheck::summarize_install_resultdict $resultdict] +#install src vendor contents (from version controlled src folder) to base of project (same target folders as our own src/modules etc ie to paths that go on the auto_path and in tcl::tm::list) -} else { - puts stderr "VENDORLIB: No src/vendorlib folder found." +set vendorlibfolders [glob -nocomplain -dir $sourcefolder -type d -tails vendorlib_tcl*] +lappend vendorlibfolders vendorlib + +foreach lf $vendorlibfolders { + if {[file exists $sourcefolder/$lf]} { + lassign [split $lf _] _vm tclx + if {$tclx ne ""} { + set which _$tclx + } else { + set which "" + } + set target_lib_folder $projectroot/lib$which + file mkdir $projectroot/lib$which + + #exclude README.md from source folder - but only the root one + #-antiglob_paths takes relative patterns e.g + # */test.txt will only match test.txt exactly one level deep. + # */*/*.foo will match any path ending in .foo that is exactly 2 levels deep. + # **/test.txt will match at any level below the root (but not in the root) + set antipaths [list\ + README.md\ + ] + + puts stdout "VENDORLIB$which: copying from $sourcefolder/$lf to $target_lib_folder (if source file changed)" + set resultdict [punkcheck::install $sourcefolder/$lf $target_lib_folder -overwrite installedsourcechanged-targets -antiglob_paths $antipaths] + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + } +} +if {![llength $vendorlibfolders]} { + puts stderr "VENDORLIB: No src/vendorlib or src/vendorlib_tcl* folder found." } -if {[file exists $sourcefolder/vendormodules]} { - #install .tm *and other files* - puts stdout "VENDORMODULES: copying from $sourcefolder/vendormodules to $target_modules_base (if source file changed)" - set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md}] - puts stdout [punkcheck::summarize_install_resultdict $resultdict] -} else { - puts stderr "VENDORMODULES: No src/vendormodules folder found." +set vendormodulefolders [glob -nocomplain -dir $sourcefolder -type d -tails vendormodules_tcl*] +lappend vendormodulefolders vendormodules + +foreach vf $vendormodulefolders { + if {[file exists $sourcefolder/$vf]} { + lassign [split $vf _] _vm tclx + if {$tclx ne ""} { + set which _$tclx + } else { + set which "" + } + set target_module_folder $projectroot/modules$which + file mkdir $target_module_folder + + #install .tm *and other files* + puts stdout "VENDORMODULES$which: copying from $sourcefolder/$vf to $target_module_folder (if source file changed)" + set resultdict [punkcheck::install $sourcefolder/$vf $target_module_folder -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md include_modules.config}] + puts stdout [punkcheck::summarize_install_resultdict $resultdict] + } +} +if {![llength $vendormodulefolders]} { + puts stderr "VENDORMODULES: No src/vendormodules or src/vendormodules_tcl* folders found." } ######################################################## @@ -516,11 +658,22 @@ foreach layoutbase $layout_bases { } ######################################################## +#consolidated /modules /modules_tclX folder used for target where X is tcl major version +#the make process will process for any _tclX not just the major version of the current interpreter -#default source module folder is at projectroot/src/modules +#default source module folders are at projectroot/src/modules and projectroot/src/modules_tclX (where X is tcl major version) #There may be multiple other src module folders at same level (e.g folder not being other special-purpose folder and not matching name vendor* that contains at least one .tm file in its root) set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] +puts stdout "SOURCEMODULES: scanning [llength $source_module_folderlist] folders" foreach src_module_dir $source_module_folderlist { + set mtail [file tail $src_module_dir] + if {[string match "modules_tcl*" $mtail]} { + set target_modules_base $projectroot/$mtail + } else { + set target_modules_base $projectroot/modules + } + file mkdir $target_modules_base + puts stderr "Processing source module dir: $src_module_dir" set dirtail [file tail $src_module_dir] #modules and associated files belonging to this package/app diff --git a/src/modules/#modpod-modpodtest-999999.0a1.0/#modpod-loadscript.tcl b/src/modules/#modpod-modpodtest-999999.0a1.0/#modpod-loadscript.tcl new file mode 100644 index 00000000..9487b6e6 --- /dev/null +++ b/src/modules/#modpod-modpodtest-999999.0a1.0/#modpod-loadscript.tcl @@ -0,0 +1,53 @@ +apply {code { + set scriptpath [file normalize [info script]] + if {[string match "#modpod-loadscript*.tcl" [file tail $scriptpath]]} { + #jump up an extra dir level if we are within a #modpod-loadscript file. + set mypath [file dirname [file dirname $scriptpath]] + #expect to be in folder #modpod-- + #Now we need to test if we are in a mounted folder vs an extracted folder + set container [file dirname $mypath] + if {[string match "#mounted-modpod-*" $container]} { + set mypath [file dirname $container] + } + set modver [string range [file tail [file dirname $scriptpath]] 8 end] ;# the containing folder is named #modpod-- + } else { + set mypath [file dirname $scriptpath] + set modver [file root [file tail [info script]]] + } + set mysegs [file split $mypath] + set overhang [list] + foreach libpath [tcl::tm::list] { + set libsegs [file split $libpath] ;#split and rejoin with '/' because sometimes module paths may have mixed \ & / + if {[file join $mysegs /] eq [file join [lrange $libsegs 0 [llength $mysegs]] /]} { + #mypath is below libpath + set overhang [lrange $mysegs [llength $libsegs]+1 end] + break + } + } + lassign [split $modver -] moduletail version + set ns [join [concat $overhang $moduletail] ::] + #if {![catch {package require modpod}]} { + # ::modpod::disconnect [info script] + #} + package provide $ns $version + namespace eval $ns $code +} ::} { + # + # Module procs here, where current namespace is that of the module. + # Package version can, if needed, be accessed as [uplevel 1 {set version}] + # Last element of module name: [uplevel 1 {set moduletail}] + # Full module name: [uplevel 1 {set ns}] + + # + # + # + + # + # + # + + # + # + # + +} diff --git a/src/modules/#modpod-modpodtest-999999.0a1.0/#z b/src/modules/#modpod-modpodtest-999999.0a1.0/#z new file mode 100644 index 00000000..a8f7b05a --- /dev/null +++ b/src/modules/#modpod-modpodtest-999999.0a1.0/#z @@ -0,0 +1,2 @@ +#Do not remove the trailing ctrl-z character from this file + \ No newline at end of file diff --git a/src/modules/#modpod-modpodtest-999999.0a1.0/modpodtest-999999.0a1.0.tm b/src/modules/#modpod-modpodtest-999999.0a1.0/modpodtest-999999.0a1.0.tm new file mode 100644 index 00000000..b10d2cb9 --- /dev/null +++ b/src/modules/#modpod-modpodtest-999999.0a1.0/modpodtest-999999.0a1.0.tm @@ -0,0 +1,181 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2024 +# +# @@ Meta Begin +# Application modpodtest 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_modpodtest 0 999999.0a1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpodtest] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpodtest +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpodtest +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval modpodtest::class { + #*** !doctools + #[subsection {Namespace modpodtest::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval modpodtest { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + #variable xyz + + #*** !doctools + #[subsection {Namespace modpodtest}] + #[para] Core API functions for modpodtest + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpodtest ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval modpodtest::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace modpodtest::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpodtest::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +#tcl::namespace::eval modpodtest::system { + #*** !doctools + #[subsection {Namespace modpodtest::system}] + #[para] Internal functions that are not part of the API + + + +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpodtest [tcl::namespace::eval modpodtest { + variable pkg modpodtest + variable version + set version 999999.0a1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/modules/#modpod-zipper-0.11/zipper-0.11.tm b/src/modules/#modpod-zipper-0.11/zipper-0.11.tm new file mode 100644 index 00000000..2e9b8baa --- /dev/null +++ b/src/modules/#modpod-zipper-0.11/zipper-0.11.tm @@ -0,0 +1,120 @@ +# ZIP file constructor + +package provide zipper 0.11 + +namespace eval zipper { + namespace export initialize addentry finalize + + namespace eval v { + variable fd + variable base + variable toc + } + + proc initialize {fd} { + set v::fd $fd + set v::base [tell $fd] + set v::toc {} + fconfigure $fd -translation binary -encoding binary + } + + proc emit {s} { + puts -nonewline $v::fd $s + } + + proc dostime {sec} { + set f [clock format $sec -format {%Y %m %d %H %M %S} -gmt 1] + regsub -all { 0(\d)} $f { \1} f + foreach {Y M D h m s} $f break + set date [expr {(($Y-1980)<<9) | ($M<<5) | $D}] + set time [expr {($h<<11) | ($m<<5) | ($s>>1)}] + return [list $date $time] + } + + proc addentry {name contents {date ""} {force 0}} { + if {$date == ""} { set date [clock seconds] } + foreach {date time} [dostime $date] break + set flag 0 + set type 0 ;# stored + set fsize [string length $contents] + set csize $fsize + set fnlen [string length $name] + + if {$force > 0 && $force != [string length $contents]} { + set csize $fsize + set fsize $force + set type 8 ;# if we're passing in compressed data, it's deflated + } + + if {[catch { zlib crc32 $contents } crc]} { + set crc 0 + } elseif {$type == 0} { + set cdata [zlib deflate $contents] + if {[string length $cdata] < [string length $contents]} { + set contents $cdata + set csize [string length $cdata] + set type 8 ;# deflate + } + } + + lappend v::toc "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} \ + $flag $type $time $date $crc $csize $fsize $fnlen \ + {0 0 0 0} 128 [tell $v::fd]]$name" + + emit [binary format a2c4ssssiiiss PK {3 4 20 0} \ + $flag $type $time $date $crc $csize $fsize $fnlen 0] + emit $name + emit $contents + } + + proc finalize {} { + set pos [tell $v::fd] + + set ntoc [llength $v::toc] + foreach x $v::toc { emit $x } + set v::toc {} + + set len [expr {[tell $v::fd] - $pos}] + incr pos -$v::base + + emit [binary format a2c2ssssiis PK {5 6} 0 0 $ntoc $ntoc $len $pos 0] + + return $v::fd + } +} + +if {[info exists pkgtest] && $pkgtest} { + puts "no test code" +} + +# test code below runs when this is launched as the main script +if {[info exists argv0] && [string match zipper-* [file tail $argv0]]} { + + catch { package require zlib } + + zipper::initialize [open try.zip w] + + set dirs [list .] + while {[llength $dirs] > 0} { + set d [lindex $dirs 0] + set dirs [lrange $dirs 1 end] + foreach f [lsort [glob -nocomplain [file join $d *]]] { + if {[file isfile $f]} { + regsub {^\./} $f {} f + set fd [open $f] + fconfigure $fd -translation binary -encoding binary + zipper::addentry $f [read $fd] [file mtime $f] + close $fd + } elseif {[file isdir $f]} { + lappend dirs $f + } + } + } + + close [zipper::finalize] + + puts "size = [file size try.zip]" + puts [exec unzip -v try.zip] + + file delete try.zip +} diff --git a/src/modules/#modpod-zipper-0.11/zipper.README b/src/modules/#modpod-zipper-0.11/zipper.README new file mode 100644 index 00000000..9af16f04 --- /dev/null +++ b/src/modules/#modpod-zipper-0.11/zipper.README @@ -0,0 +1,28 @@ +Creating ZIP archives in Tcl +============================ + + Rev 0.11: Added ?force? arg to bypass re-compression + Rev 0.10: Initial release + + +Zipper is a package to create ZIP archives with a few simple commands: + + zipper::initialize $fd + initialize things to start writing zip file entries + + zipper::addentry name contents ?date? ?force? + add one entry, modification date defaults to [clock seconds] + + zipper::finalize + write trailing table of contents, returns file descriptor + +Example: + + package require zipper + zipper::initialize [open try.zip w] + zipper::addentry dir/file.txt "some data to store" + close [zipper::finalize] + +If the "zlib" package is available, it will be used to to compress the +data when possible and to calculate proper CRC-32 checksums. Otherwise, +the output file will contain uncompressed data and zero checksums. diff --git a/src/modules/modpodtest-buildversion.txt b/src/modules/modpodtest-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/modpodtest-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/oolib-0.1.2.tm b/src/modules/oolib-0.1.2.tm index af5da523..858c61cd 100644 --- a/src/modules/oolib-0.1.2.tm +++ b/src/modules/oolib-0.1.2.tm @@ -1,201 +1,201 @@ -#JMN - api should be kept in sync with package patternlib where possible -# -package provide oolib [namespace eval oolib { - variable version - set version 0.1.2 -}] - -namespace eval oolib { - oo::class create collection { - variable o_data ;#dict - #variable o_alias - constructor {} { - set o_data [dict create] - } - method info {} { - return [dict info $o_data] - } - method count {} { - return [dict size $o_data] - } - method isEmpty {} { - expr {[dict size $o_data] == 0} - } - method names {{globOrIdx {}}} { - if {[llength $globOrIdx]} { - if {[string is integer -strict $globOrIdx]} { - set idx $globOrIdx - if {$idx < 0} { - set idx "end-[expr {abs($idx + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $idx} result]} { - error "[self object] no such index : '$idx'" - } else { - return $result - } - } else { - #glob - return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] - } - } else { - return [dict keys $o_data] - } - } - #like names but without globbing - method keys {} { - dict keys $o_data - } - method key {{posn 0}} { - if {$posn < 0} { - set posn "end-[expr {abs($posn + 1)}]" - } - if {[catch {lindex [dict keys $o_data] $posn} result]} { - error "[self object] no such index : '$posn'" - } else { - return $result - } - } - method hasKey {key} { - dict exists $o_data $key - } - method get {} { - return $o_data - } - method items {} { - return [dict values $o_data] - } - method item {key} { - if {[string is integer -strict $key]} { - if {$key >= 0} { - set valposn [expr {(2*$key) +1}] - return [lindex $o_data $valposn] - } else { - set key "end-[expr {abs($key + 1)}]" - return [lindex $o_data $key] - #return [lindex [dict keys $o_data] $key] - } - } - if {[dict exists $o_data $key]} { - return [dict get $o_data $key] - } - } - #inverse lookup - method itemKeys {value} { - set value_indices [lsearch -all [dict values $o_data] $value] - set keylist [list] - foreach i $value_indices { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - method search {value args} { - set matches [lsearch {*}$args [dict values $o_data] $value] - if {"-inline" in $args} { - return $matches - } else { - set keylist [list] - foreach i $matches { - set idx [expr {(($i + 1) *2) -2}] - lappend keylist [lindex $o_data $idx] - } - return $keylist - } - } - #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? - #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well? - #method alias {newAlias existingKeyOrAlias} { - # if {[string is integer -strict $newAlias]} { - # error "[self object] collection key alias cannot be integer" - # } - # if {[string length $existingKeyOrAlias]} { - # set o_alias($newAlias) $existingKeyOrAlias - # } else { - # unset o_alias($newAlias) - # } - #} - #method aliases {{key ""}} { - # if {[string length $key]} { - # set result [list] - # foreach {n v} [array get o_alias] { - # if {$v eq $key} { - # lappend result $n $v - # } - # } - # return $result - # } else { - # return [array get o_alias] - # } - #} - ##if the supplied index is an alias, return the underlying key; else return the index supplied. - #method realKey {idx} { - # if {[catch {set o_alias($idx)} key]} { - # return $idx - # } else { - # return $key - # } - #} - method add {value key} { - if {[string is integer -strict $key]} { - error "[self object] collection key must not be an integer. Use another structure if integer keys required" - } - if {[dict exists $o_data $key]} { - error "[self object] col_processors object error: key '$key' already exists in collection" - } - dict set o_data $key $value - return [expr {[dict size $o_data] - 1}] ;#return index of item - } - method remove {idx {endRange ""}} { - if {[string length $endRange]} { - error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" - } - if {[string is integer -strict $idx]} { - if {$idx < 0} { - set idx "end-[expr {abs($idx+1)}]" - } - set key [lindex [dict keys $o_data] $idx] - set posn $idx - } else { - set key $idx - set posn [lsearch -exact [dict keys $o_data] $key] - if {$posn < 0} { - error "[self object] no such index: '$idx' in this collection" - } - } - dict unset o_data $key - return - } - method clear {} { - set o_data [dict create] - return - } - method reverse_the_collection {} { - #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs - #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy. - #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations. - set dictnew [dict create] - foreach k [lreverse [dict keys $o_data]] { - dict set dictnew $k [dict get $o_data $k] - } - set o_data $dictnew - return - } - #review - cmd as list vs cmd as script? - method map {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list {*}$cmd $v]] - } - return $seed - } - method objectmap {cmd} { - set seed [list] - dict for {k v} $o_data { - lappend seed [uplevel #0 [list $v {*}$cmd]] - } - return $seed - } - } - -} - +#JMN - api should be kept in sync with package patternlib where possible +# +package provide oolib [namespace eval oolib { + variable version + set version 0.1.2 +}] + +namespace eval oolib { + oo::class create collection { + variable o_data ;#dict + #variable o_alias + constructor {} { + set o_data [dict create] + } + method info {} { + return [dict info $o_data] + } + method count {} { + return [dict size $o_data] + } + method isEmpty {} { + expr {[dict size $o_data] == 0} + } + method names {{globOrIdx {}}} { + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + set idx $globOrIdx + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "[self object] no such index : '$idx'" + } else { + return $result + } + } else { + #glob + return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] + } + } else { + return [dict keys $o_data] + } + } + #like names but without globbing + method keys {} { + dict keys $o_data + } + method key {{posn 0}} { + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "[self object] no such index : '$posn'" + } else { + return $result + } + } + method hasKey {key} { + dict exists $o_data $key + } + method get {} { + return $o_data + } + method items {} { + return [dict values $o_data] + } + method item {key} { + if {[string is integer -strict $key]} { + if {$key >= 0} { + set valposn [expr {(2*$key) +1}] + return [lindex $o_data $valposn] + } else { + set key "end-[expr {abs($key + 1)}]" + return [lindex $o_data $key] + #return [lindex [dict keys $o_data] $key] + } + } + if {[dict exists $o_data $key]} { + return [dict get $o_data $key] + } + } + #inverse lookup + method itemKeys {value} { + set value_indices [lsearch -all [dict values $o_data] $value] + set keylist [list] + foreach i $value_indices { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + method search {value args} { + set matches [lsearch {*}$args [dict values $o_data] $value] + if {"-inline" in $args} { + return $matches + } else { + set keylist [list] + foreach i $matches { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + } + #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? + #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well? + #method alias {newAlias existingKeyOrAlias} { + # if {[string is integer -strict $newAlias]} { + # error "[self object] collection key alias cannot be integer" + # } + # if {[string length $existingKeyOrAlias]} { + # set o_alias($newAlias) $existingKeyOrAlias + # } else { + # unset o_alias($newAlias) + # } + #} + #method aliases {{key ""}} { + # if {[string length $key]} { + # set result [list] + # foreach {n v} [array get o_alias] { + # if {$v eq $key} { + # lappend result $n $v + # } + # } + # return $result + # } else { + # return [array get o_alias] + # } + #} + ##if the supplied index is an alias, return the underlying key; else return the index supplied. + #method realKey {idx} { + # if {[catch {set o_alias($idx)} key]} { + # return $idx + # } else { + # return $key + # } + #} + method add {value key} { + if {[string is integer -strict $key]} { + error "[self object] collection key must not be an integer. Use another structure if integer keys required" + } + if {[dict exists $o_data $key]} { + error "[self object] col_processors object error: key '$key' already exists in collection" + } + dict set o_data $key $value + return [expr {[dict size $o_data] - 1}] ;#return index of item + } + method remove {idx {endRange ""}} { + if {[string length $endRange]} { + error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" + } + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx+1)}]" + } + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + error "[self object] no such index: '$idx' in this collection" + } + } + dict unset o_data $key + return + } + method clear {} { + set o_data [dict create] + return + } + method reverse_the_collection {} { + #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs + #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy. + #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations. + set dictnew [dict create] + foreach k [lreverse [dict keys $o_data]] { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + return + } + #review - cmd as list vs cmd as script? + method map {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + return $seed + } + method objectmap {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + return $seed + } + } + +} + diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 6821acc8..afeacf7e 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -505,6 +505,8 @@ namespace eval punk { proc splitstrposn_nonzero {s p} { scan $s %${p}s%s } + + #split top level of patterns only. proc _split_patterns {varspecs} { set name_mapped [pipecmd_namemapping $varspecs] set cmdname ::punk::pipecmds::split_patterns_$name_mapped @@ -519,11 +521,13 @@ namespace eval punk { # % string functions # ! not set var_terminals [list "@" "/" "#" "%" "!" ">" "<"] ;# (> required for insertionspecs at rhs of = & .= ) + #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname + #except when prefixed directly by pin classifier ^ set protect_terminals [list "^"] ;# e.g sequence ^# #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' - set in_brackets 0 + set in_brackets 0 ;#count depth set in_atom 0 #set varspecs [string trimleft $varspecs ,] set token "" @@ -532,22 +536,38 @@ namespace eval punk { #} set first_term -1 set token_index 0 ;#index of terminal char within each token + set indq 0 + set inesc 0 ;#whether last char was backslash (see also punk::escv) set prevc "" set char_index 0 foreach c [split $varspecs ""] { - if {$in_atom} { + if {$indq} { + if {$inesc} { + #puts stderr "inesc adding '$c'" + append token $c + } else { + if {$c eq {"}} { + set indq 0 + } else { + append token $c + } + } + } elseif {$in_atom} { + #ignore dquotes/brackets in atoms - pass through append token $c #set nextc [lindex $chars $char_index+1] if {$c eq "'"} { set in_atom 0 } - } elseif {$in_brackets} { + } elseif {$in_brackets > 0} { append token $c if {$c eq ")"} { - set in_brackets 0 + incr in_brackets -1 } } else { - if {$c eq ","} { + if {$c eq {"} && !$inesc} { + set indq 1 + } elseif {$c eq ","} { #lappend varlist [splitstrposn $token $first_term] set var $token set spec "" @@ -568,16 +588,33 @@ namespace eval punk { set first_term -1 } else { append token $c - if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index - } elseif {$c eq "'"} { - set in_atom 1 - } elseif {$c eq "("} { - set in_brackets 1 + switch -exact -- $c { + ' { + set in_atom 1 + } + ( { + incr in_brackets + } + default { + if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { + set first_term $token_index + } + } } } } set prevc $c + if {$c eq "\\"} { + #review + if {$inesc} { + set inesc 0 + } else { + set token [string range $token 0 end-1] + set inesc 1 + } + } else { + set inesc 0 + } incr token_index incr char_index } @@ -1268,8 +1305,10 @@ namespace eval punk { append script \n "# index_operation listindex-nested" \n lappend INDEX_OPERATIONS listindex-nested } - append script \n [string map [list $subindices] { - set leveldata [lindex $leveldata ] + append script \n [tstr -return string -allowcommands { + if {[catch {lindex $leveldata ${$subindices}} leveldata]} { + ${[tstr -ret string $tpl_return_mismatch_not_a_list]} + } }] # -- --- --- #append script \n $returnline \n @@ -1283,7 +1322,7 @@ namespace eval punk { set keypath [string range $selector 2 end] set keylist [split $keypath /] lappend INDEX_OPERATIONS dict_path - if {([lindex $rawkeylist 0] ne "@@") && ([lsearch $keylist @*] == -1) && ([lsearch $keylist #*] == -1)} { + if {([lindex $rawkeylist 0] ne "@@") && ([lsearch $keylist @*] == -1) && ([lsearch $keylist #*] == -1) && ([lsearch $keylist %*] == -1)} { #pure keylist for dict - process in one go #dict exists will return 0 if not a valid dict. # is equivalent to {*}keylist when substituted @@ -1333,7 +1372,7 @@ namespace eval punk { #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. #append script \n {set do_boundscheck 0} switch -exact -- $index { - # { + # - @# { #list length set active_key_type "list" if {$get_not} { @@ -1395,16 +1434,96 @@ namespace eval punk { append script \n {set assigned [string length $leveldata]} set level_script_complete 1 } + %%# { + #experimental + set active_key_type "string" + if $get_not { + error "!%%# not string length is not supported" + } + #string length - REVIEW - + lappend INDEX_OPERATIONS ansistring-length + append script \n {# set active_key_type "" index_operation: ansistring-length} + append script \n {set assigned [ansistring length $leveldata]} + set level_script_complete 1 + } %str { set active_key_type "string" if $get_not { - error "!%# not string-get is not supported" + error "!%str - not string-get is not supported" } lappend INDEX_OPERATIONS string-get append script \n {# set active_key_type "" index_operation: string-get} append script \n {set assigned $leveldata} set level_script_complete 1 } + %sp { + #experimental + set active_key_type "string" + if $get_not { + error "!%sp - not string-space is not supported" + } + lappend INDEX_OPERATIONS string-space + append script \n {# set active_key_type "" index_operation: string-space} + append script \n {set assigned " "} + set level_script_complete 1 + } + %empty { + #experimental + set active_key_type "string" + if $get_not { + error "!%empty - not string-empty is not supported" + } + lappend INDEX_OPERATIONS string-empty + append script \n {# set active_key_type "" index_operation: string-empty} + append script \n {set assigned ""} + set level_script_complete 1 + } + @words { + set active_key_type "string" + if $get_not { + error "!%words - not list-words-from-string is not supported" + } + lappend INDEX_OPERATIONS list-words-from-string + append script \n {# set active_key_type "" index_operation: list-words-from-string} + append script \n {set assigned [regexp -inline -all {\S+} $leveldata]} + set level_script_complete 1 + } + @chars { + #experimental - leading character based on result not input(?) + #input type is string - but output is list + set active_key_type "list" + if $get_not { + error "!%chars - not list-chars-from-string is not supported" + } + lappend INDEX_OPERATIONS list-from_chars + append script \n {# set active_key_type "" index_operation: list-chars-from-string} + append script \n {set assigned [split $leveldata ""]} + set level_script_complete 1 + } + @join { + #experimental - flatten one level of list + #join without arg - output is list + set active_key_type "string" + if $get_not { + error "!@join - not list-join-list is not supported" + } + lappend INDEX_OPERATIONS list-join-list + append script \n {# set active_key_type "" index_operation: list-join-list} + append script \n {set assigned [join $leveldata]} + set level_script_complete 1 + } + %join { + #experimental + #input type is list - but output is string + set active_key_type "string" + if $get_not { + error "!%join - not string-join-list is not supported" + } + lappend INDEX_OPERATIONS string-join-list + append script \n {# set active_key_type "" index_operation: string-join-list} + append script \n {set assigned [join $leveldata ""]} + set level_script_complete 1 + } %ansiview { set active_key_type "string" if $get_not { @@ -1434,7 +1553,7 @@ namespace eval punk { #v_list_idx in context of _multi_bind_result append script \n {upvar v_list_idx v_list_idx} set active_key_type "list" - append script \n {# set active_key_type "list" index_operation: get-next} + append script \n {# set active_key_type "list" index_operation: list-get-next} #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3 #while x@,y@.= is reasonably handy - especially for args e.g $scopepattern] { #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail - set d [punk::_multi_bind_result "" $segmenttail] + set d [punk::_multi_bind_result {} $segmenttail] #return [punk::_handle_bind_result $d] #maintenance: inlined if {![dict exists $d result]} { @@ -3816,6 +3977,7 @@ namespace eval punk { } #return a script for inserting data into listvar + #review - needs updating for list-return semantics of patterns? proc list_insertion_script {keyspec listvar {data }} { set positionspec [string trimright $keyspec "*"] set do_expand [expr {[string index $keyspec end] eq "*"}] @@ -4495,7 +4657,7 @@ namespace eval punk { } append insertion_script \n {set insertion_data $v} } else { - + #todo - we should potentially group by the variable name and pass as a single call to _multi_bind_result - because stateful @ and @@ won't work in independent calls append insertion_script \n [string map [list $cmdname] { #puts ">>> v: $v dict_tagval:'$dict_tagval'" if {$v eq ""} { @@ -5042,93 +5204,96 @@ namespace eval punk { if {![info exists auto_noexec]} { set new [auto_execok $name] if {$new ne ""} { - set redir "" - if {[namespace which -command console] eq ""} { - set redir ">&@stdout <@stdin" - } + set redir "" + if {[namespace which -command console] eq ""} { + set redir ">&@stdout <@stdin" + } - #windows experiment todo - use twapi and named pipes - #twapi::namedpipe_server {\\.\pipe\something} - #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones - #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc - # - - if {[string first " " $new] > 0} { - set c1 $name - } else { - set c1 $new - } - - # -- --- --- --- --- - set idlist_stdout [list] - set idlist_stderr [list] - #set shellrun::runout "" - #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks - #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] - #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] + #windows experiment todo - use twapi and named pipes + #twapi::namedpipe_server {\\.\pipe\something} + #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones + #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc + # - if {![dict get $::punk::config::running exec_unknown]} { - #This runs external executables in a context in which they are not attached to a terminal - #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output - #ctrl-c propagation also needs to be considered - - set teehandle punksh - uplevel 1 [list ::catch \ - [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ - ::tcl::UnknownResult ::tcl::UnknownOptions] - - if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { - dict set ::tcl::UnknownOptions -code error - set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" + if {[string first " " $new] > 0} { + set c1 $name } else { - #no point returning "exitcode 0" if that's the only non-error return. - #It is misleading. Better to return empty string. - set ::tcl::UnknownResult "" + set c1 $new } - } else { - set repl_runid [punk::get_repl_runid] - #set ::punk::last_run_display [list] - - set redir ">&@stdout <@stdin" - uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] - #we can't detect stdout/stderr output from the exec - #for now emit an extra \n on stderr - #todo - there is probably no way around this but to somehow exec in the context of a completely separate console - #This is probably a tricky problem - especially to do cross-platform - # - # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit - if {[dict get $::tcl::UnknownOptions -code] == 0} { - set c green - set m "ok" + + # -- --- --- --- --- + set idlist_stdout [list] + set idlist_stderr [list] + #set shellrun::runout "" + #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks + #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] + #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] + + if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it + #not a trivial task + + #This runs external executables in a context in which they are not attached to a terminal + #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output + #ctrl-c propagation also needs to be considered + + set teehandle punksh + uplevel 1 [list ::catch \ + [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ + ::tcl::UnknownResult ::tcl::UnknownOptions] + + if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { + dict set ::tcl::UnknownOptions -code error + set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" + } else { + #no point returning "exitcode 0" if that's the only non-error return. + #It is misleading. Better to return empty string. + set ::tcl::UnknownResult "" + } } else { - set c yellow - set m "errorCode $::errorCode" - } - set chunklist [list] - lappend chunklist [list "info" "[a $c]$m[a] " ] - if {$repl_runid != 0} { - tsv::lappend repl runchunks-$repl_runid {*}$chunklist - } + set repl_runid [punk::get_repl_runid] + #set ::punk::last_run_display [list] + + set redir ">&@stdout <@stdin" + uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr + #todo - there is probably no way around this but to somehow exec in the context of a completely separate console + #This is probably a tricky problem - especially to do cross-platform + # + # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit + if {[dict get $::tcl::UnknownOptions -code] == 0} { + set c green + set m "ok" + } else { + set c yellow + set m "errorCode $::errorCode" + } + set chunklist [list] + lappend chunklist [list "info" "[a $c]$m[a] " ] + if {$repl_runid != 0} { + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } - } + } - foreach id $idlist_stdout { - shellfilter::stack::remove stdout $id - } - foreach id $idlist_stderr { - shellfilter::stack::remove stderr $id - } - # -- --- --- --- --- + foreach id $idlist_stdout { + shellfilter::stack::remove stdout $id + } + foreach id $idlist_stderr { + shellfilter::stack::remove stderr $id + } + # -- --- --- --- --- - #uplevel 1 [list ::catch \ - # [concat exec $redir $new [lrange $args 1 end]] \ - # ::tcl::UnknownResult ::tcl::UnknownOptions] + #uplevel 1 [list ::catch \ + # [concat exec $redir $new [lrange $args 1 end]] \ + # ::tcl::UnknownResult ::tcl::UnknownOptions] - #puts "===exec with redir:$redir $::tcl::UnknownResult ==" - dict incr ::tcl::UnknownOptions -level - return -options $::tcl::UnknownOptions $::tcl::UnknownResult + #puts "===exec with redir:$redir $::tcl::UnknownResult ==" + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult } } @@ -5374,8 +5539,6 @@ namespace eval punk { # #know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} #know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} - know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} - know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} @@ -5449,9 +5612,43 @@ namespace eval punk { return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] } + + # + know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} - know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^\t\r\n=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + + #add escaping backslashes to a value + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g + #set ktest {a"b} + #@@[escv $ktest].= list a"b val + #without escv: + #@@"a\\"b".= list a"b val + #with more backslashes in keys the escv use becomes more apparent: + #set ktest {\\x} + #@@[escv $ktest].= list $ktest val + #without escv we would need: + #@@\\\\\\\\x.= list $ktest val + proc escv {v} { + #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically + #thanks to DKF + regsub -all {\W} $v {\\&} + } + interp alias {} escv {} punk::escv + #review + #set v "\u2767" + # + #escv $v + #\ + #the + #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { # set argstail [lassign $args hd] @@ -7859,7 +8056,7 @@ namespace eval punk { set displaycount "" } if {$opt_ansi == 0} { - set displayval [punk::ansi::stripansi $displayval] + set displayval [punk::ansi::ansistrip $displayval] } elseif {$opt_ansi == 2} { set displayval [ansistring VIEW $displayval] } @@ -7951,20 +8148,26 @@ namespace eval punk { set text "" if {$topic in [list env environment]} { #todo - move to punk::config? + upvar ::punk::config::punk_env_vars_config punkenv_config + upvar ::punk::config::other_env_vars_config otherenv_config - set known_punk $::punk::config::known_punk_env_vars - set known_other $::punk::config::known_other_env_vars + set known_punk [dict keys $punkenv_config] + set known_other [dict keys $otherenv_config] append text \n set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] - foreach v $known_punk { + foreach {v vinfo} $punkenv_config { if {[info exists ::env($v)]} { set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } - $t add_row [list $v $c2] + set help "" + if {[dict exists $vinfo help]} { + set help [dict get $vinfo help] + } + $t add_row [list $v $c2 $help] } $t configure_column 0 -headers [list "Punk environment vars"] $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {any} @@ -7973,7 +8176,7 @@ namespace eval punk { $t destroy set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] - foreach v $known_other { + foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { set c2 [set ::env($v)] } else { @@ -8318,13 +8521,15 @@ namespace eval punk { # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion interp alias {} l {} sh_runout -n ls -A ;#plain text listing #interp alias {} ls {} sh_runout -n ls -AF --color=always - interp alias {} ls {} unknown ls -AF --color=always ;#use unknown to use terminal and allow | more | less + interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less #note that shell globbing with * won't work on unix systems when using unknown/exec interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? #interp alias {} lw {} ls -aFv --color=always + interp alias {} dir {} shellrun::console dir + interp alias {} ./ {} punk::d/ interp alias {} ../ {} punk::dd/ @@ -8358,8 +8563,8 @@ namespace eval punk { interp alias {} psr {} run -n pwsh -nop -nolo -c interp alias {} psout {} runout -n pwsh -nop -nolo -c interp alias {} pserr {} runerr -n pwsh -nop -nolo -c - interp alias {} psls {} pwsh -nop -nolo -c ls - interp alias {} psps {} pwsh -nop -nolo -c ps + interp alias {} psls {} shellrun::runconsole pwsh -nop -nolo -c ls + interp alias {} psps {} shellrun::runconsole pwsh -nop -nolo -c ps } else { set ps_missing "powershell missing (powershell is open source and can be installed on windows and most unix-like platforms)" interp alias {} ps {} puts stderr $ps_missing diff --git a/src/modules/punk/aliascore-999999.0a1.0.tm b/src/modules/punk/aliascore-999999.0a1.0.tm index 55926216..90a31f7c 100644 --- a/src/modules/punk/aliascore-999999.0a1.0.tm +++ b/src/modules/punk/aliascore-999999.0a1.0.tm @@ -112,7 +112,8 @@ tcl::namespace::eval punk::aliascore { plist [list ::punk::lib::pdict -roottype list]\ showlist [list ::punk::lib::showdict -roottype list]\ showdict ::punk::lib::showdict\ - ansistrip ::punk::ansi::stripansi\ + ansistrip ::punk::ansi::ansistrip\ + stripansi ::punk::ansi::ansistrip\ ] #*** !doctools @@ -165,18 +166,31 @@ tcl::namespace::eval punk::aliascore { error "punk::aliascore::init declined to create any aliases or imports because -force == 0 and conflicts found:$conflicts" } } + set tempns ::temp_[info cmdcount] ;#temp ns for renames dict for {a cmd} $aliases { + #puts "aliascore $a -> $cmd" if {[llength $cmd] > 1} { interp alias {} $a {} {*}$cmd } else { if {[tcl::info::commands $cmd] ne ""} { #todo - ensure exported? noclobber? - tcl::namespace::eval :: [list namespace import $cmd] + if {[tcl::namespace::tail $a] eq [tcl::namespace::tail $cmd]} { + #puts stderr "importing $cmd" + tcl::namespace::eval :: [list namespace import $cmd] + } else { + #target command name differs from exported name + #e.g stripansi -> punk::ansi::ansistrip + #import and rename + #puts stderr "importing $cmd (with rename to ::$a)" + tcl::namespace::eval $tempns [list namespace import $cmd] + catch {rename ${tempns}::[namespace tail $cmd] ::$a} + } } else { interp alias {} $a {} {*}$cmd } } } + #tcl::namespace::delete $tempns return [dict keys $aliases] } @@ -188,7 +202,7 @@ tcl::namespace::eval punk::aliascore { #interp alias {} list_as_lines {} punk::lib::list_as_lines #interp alias {} lines_as_list {} punk::lib::lines_as_list -#interp alias {} ansistrip {} punk::ansi::stripansi ;#review +#interp alias {} ansistrip {} punk::ansi::ansistrip ;#review #interp alias {} linelist {} punk::lib::linelist ;#critical for = assignment features #interp alias {} linesort {} punk::lib::linesort diff --git a/src/modules/punk/ansi-999999.0a1.0.tm b/src/modules/punk/ansi-999999.0a1.0.tm index 8017b5e6..747364c1 100644 --- a/src/modules/punk/ansi-999999.0a1.0.tm +++ b/src/modules/punk/ansi-999999.0a1.0.tm @@ -265,13 +265,13 @@ tcl::namespace::eval punk::ansi::class { } set opts_width [tcl::dict::get $opts -width] if {$opts_width eq ""} { - return [punk::ansi::stripansiraw [$o_ansistringobj get]] + return [punk::ansi::ansistripraw [$o_ansistringobj get]] } elseif {$opts_width eq "auto"} { lassign [punk::console::get_size] _cols columns _rows rows set displaycols [expr {$columns -4}] ;#review - return [overtype::renderspace -width $displaycols -wrap 1 "" [punk::ansi::stripansiraw [$o_ansistringobj get]]] + return [overtype::renderspace -width $displaycols -wrap 1 "" [punk::ansi::ansistripraw [$o_ansistringobj get]]] } elseif {[tcl::string::is integer -strict $opts_width] && $opts_width > 0} { - return [overtype::renderspace -width $opts_width -wrap 1 "" [punk::ansi::stripansiraw [$o_ansistringobj get]]] + return [overtype::renderspace -width $opts_width -wrap 1 "" [punk::ansi::ansistripraw [$o_ansistringobj get]]] } else { error "viewchars unrecognised value for -width. Try auto or a positive integer" } @@ -420,7 +420,7 @@ tcl::namespace::eval punk::ansi { get_*\ move*\ reset*\ - strip*\ + ansistrip*\ test_decaln\ titleset\ @@ -750,7 +750,7 @@ tcl::namespace::eval punk::ansi { #mqj #m = boxd_lur - #don't call detect_g0 in here. Leave for caller. e.g stripansi uses detect_g0 to decide whether to call this. + #don't call detect_g0 in here. Leave for caller. e.g ansistrip uses detect_g0 to decide whether to call this. set re_g0_open_or_close {\x1b\(0|\x1b\(B} set parts [::punk::ansi::ta::_perlish_split $re_g0_open_or_close $text] @@ -813,14 +813,17 @@ tcl::namespace::eval punk::ansi { proc g0 {text} { return \x1b(0$text\x1b(B } + proc ansistrip_gx {text} { + #e.g "\033(0" - select VT100 graphics for character set G0 + #e.g "\033(B" - reset + #e.g "\033)0" - select VT100 graphics for character set G1 + #e.g "\033)X" - where X is any char other than 0 to reset ?? + + #return [convert_g0 $text] + return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] + } proc stripansi_gx {text} { - #e.g "\033(0" - select VT100 graphics for character set G0 - #e.g "\033(B" - reset - #e.g "\033)0" - select VT100 graphics for character set G1 - #e.g "\033)X" - where X is any char other than 0 to reset ?? - - #return [convert_g0 $text] - return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] + return [tcl::string::map [list "\x1b(0" "" \x1b(B" "" "\x1b)0" "" "\x1b)X" ""] $text] } @@ -1085,7 +1088,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #This is an in depth analysis of the xterm colour set which gives names(*) to all of the 256 colours and describes possible indexing by Hue,Luminance,Saturation #https://www.wowsignal.io/articles/xterm256 - #*The names are wildly-imaginative, often unintuitively so, and multiple (5?) given for each colour - so they are unlikely to be of practical use or any sort of standard. + # *The names are wildly-imaginative, often unintuitively so, and multiple (5?) given for each colour - so they are unlikely to be of practical use or any sort of standard. #e.g who is to know that 'Rabbit Paws', 'Forbidden Thrill' and 'Tarsier' refer to a particular shade of pinky-red? (code 95) #Perhaps it's an indication that colour naming once we get to 256 colours or more is a fool's errand anyway. #The xterm names are boringly unimaginative - and also have some oddities such as: @@ -2263,23 +2266,25 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #sgr_cache clear called by punk::console::ansi when set to off proc sgr_cache {args} { set argd [punk::args::get_dict { - -action -default "" -choices "clear" - -pretty -default 1 -help "use 'pdict punk::ansi::sgr_cache */%str,%ansiview' output" + *proc -name punk::ansi::sgr_cache -help "Convenience function to view and optionally clear the ansi character attribute cache (ansi SGR codes) + " + -action -default "" -choices "clear" -help "-action clear will unset the keys in the punk::ansi::sgr_cache dict + This is called automatically when setting 'colour false' in the console" + + -pretty -default 1 -type boolean -help "use 'pdict punk::ansi::sgr_cache */%str,%ansiview' output" *values -min 0 -max 0 } $args] set action [dict get $argd opts -action] set pretty [dict get $argd opts -pretty] variable sgr_cache - if {$action ni {"" clear}} { - error "sgr_cache action '$action' not understood. Valid actions: clear" - } if {$action eq "clear"} { set sgr_cache [tcl::dict::create] return "sgr_cache cleared" } if {$pretty} { - return [pdict -channel none sgr_cache */%str,%ansiview] + #return [pdict -channel none sgr_cache */%str,%ansiview] + return [pdict -channel none sgr_cache */%rpadstr-"sample",%ansiviewstyle] } if {[catch { @@ -2323,7 +2328,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #function name part of cache-key because a and a+ return slightly different results (a has leading reset) variable sgr_cache - set cache_key a+$args ;#ensure cache_key static - we may remove for example 'forcecolour' from args - but it needs to remain part of cache_key + set cache_key "a+ $args" ;#ensure cache_key static - we may remove for example 'forcecolour' from args - but it needs to remain part of cache_key if {[tcl::dict::exists $sgr_cache $cache_key]} { return [tcl::dict::get $sgr_cache $cache_key] } @@ -2682,7 +2687,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #It's important to put the functionname in the cache-key because a and a+ return slightly different results variable sgr_cache - set cache_key a_$args + set cache_key "a $args" if {[tcl::dict::exists $sgr_cache $cache_key]} { return [tcl::dict::get $sgr_cache $cache_key] } @@ -2693,7 +2698,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu variable TERM_colour_map set colour_disabled 0 - #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache clear + #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache -action clear if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { set colour_disabled 1 } @@ -3393,10 +3398,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #arguably - \b and \r are cursor move operations too - so processing them here is not very symmetrical - review #the purpose of backspace (or line cr) in embedded text is unclear. Should it allow some sort of character combining/overstrike as it has sometimes done historically (nroff/less)? e.g a\b` as an alternative combiner or bolding if same char #This should presumably only be done if the over_strike (os) capability is enabled in the terminal. Either way - it presumably won't affect printing width? - set line [punk::ansi::stripansi $line] + set line [punk::ansi::ansistrip $line] #we can't use simple \b processing if we get ansi codes and aren't actually processing them (e.g moves) - set line [punk::char::strip_nonprinting_ascii $line] ;#only strip nonprinting after stripansi - some like BEL are part of ansi + set line [punk::char::strip_nonprinting_ascii $line] ;#only strip nonprinting after ansistrip - some like BEL are part of ansi #backspace 0x08 only erases* printing characters anyway - so presumably order of processing doesn't matter #(* more correctly - moves cursor back) #Note some terminals process backspace before \v - which seems quite wrong @@ -3512,6 +3517,40 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } + #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks + proc ansistrip {text} { + #*** !doctools + #[call [fun ansistrip] [arg text] ] + #[para]Return a string with ansi codes stripped out + #[para]Alternate graphics chars are replaced with modern unicode equivalents (e.g boxdrawing glyphs) + + if {[punk::ansi::ta::detect_g0 $text]} { + set text [convert_g0 $text];#Convert ansi borders to unicode line drawing instead of ascii letters + } + set parts [punk::ansi::ta::split_codes $text] + set out "" + foreach {pt code} $parts { + append out $pt + } + return $out + } + #interp alias {} stripansi {} ::punk::ansi::ansistrip + proc ansistripraw {text} { + #*** !doctools + #[call [fun ansistripraw] [arg text] ] + #[para]Return a string with ansi codes stripped out + #[para]Alternate graphics modes will be stripped rather than converted to unicode - exposing the raw ascii characters as they appear without graphics mode. + #[para]ie instead of a horizontal line you may see: qqqqqq + + set parts [punk::ansi::ta::split_codes $text] + set out "" + foreach {pt code} $parts { + append out $pt + } + return $out + } + #interp alias {} stripansiraw {} ::punk::ansi::ansistripraw + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::ansi ---}] } @@ -4293,16 +4332,16 @@ tcl::namespace::eval punk::ansi::ta { #*** !doctools #[call [fun strip] [arg text]] #[para]Return text stripped of Ansi codes - #[para]This is a tailcall to punk::ansi::stripansi - tailcall stripansi $text + #[para]This is a tailcall to punk::ansi::ansistrip + tailcall ansistrip $text } proc length {text} { #*** !doctools #[call [fun length] [arg text]] #[para]Return the character length after stripping ansi codes - not the printing length - #we can use stripansiraw to avoid g0 conversion - as the length should remain the same - tcl::string::length [stripansiraw $text] + #we can use ansistripraw to avoid g0 conversion - as the length should remain the same + tcl::string::length [ansistripraw $text] } #todo - handle newlines #not in perl ta @@ -5451,32 +5490,8 @@ tcl::namespace::eval punk::ansi::class { } } tcl::namespace::eval punk::ansi { - proc stripansi {text} { - #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks - if {[punk::ansi::ta::detect_g0 $text]} { - set text [convert_g0 $text];#Convert ansi borders to unicode line drawing instead of ascii letters - } - set parts [punk::ansi::ta::split_codes $text] - set out "" - foreach {pt code} $parts { - append out $pt - } - return $out - } - proc stripansiraw {text} { - #slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks - set parts [punk::ansi::ta::split_codes $text] - set out "" - foreach {pt code} $parts { - append out $pt - } - return $out - } + proc stripansi3 {text} [string map [list $::punk::ansi::ta::re_ansi_split] { - #*** !doctools - #[call [fun stripansi] [arg text] ] - #[para]Return a string with ansi codes stripped out - #[para]Alternate graphics chars are replaced with modern unicode equivalents (e.g boxdrawing glyphs) #using detect costs us a couple of uS - but saves time on plain text #we should probably leave this for caller - otherwise it ends up being called more than necessary @@ -5493,11 +5508,6 @@ tcl::namespace::eval punk::ansi { }] proc stripansiraw3 {text} [string map [list $::punk::ansi::ta::re_ansi_split] { - #*** !doctools - #[call [fun stripansi] [arg text] ] - #[para]Return a string with ansi codes stripped out - #[para]Alternate graphics modes will be stripped rather than converted to unicode - exposing the raw ascii characters as they appear without graphics mode. - #[para]ie instead of a horizontal line you may see: qqqqqq #join [::punk::ansi::ta::split_at_codes $text] "" punk::ansi::ta::Do_split_at_codes_join $text {} @@ -5923,7 +5933,7 @@ tcl::namespace::eval punk::ansi::ansistring { #[para]Returns the count of visible graphemes and non-ansi control characters #[para]Incomplete! grapheme clustering support not yet implemented - only diacritics are currently clustered to count as one grapheme. #[para]This will not count strings hidden inside a 'privacy message' or other ansi codes which may have content between their opening escape and their termination sequence. - #[para]This is not quite equivalent to calling string length on the result of stripansi $string due to diacritics and/or grapheme combinations + #[para]This is not quite equivalent to calling string length on the result of ansistrip $string due to diacritics and/or grapheme combinations #[para]Note that this returns the number of characters in the payload (after applying combiners) #It is not always the same as the width of the string as rendered on a terminal due to 2wide Unicode characters and the usual invisible control characters such as \r and \n #[para]To get the width, use punk::ansi::printing_length instead, which is also ansi aware. @@ -5935,17 +5945,17 @@ tcl::namespace::eval punk::ansi::ansistring { set string [regsub -all $re_diacritics $string ""] #we want length to return number of glyphs.. not screen width. Has to be consistent with index function - tcl::string::length [stripansi $string] + tcl::string::length [ansistrip $string] } #included as a test/verification - slightly slower. #grapheme split version may end up being used once it supports unicode grapheme clusters proc count2 {string} { #we want count to return number of glyphs.. not screen width. Has to be consistent with index function - return [llength [punk::char::grapheme_split [stripansi $string]]] + return [llength [punk::char::grapheme_split [ansistrip $string]]] } proc length {string} { - tcl::string::length [stripansi $string] + tcl::string::length [ansistrip $string] } proc _splits_trimleft {sclist} { @@ -6055,9 +6065,9 @@ tcl::namespace::eval punk::ansi::ansistring { #[para]The string could contain non SGR ansi codes - and these will (mostly) be ignored, so shouldn't affect the output. #[para]Some terminals don't hide 'privacy message' and other strings within an ESC X ESC ^ or ESC _ sequence (terminated by ST) #[para]It's arguable some of these are application specific - but this function takes the view that they are probably non-displaying - so index won't see them. - #[para]If the caller wants just the character - they should use a normal string index after calling stripansi, or call stripansi afterwards. - #[para]As any operation using end-+ will need to strip ansi to precalculate the length anyway; the caller should probably just use stripansi and standard string index if the ansi coded output isn't required and they are using and end-based index. - #[para]In fact, any operation where the ansi info isn't required in the output would probably be slightly more efficiently obtained by using stripansi and normal string operations on that. + #[para]If the caller wants just the character - they should use a normal string index after calling ansistrap, or call ansistrip afterwards. + #[para]As any operation using end-+ will need to strip ansi to precalculate the length anyway; the caller should probably just use ansistrip and standard string index if the ansi coded output isn't required and they are using and end-based index. + #[para]In fact, any operation where the ansi info isn't required in the output would probably be slightly more efficiently obtained by using ansistrip and normal string operations on that. #[para]The returned character will (possibly) have a leading ansi escape sequence but no trailing escape sequence - even if the string was taken from a position immediately before a reset or other SGR ansi code #[para]The ansi-code prefix in the returned string is built up by concatenating previous SGR ansi codes seen - but it is optimised to re-start the process if any full SGR reset is encountered. #[para]The code sequence doesn't detect individual properties being turned on and then off again, only full resets; so in some cases the ansi-prefix may not be as short as it could be. diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 1b85cfec..b41c9fb5 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -267,6 +267,9 @@ tcl::namespace::eval punk::args { #[list_begin definitions] + #todo? -synonym ? (applies to opts only not values) + #e.g -background -synonym -bg -default White + proc Get_argspecs {optionspecs args} { variable argspec_cache variable argspecs @@ -333,7 +336,7 @@ tcl::namespace::eval punk::args { foreach rawline $linelist { set recordsofar [tcl::string::cat $linebuild $rawline] #ansi colours can stop info complete from working (contain square brackets) - if {![tcl::info::complete [punk::ansi::stripansi $recordsofar]]} { + if {![tcl::info::complete [punk::ansi::ansistrip $recordsofar]]} { #append linebuild [string trimleft $rawline] \n if {$in_record} { if {[tcl::string::length $lastindent]} { @@ -696,6 +699,7 @@ tcl::namespace::eval punk::args { } proc arg_error {msg spec_dict {badarg ""}} { + # use basic colours here to support terminals without extended colours #todo - add checks column (e.g -minlen -maxlen) set errmsg $msg if {![catch {package require textblock}]} { @@ -704,18 +708,21 @@ tcl::namespace::eval punk::args { set procname [punk::lib::dict_getdef $spec_dict proc_info -name ""] set prochelp [punk::lib::dict_getdef $spec_dict proc_info -help ""] - set t [textblock::class::table new [a+ web-yellow]Usage[a]] + #set t [textblock::class::table new [a+ web-yellow]Usage[a]] + set t [textblock::class::table new [a+ brightyellow]Usage[a]] set blank_header_col [list ""] if {$procname ne ""} { lappend blank_header_col "" - set procname_display [a+ web-white]$procname[a] + #set procname_display [a+ web-white]$procname[a] + set procname_display [a+ brightwhite]$procname[a] } else { set procname_display "" } if {$prochelp ne ""} { lappend blank_header_col "" - set prochelp_display [a+ web-white]$prochelp[a] + #set prochelp_display [a+ web-white]$prochelp[a] + set prochelp_display [a+ brightwhite]$prochelp[a] } else { set prochelp_display "" } @@ -738,9 +745,12 @@ tcl::namespace::eval punk::args { $t configure_header 2 -values {Arg Type Default Multiple Help} } - set c_default [a+ web-white Web-limegreen] - set c_badarg [a+ web-crimson] - set greencheck [a+ web-limegreen]\u2713[a] + #set c_default [a+ web-white Web-limegreen] + set c_default [a+ brightwhite Brightgreen] + #set c_badarg [a+ web-crimson] + set c_badarg [a+ brightred] + #set greencheck [a+ web-limegreen]\u2713[a] + set greencheck [a+ brightgreen]\u2713[a] foreach arg [dict get $spec_dict opt_names] { set arginfo [dict get $spec_dict arg_info $arg] @@ -789,7 +799,8 @@ tcl::namespace::eval punk::args { } - $t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ web-white] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] + #$t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ web-white] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] + $t configure -show_hseps 0 -show_header 1 -ansibase_body [a+ brightwhite] -ansibase_header [a+ brightyellow] -ansiborder_header [a+ brightyellow] -ansiborder_body [a+ brightyellow] $t configure -maxwidth 80 append errmsg [$t print] $t destroy @@ -1209,7 +1220,7 @@ tcl::namespace::eval punk::args { package require punk::ansi set vlist_check [list] foreach e $vlist { - lappend vlist_check [punk::ansi::stripansi $e] + lappend vlist_check [punk::ansi::ansistrip $e] } } else { #validate_without_ansi 0 @@ -1437,7 +1448,7 @@ tcl::namespace::eval punk::args { } } if {$is_strip_ansi} { - set stripped_list [lmap e $vlist {punk::ansi::stripansi $e}] ;#no faster or slower, but more concise than foreach + set stripped_list [lmap e $vlist {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach if {[tcl::dict::get $thisarg -multiple]} { if {[tcl::dict::get $thisarg -ARGTYPE] eq "option"} { tcl::dict::set opts $argname $stripped_list diff --git a/src/modules/punk/char-999999.0a1.0.tm b/src/modules/punk/char-999999.0a1.0.tm index 4d8503c3..3f41f36d 100644 --- a/src/modules/punk/char-999999.0a1.0.tm +++ b/src/modules/punk/char-999999.0a1.0.tm @@ -1950,7 +1950,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} @@ -2057,7 +2057,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} @@ -2161,7 +2161,7 @@ tcl::namespace::eval punk::char { #we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!) #anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error #if {[tcl::string::first \033 $text] >= 0} { - # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first" + # error "string_width doesn't accept ansi escape sequences. Use punk::ansi::ansistrip first" #} diff --git a/src/modules/punk/config-0.1.tm b/src/modules/punk/config-0.1.tm index cf23c15a..a1f11212 100644 --- a/src/modules/punk/config-0.1.tm +++ b/src/modules/punk/config-0.1.tm @@ -3,11 +3,13 @@ tcl::namespace::eval punk::config { variable loaded variable startup ;#include env overrides variable running - variable known_punk_env_vars - variable known_other_env_vars + variable punk_env_vars + variable other_env_vars variable vars + namespace export {[a-z]*} + #todo - XDG_DATA_HOME etc #https://specifications.freedesktop.org/basedir-spec/latest/ # see also: http://hiphish.github.io/blog/2020/08/30/dotfiles-were-a-mistake/ @@ -16,8 +18,10 @@ tcl::namespace::eval punk::config { variable defaults variable startup variable running - variable known_punk_env_vars - variable known_other_env_vars + variable punk_env_vars + variable punk_env_vars_config + variable other_env_vars + variable other_env_vars_config set exename "" catch { @@ -55,12 +59,13 @@ tcl::namespace::eval punk::config { set default_logfile_stderr "" } - # exec_unknown ;#whether to use exec instead of experimental shellfilter::run + # auto_exec_mechanism ;#whether to use exec instead of experimental shellfilter::run #startup color_stdout - parameters as suitable for punk::ansi::a+ (test with 'punk::ansi::a?') e.g "cyan bold" ;#not a good idea to default set default_color_stdout "" #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. #set default_color_stderr "red bold" - set default_color_stderr "web-lightsalmon" + #set default_color_stderr "web-lightsalmon" + set default_color_stderr yellow ;#limit to basic colours for wider terminal support. yellow = term-olive set homedir "" if {[catch { @@ -134,7 +139,8 @@ tcl::namespace::eval punk::config { syslog_stdout "127.0.0.1:514"\ syslog_stderr "127.0.0.1:514"\ syslog_active 0\ - exec_unknown true\ + auto_exec_mechanism exec\ + auto_noexec 0\ xdg_config_home $default_xdg_config_home\ xdg_data_home $default_xdg_data_home\ xdg_cache_home $default_xdg_cache_home\ @@ -159,31 +165,53 @@ tcl::namespace::eval punk::config { #todo - load/save config file #todo - define which configvars are settable in env - set known_punk_env_vars [list \ - PUNK_APPS\ - PUNK_CONFIG\ - PUNK_CONFIGSET\ - PUNK_SCRIPTLIB\ - PUNK_EXECUNKNOWN\ - PUNK_COLOR_STDERR\ - PUNK_COLOR_STDOUT\ - PUNK_LOGFILE_STDOUT\ - PUNK_LOGFILE_STDERR\ - PUNK_LOGFILE_ACTIVE\ - PUNK_SYSLOG_STDOUT\ - PUNK_SYSLOG_STDERR\ - PUNK_SYSLOG_ACTIVE\ - PUNK_THEME_POSH_OVERRIDE\ + #list of varname varinfo where varinfo is a sub dictionary (type key is mandatory, with value from: string,pathlist,boolean) + set punk_env_vars_config [dict create \ + PUNK_APPS {type pathlist}\ + PUNK_CONFIG {type string}\ + PUNK_CONFIGSET {type string}\ + PUNK_SCRIPTLIB {type string}\ + PUNK_AUTO_EXEC_MECHANISM {type string}\ + PUNK_AUTO_NOEXEC {type string default 0 help "set 1 to set Tcl's ::auto_noexec true.\nStops 'unknown' from running external programs"}\ + PUNK_COLOR_STDERR {type string}\ + PUNK_COLOR_STDOUT {type string}\ + PUNK_LOGFILE_STDOUT {type string}\ + PUNK_LOGFILE_STDERR {type string}\ + PUNK_LOGFILE_ACTIVE {type string}\ + PUNK_SYSLOG_STDOUT {type string}\ + PUNK_SYSLOG_STDERR {type string}\ + PUNK_SYSLOG_ACTIVE {type string}\ + PUNK_THEME_POSH_OVERRIDE {type string}\ ] + set punk_env_vars [dict keys $punk_env_vars_config] #override with env vars if set - foreach evar $known_punk_env_vars { + foreach {evar varinfo} $punk_env_vars_config { if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] set f [set ::env($evar)] if {$f ne "default"} { #e.g PUNK_SCRIPTLIB -> scriptlib set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] - tcl::dict::set startup $varname $f + if {$vartype eq "pathlist"} { + #colon vs semicolon path sep is problematic for windows environments where unix-like systems such as cygwin/wsl are used and a variable may be set for either the native path separator or the unix-like system + #Even without the colon vs semicolon issue, native vs unix-like paths on windows mixed environment systems can cause grief. + #For now at least, we will simply respect the platform pathSeparator and hope the user manages the environment variables appropriately. + #some programs do automatic translation - which is a nice idea in principle - but is also prone to error as we don't know if it's already occurred or not depending on how things are launched. + #An example of where this sort of thing can go wrong is env(TCLLIBPATH) - which is defined as a space separated list not requiring further splitting + # - but some programs have been known to split this value on colon anyway, which breaks things on windows. + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } } } } @@ -194,22 +222,37 @@ tcl::namespace::eval punk::config { # set colour_disabled 1 # } #} - set known_other_env_vars [list\ - NO_COLOR\ - XDG_CONFIG_HOME\ - XDG_DATA_HOME\ - XDG_CACHE_HOME\ - XDG_STATE_HOME\ - XDG_DATA_DIRS\ - POSH_THEME\ - POSH_THEMES_PATH\ + set other_env_vars_config [dict create\ + NO_COLOR {type string}\ + XDG_CONFIG_HOME {type string}\ + XDG_DATA_HOME {type string}\ + XDG_CACHE_HOME {type string}\ + XDG_STATE_HOME {type string}\ + XDG_DATA_DIRS {type pathlist}\ + POSH_THEME {type string}\ + POSH_THEMES_PATH {type string}\ ] - foreach evar $known_other_env_vars { + set other_env_vars [dict keys $other_env_vars_config] + + foreach {evar varinfo} $other_env_vars_config { if {[info exists ::env($evar)]} { + set vartype [dict get $varinfo type] set f [set ::env($evar)] if {$f ne "default"} { set varname [tcl::string::tolower $evar] - tcl::dict::set startup $varname $f + if {$vartype eq "pathlist"} { + set paths [split $f $::tcl_platform(pathSeparator)] + set final [list] + #eliminate empty values (leading or trailing or extraneous separators) + foreach p $paths { + if {[tcl::string::trim $p] ne ""} { + lappend final $p + } + } + tcl::dict::set startup $varname $final + } else { + tcl::dict::set startup $varname $f + } } } } @@ -217,11 +260,39 @@ tcl::namespace::eval punk::config { #unset -nocomplain vars + #todo set running [tcl::dict::create] set running [tcl::dict::merge $running $startup] } init + #todo + proc Apply {config} { + puts stderr "punk::config::Apply partially implemented" + set configname [string map {-config ""} $config] + if {$configname in {startup running}} { + upvar ::punk::config::$configname applyconfig + + if {[dict exists $applyconfig auto_noexec]} { + set auto [dict get $applyconfig auto_noexec] + if {![string is boolean -strict $auto]} { + error "config::Apply error - invalid data for auto_noexec:'$auto' - expected boolean" + } + if {$auto} { + set ::auto_noexec 1 + } else { + #puts "auto_noexec false" + unset -nocomplain ::auto_noexec + } + } + + } else { + error "no config named '$config' found" + } + return "apply done" + } + Apply startup + #todo - consider how to divide up settings, categories, 'devices', decks etc proc get_running_global {varname} { variable running @@ -256,7 +327,8 @@ tcl::namespace::eval punk::config { set argd [punk::args::get_dict { whichconfig -type string -choices {startup running} - }] + } $args] + } proc show {whichconfig} { @@ -279,11 +351,58 @@ tcl::namespace::eval punk::config { # copy running-config startup-config # copy startup-config test-config.cfg # copy backup-config.cfg running-config - #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite ? - proc copy {fromconfig toconfig} { - error "sorry - unimplemented" - switch -- $toconfig { + #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite + #This is to allow partial configs to be loaded to running, whereas a save of running to any target is always a complete configuration + proc copy {args} { + set argd [punk::args::get_dict { + *proc -name punk::config::copy -help "Copy a partial or full configuration from one config to another + If a target config has additional settings, then the source config can be considered to be partial with regards to the target. + " + -type -default "" -choices {replace merge} -help "Defaults to merge when target is running-config + Defaults to replace when source is running-config" + *values -min 2 -max 2 + fromconfig -help "running or startup or file name (not fully implemented)" + toconfig -help "running or startup or file name (not fully implemented)" + } $args] + set fromconfig [dict get $argd values fromconfig] + set toconfig [dict get $argd values toconfig] + set fromconfig [string map {-config ""} $fromconfig] + set toconfig [string map {-config ""} $toconfig] + + set copytype [dict get $argd opts -type] + + + #todo - warn & prompt if doing merge copy to startup + switch -exact -- $fromconfig-$toconfig { + running-startup { + if {$copytype eq ""} { + set copytype replace ;#full configuration + } + if {$copytype eq "replace"} { + error "punk::config::copy error. full configuration copy from running to startup config not yet supported" + } else { + error "punk::config::copy error. merge configuration copy from running to startup config not yet supported" + } + } + startup-running { + #default type merge - even though it's not always what is desired + if {$copytype eq ""} { + set copytype merge ;#load in a partial configuration + } + #warn/prompt either way + if {$copytype eq "replace"} { + #some routers require use of a separate command for this branch. + #presumably to ensure the user doesn't accidentally load partials onto a running system + # + error "punk::config::copy error. full configuration copy from startup to overwrite running config not supported" + } else { + error "punk::config::copy error. merge copy from possibly partial configuration: startup to running config not currently supported" + } + } + default { + error "punk::config::copy error. copy must from running to startup or startup to running. File sources/targets not yet supported" + } } } diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index d1a6d399..bf2e2460 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -51,7 +51,7 @@ namespace eval punk::console { variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run. #-1 still evaluates to true - as the modern assumption for ansi availability is true #only false if ansi_available has been set 0 by test_can_ansi - #support stripansi for legacy windows terminals + #support ansistrip for legacy windows terminals # -- variable ansi_wanted 2 ;#2 for default assumed yes, will be set to -1 for automatically unwanted when ansi unavailable values of 0 or 1 won't be autoset @@ -780,7 +780,7 @@ namespace eval punk::console { #stdout variable ansi_wanted if {$ansi_wanted <= 0} { - puts -nonewline [punk::ansi::stripansiraw [::punk::ansi::a?]] + puts -nonewline [punk::ansi::ansistripraw [::punk::ansi::a?]] } else { tailcall ansi::a? {*}$args } @@ -806,7 +806,7 @@ namespace eval punk::console { proc code_a? {args} { variable ansi_wanted if {$ansi_wanted <= 0} { - return [punk::ansi::stripansi [::punk::ansi::a? {*}$args]] + return [punk::ansi::ansistripraw [::punk::ansi::a? {*}$args]] } else { tailcall ::punk::ansi::a? {*}$args } @@ -833,7 +833,7 @@ namespace eval punk::console { false - no { set ansi_wanted 0 - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear } default { set ansi_wanted 2 @@ -859,7 +859,7 @@ namespace eval punk::console { if {$on} { if {$colour_disabled} { #change of state - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear catch {punk::repl::reset_prompt} set colour_disabled 0 } @@ -867,7 +867,7 @@ namespace eval punk::console { #we don't disable a/a+ entirely - they must still emit underlines/bold/reverse if {!$colour_disabled} { #change of state - punk::ansi::sgr_cache clear + punk::ansi::sgr_cache -action clear catch {punk::repl::reset_prompt} set colour_disabled 1 } diff --git a/src/modules/punk/fileline-999999.0a1.0.tm b/src/modules/punk/fileline-999999.0a1.0.tm index 90fb97b4..254cea84 100644 --- a/src/modules/punk/fileline-999999.0a1.0.tm +++ b/src/modules/punk/fileline-999999.0a1.0.tm @@ -318,7 +318,7 @@ namespace eval punk::fileline::class { package require overtype # will require punk::char and punk::ansi - if {"::punk::fileline::ansi::stripansi" ne [info commands ::punk::fileline::ansi::stripansi]} { + if {"::punk::fileline::ansi::ansistrip" ne [info commands ::punk::fileline::ansi::ansistrip]} { namespace eval ::punk::fileline::ansi { namespace import ::punk::ansi::* } @@ -334,7 +334,7 @@ namespace eval punk::fileline::class { } else { set ::punk::fileline::ansi::enabled 0 } - if {"::punk::fileline::stripansi" ne [info commands ::punk::fileline::stripansi]} { + if {"::punk::fileline::ansistrip" ne [info commands ::punk::fileline::ansistrip]} { proc ::punk::fileline::a {args} { if {$::punk::fileline::ansi::enabled} { tailcall ::punk::fileline::ansi::a {*}$args @@ -349,9 +349,9 @@ namespace eval punk::fileline::class { return "" } } - proc ::punk::fileline::stripansi {str} { + proc ::punk::fileline::ansistrip {str} { if {$::punk::fileline::ansi::enabled} { - tailcall ::punk::fileline::ansi::stripansi $str + tailcall ::punk::fileline::ansi::ansistrip $str } else { return $str } @@ -560,7 +560,7 @@ namespace eval punk::fileline::class { set title_line "Line" #todo - use punk::char for unicode support of wide chars etc? set widest_linenum [tcl::mathfunc::max {*}[lmap v [concat [list $title_linenum] $linenums] {string length $v}]] - set widest_marker [tcl::mathfunc::max {*}[lmap v [concat [list $title_marker] $markers] {string length [stripansi $v]}]] + set widest_marker [tcl::mathfunc::max {*}[lmap v [concat [list $title_marker] $markers] {string length [ansistrip $v]}]] set widest_status [expr {max([string length $opt_cmark], [string length $opt_tmark])}] set widest_line [tcl::mathfunc::max {*}[lmap v [concat [list $title_line] $lines] {string length $v}]] foreach row $result_list { @@ -1711,7 +1711,7 @@ namespace eval punk::fileline::ansi { #*** !doctools #[call [fun ansi::a]] #[call [fun ansi::a+]] - #[call [fun ansi::stripansi]] + #[call [fun ansi::ansistrip]] #*** !doctools #[list_end] [comment {--- end definitions namespace punk::fileline::ansi ---}] diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index 4a8a3738..b59b5fee 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/src/modules/punk/lib-999999.0a1.0.tm @@ -66,34 +66,34 @@ package require Tcl 8.6- # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # oo::class namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::lib::class { - #*** !doctools - #[subsection {Namespace punk::lib::class}] - #[para] class definitions - if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { - #*** !doctools - #[list_begin enumerated] - - # oo::class create interface_sample1 { - # #*** !doctools - # #[enum] CLASS [class interface_sample1] - # #[list_begin definitions] - - # method test {arg1} { - # #*** !doctools - # #[call class::interface_sample1 [method test] [arg arg1]] - # #[para] test method - # puts "test: $arg1" - # } - - # #*** !doctools - # #[list_end] [comment {-- end definitions interface_sample1}] - # } - - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - } -} +#tcl::namespace::eval punk::lib::class { +# #*** !doctools +# #[subsection {Namespace punk::lib::class}] +# #[para] class definitions +# if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} { +# #*** !doctools +# #[list_begin enumerated] +# +# # oo::class create interface_sample1 { +# # #*** !doctools +# # #[enum] CLASS [class interface_sample1] +# # #[list_begin definitions] +# +# # method test {arg1} { +# # #*** !doctools +# # #[call class::interface_sample1 [method test] [arg arg1]] +# # #[para] test method +# # puts "test: $arg1" +# # } +# +# # #*** !doctools +# # #[list_end] [comment {-- end definitions interface_sample1}] +# # } +# +# #*** !doctools +# #[list_end] [comment {--- end class enumeration ---}] +# } +#} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::lib::ensemble { @@ -579,19 +579,53 @@ namespace eval punk::lib { proc pdict {args} { - set sep " [a+ Web-seagreen]=[a] " + if {[catch {package require punk::ansi} errM]} { + 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] { *proc -name pdict -help {Print dict keys,values to channel (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 dict variable" - patterns -type string -default "*" -multiple 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 + 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 segement 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. + + The pdict function operates on variable names - passing the value to the showdict function which operates on values + } }] #puts stderr "$argspec" set argd [punk::args::get_dict $argspec $args] @@ -621,20 +655,33 @@ namespace eval punk::lib { # - The current version is incomplete but passably usable. # - Copy proc and attempt rework so we can get back to this as a baseline for functionality proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value) - set sep " [a+ Web-seagreen]=[a] " - set argd [punk::args::get_dict [string map [list %sep% $sep] { - *id punk::lib::pdict - *proc -name punk::lib::pdict -help "display dictionary keys and values" + #set sep " [a+ Web-seagreen]=[a] " + if {[catch {package require punk::ansi} errM]} { + set sep " = " + set RST "" + set sep_mismatch " mismatch " + } else { + set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support + set RST [punk::ansi::a] + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch[punk::ansi::a] " + } + package require punk ;#we need pipeline pattern matching features + package require textblock + + set argd [punk::args::get_dict [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] { + *id punk::lib::showdict + *proc -name punk::lib::showdict -help "display dictionary keys and values" #todo - table tableobject -return -default "tailtohead" -choices {tailtohead sidebyside} -channel -default none -trimright -default 1 -type boolean -help "Trim whitespace off rhs of each line. This can help prevent a single long line that wraps in terminal from making every line wrap due to long rhs padding " - -separator -default "%sep%" -help "Separator column between keys and values" + -separator -default {%sep%} -help "Separator column between keys and values" + -separator_mismatch -default {%sep_mismatch%} -help "Separator to use when patterns mismatch" -roottype -default "dict" -help "list,dict,string" - -substructure -default {} -ansibase_keys -default "" -help "ansi list for each level in -substructure. e.g \[list \[a+ red\] \[a+ web-green\]\]" + -substructure -default {} -ansibase_values -default "" -keytemplates -default {${$key}} -type list -help "list of templates for keys at each level" -keysorttype -default "none" -choices {none dictionary ascii integer real} @@ -644,6 +691,7 @@ namespace eval punk::lib { patterns -default "*" -type string -multiple 1 -help "key or key glob pattern" }] $args] set opt_sep [dict get $argd opts -separator] + set opt_mismatch_sep [dict get $argd opts -separator_mismatch] set opt_keysorttype [dict get $argd opts -keysorttype] set opt_keysortdirection [dict get $argd opts -keysortdirection] set opt_trimright [dict get $argd opts -trimright] @@ -659,10 +707,40 @@ namespace eval punk::lib { set result "" + #pattern hierarchy + # */@1/@0,%#,%str @0/@1 - patterns each one is a pattern or pattern_nest + # * @1 @0,%#,%str - segments + # a b 1 0 %# %str - keys + set pattern_key_index [list] ;#list of pattern_nests, same length as number of keys generated set pattern_next_substructure [dict create] set pattern_this_structure [dict create] + # -- --- --- --- + #REVIEW + #as much as possible we should pass the indices along as a query to the pipeline pattern matching system so we're not duplicating the work and introducing inconsistencies. + #The main difference here is that sometimes we are treating the result as key-val pairs with the key being the query, other times the key is part of the query, or from the result itself (list/dict indices/keys). + #todo - determine if there is a more consistent rule-based way to do this rather than adhoc + #e.g pdict something * + #we want the keys from the result as individual lines on lhs + #e.g pdict something @@ + #we want on lhs result on rhs + # = v0 + #e.g pdict something @0-2,@4 + #we currently return: + #0 = v0 + #1 = v1 + #2 = v2 + #4 = v4 + #This means we've effectively auto-expanded the first list - elements 0-2. (or equivalently stated: we've flattened the 3 element and 1 element lists into one list of 4 elements) + #ie pdict is doing 'magic' compared to the normal pattern matching syntax, to make useage more convenient. + #this is a tradeoff that could create surprises and make things messy and/or inconsistent. + #todo - see if we can find a balance that gives consistency and logicality to the results whilst allowing still simplified matching syntax that is somewhat intuitive. + #It may be a matter of documenting what type of indexes are used directly as keys, and which return sets of further keys + #The solution for more consistency/predictability may involve being able to bracket some parts of the segment so for example we can apply an @join or %join within a segment + #that involves more complex pattern syntax & parsing (to be added to the main pipeline pattern syntax) + # -- --- --- --- + set filtered_keys [list] if {$opt_roottype in {dict list string}} { #puts "getting keys for roottype:$opt_roottype" @@ -671,176 +749,221 @@ namespace eval punk::lib { set re_idxdashidx {^([-+]{0,1}\d+|end[-+]{1}\d+|end)-([-+]{0,1}\d+|end[-+]{1}\d+|end)$} foreach pattern_nest $patterns { set keyset [list] - set pattern_nest_list [split $pattern_nest /] - set p [lindex $pattern_nest_list 0] - switch -exact -- $p { - * - "" { - if {$opt_roottype eq "list"} { - lappend keyset {*}[punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality - dict set pattern_this_structure $pattern_nest list - } elseif {$opt_roottype eq "dict"} { - lappend keyset {*}[dict keys $dval] - dict set pattern_this_structure $pattern_nest dict - } else { - lappend keyset %string - dict set pattern_this_structure $pattern_nest string - } - } - %# { - dict set pattern_this_structure $pattern_nest string - lappend keyset %# - } - # { - dict set pattern_this_structure $pattern_nest list - lappend keyset # - } - ## { - dict set pattern_this_structure $pattern_nest dict - lappend keyset [list ## query] - } - @* { - dict set pattern_this_structure $pattern_nest list - lappend keyset {*}[punk::lib::range 0 [llength $dval]-1] - } - @@ { - #get first k v from dict - dict set pattern_this_structure $pattern_nest dict - lappend keyset [list @@ query] - } - @*k@* - @*K@* { - #returns keys only - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - @*.@* { - lappend keyset {*}[dict keys $dval] - dict set pattern_this_structure $pattern_nest dict - } - default { - #puts stderr "===p:$p" - switch -glob -- $p { - {@k\*@*} - {@K\*@*} { - #value glob return keys - #set search [string range $p 4 end] - #dict for {k v} $dval { - # if {[string match $search $v]} { - # lappend keyset $k - # } - #} - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict + set keyset_structure [list] + + set segments [split $pattern_nest /] + set levelpatterns [lindex $segments 0] ;#possibly comma separated patterns + #we need to use _split_patterns to separate (e.g to protext commas that appear within quotes) + set patterninfo [punk::_split_patterns $levelpatterns] + #puts stderr "showdict-->_split_patterns: $patterninfo" + foreach v_idx $patterninfo { + lassign $v_idx v idx + #we don't support vars on lhs of index in this context - (because we support simplified glob patterns such as x* and literal dict keys such as kv which would otherwise be interpreted as vars with no index) + set p $v$idx ;#_split_patterns has split too far in this context - the entire pattern is the index pattern + switch -exact -- $p { + * - "" { + if {$opt_roottype eq "list"} { + set keys [punk::lib::range 0 [llength $dval]-1] ;#compat wrapper around subset of lseq functionality + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + dict set pattern_this_structure $p list + } elseif {$opt_roottype eq "dict"} { + set keys [dict keys $dval] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } else { + lappend keyset %string + lappend keyset_structure string + dict set pattern_this_structure $p string } - @@* { - #exact match key - review - should raise error to match punk pipe behaviour? - set k [string range $p 2 end] - if {[dict exists $dval $k]} { - lappend keyset $k + } + %# { + dict set pattern_this_structure $p string + lappend keyset %# + lappend keyset_structure string + } + # { + dict set pattern_this_structure $p list + lappend keyset # + lappend keyset_structure list + } + ## { + dict set pattern_this_structure $p dict + lappend keyset [list ## query] + lappend keyset_structure dict + } + @* { + puts ---->HERE<---- + dict set pattern_this_structure $p list + set keys [punk::lib::range 0 [llength $dval]-1] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + } + @@ { + #get first k v from dict + dict set pattern_this_structure $p dict + lappend keyset [list @@ query] + lappend keyset_structure dict + } + @*k@* - @*K@* { + #returns keys only + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + @*.@* { + set keys [dict keys $dval] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } + default { + #puts stderr "===p:$p" + #the basic scheme also doesn't allow commas in dict keys access via the convenience @@key - which isn't great, especially for arrays where it is common practice! + #we've already sacrificed whitespace in keys - so extra limitations should be reduced if it's to be passably useful + #@@"key,etc" should allow any non-whitespace key + switch -glob -- $p { + {@k\*@*} - {@K\*@*} { + #value glob return keys + #set search [string range $p 4 end] + #dict for {k v} $dval { + # if {[string match $search $v]} { + # lappend keyset $k + # } + #} + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict } - dict set pattern_this_structure $pattern_nest dict - } - @k@* - @K@* { - set k [string range $p 3 end] - if {[dict exists $dval $k]} { - lappend keyset $k + @@* { + #exact match key - review - should raise error to match punk pipe behaviour? + set k [string range $p 2 end] + if {[dict exists $dval $k]} { + lappend keyset $k + lappend keyset_structure dict + } + dict set pattern_this_structure $p dict } - dict set pattern_this_structure $pattern_nest dict - } - {@\*@*} { - #return list of values - #set k [string range $p 3 end] - #lappend keyset {*}[dict keys $dval $k] - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - {@\*.@*} { - set k [string range $p 4 end] - lappend keyset {*}[dict keys $dval $k] - dict set pattern_this_structure $pattern_nest dict - } - {@v\*@*} - {@V\*@*} { - #value-glob return value - #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - {@\*v@*} - {@\*V@*} { - #key-glob return value - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - {@\*@*} - {@\*v@*} - {@\*V@} { - #key glob return val - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - @??@* { - #exact key match - no error - lappend keyset [list $p query] - dict set pattern_this_structure $pattern_nest dict - } - default { - set this_type $opt_roottype - if {[string match @* $p]} { - #list mode - trim optional list specifier @ - set p [string range $p 1 end] - dict set pattern_this_structure $pattern_nest list - set this_type list - } elseif {[string match %* $p]} { - dict set pattern_this_structure $pattern_nest string - lappend keyset $p - set this_type string + @k@* - @K@* { + set k [string range $p 3 end] + if {[dict exists $dval $k]} { + lappend keyset $k + lappend keyset_structure dict + } + dict set pattern_this_structure $p dict } - if {$this_type eq "list"} { - dict set pattern_this_structure $pattern_nest list - if {[string is integer -strict $p]} { + {@\*@*} { + #return list of values + #set k [string range $p 3 end] + #lappend keyset {*}[dict keys $dval $k] + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*.@*} { + set k [string range $p 4 end] + set keys [dict keys $dval $k] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] + dict set pattern_this_structure $p dict + } + {@v\*@*} - {@V\*@*} { + #value-glob return value + #error "dict value-glob value-return only not supported here - bad pattern '$p' in '$pattern_nest'" + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*v@*} - {@\*V@*} { + #key-glob return value + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + {@\*@*} - {@\*v@*} - {@\*V@} { + #key glob return val + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + @??@* { + #exact key match - no error + lappend keyset [list $p query] + lappend keyset_structure dict + dict set pattern_this_structure $p dict + } + default { + set this_type $opt_roottype + if {[string match @* $p]} { + #list mode - trim optional list specifier @ + set p [string range $p 1 end] + dict set pattern_this_structure $p list + set this_type list + } elseif {[string match %* $p]} { + dict set pattern_this_structure $p string lappend keyset $p - } elseif {[string match "?*-?*" $p]} { - #could be either - don't change type - #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers - #now we should map _ to "" first - set p [string map {_ {}} $p] - #lassign [textutil::split::splitx $p {\.\.}] a b - if {![regexp $re_idxdashidx $p _match a b]} { - error "unrecognised pattern $p" - } - set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high - #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds - if {${lower_resolve} == -1} { - #lower bound is above upper list range - #match with decreasing indices is still possible - set lower [expr {[llength $dval]-1}] ;#set to max - } elseif {$lower_resolve == -2} { - set lower 0 - } else { - set lower $lower_resolve - } - set upper [punk::lib::lindex_resolve $dval $b] - if {$upper == -2} { - #upper bound is below list range - - if {$lower_resolve >=-1} { - set upper 0 + lappend keyset_structure string + set this_type string + } + if {$this_type eq "list"} { + dict set pattern_this_structure $p list + if {[string is integer -strict $p]} { + lappend keyset $p + lappend keyset_structure list + } elseif {[string match "?*-?*" $p]} { + #could be either - don't change type + #list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers + #now we should map _ to "" first + set p [string map {_ {}} $p] + #lassign [textutil::split::splitx $p {\.\.}] a b + if {![regexp $re_idxdashidx $p _match a b]} { + error "unrecognised pattern $p" + } + set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high + #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds + if {${lower_resolve} == -1} { + #lower bound is above upper list range + #match with decreasing indices is still possible + set lower [expr {[llength $dval]-1}] ;#set to max + } elseif {$lower_resolve == -2} { + set lower 0 } else { - continue + set lower $lower_resolve + } + set upper [punk::lib::lindex_resolve $dval $b] + if {$upper == -2} { + #upper bound is below list range - + if {$lower_resolve >=-1} { + set upper 0 + } else { + continue + } + } elseif {$upper == -1} { + #use max + set upper [expr {[llength $dval]-1}] + #assert - upper >=0 because we have ruled out empty lists } - } elseif {$upper == -1} { - #use max - set upper [expr {[llength $dval]-1}] - #assert - upper >=0 because we have ruled out empty lists - } - #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order - lappend keyset {*}[punk::lib::range $lower $upper] + #note lower can legitimately be higher than upper - lib::range, like lseq can produce sequence in reverse order + set keys [punk::lib::range $lower $upper] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] list] + } else { + lappend keyset [list @$p query] + lappend keyset_structure list + } + } elseif {$this_type eq "string"} { + dict set pattern_this_structure $p string + } elseif {$this_type eq "dict"} { + #default equivalent to @\*@* + dict set pattern_this_structure $p dict + #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" + set keys [dict keys $dval $p] + lappend keyset {*}$keys + lappend keyset_structure {*}[lrepeat [llength $keys] dict] } else { - lappend keyset [list @$p query] - } - } elseif {$this_type eq "string"} { - dict set pattern_this_structure $pattern_nest string - } elseif {$this_type eq "dict"} { - #default equivalent to @\*@* - dict set pattern_this_structure $pattern_nest dict - #puts "dict: appending keys from index '$p' keys: [dict keys $dval $p]" - lappend keyset {*}[dict keys $dval $p] - } else { - puts stderr "list: unrecognised pattern $p" + puts stderr "list: unrecognised pattern $p" + } } } } @@ -848,48 +971,61 @@ namespace eval punk::lib { } # -- --- --- --- - #check next pattern for substructure type to use + #check next pattern-segment for substructure type to use # -- --- --- --- set substructure "" - set pnext [lindex $pattern_nest_list 1] - switch -exact $pnext { - "" { - set substructure string - } - @*k@* - @*K@* - @*.@* - ## { - set substructure dict - } - # { - set substructure list - } - ## { - set substructure dict - } - %# { - set substructure string - } - * { - #set substructure $opt_roottype - set substructure [dict get $pattern_this_structure $pattern_nest] - } - default { - switch -glob -- $pnext { - @??@* - @?@* - @@* { - #all 4 or 3 len prefixes bounded by @ are dict - set substructure dict - } - default { - if {[string match @* $pnext]} { - set substructure list - } elseif {[string match %* $pnext]} { - set substructure string - } else { - #set substructure $opt_roottype - set substructure [dict get $pattern_this_structure $pattern_nest] + set pnext [lindex $segments 1] + set patterninfo [punk::_split_patterns $levelpatterns] + if {[llength $patterninfo] == 0} { + # // ? -review - what does this mean? for xpath this would mean at any level + set substructure [lindex $pattern_this_structure end] + } elseif {[llength $patterninfo] == 1} { + # single type in segment e.g /@@something/ + switch -exact $pnext { + "" { + set substructure string + } + @*k@* - @*K@* - @*.@* - ## { + set substructure dict + } + # { + set substructure list + } + ## { + set substructure dict + } + %# { + set substructure string + } + * { + #set substructure $opt_roottype + #set substructure [dict get $pattern_this_structure $pattern_nest] + set substructure [lindex $pattern_this_structure end] + } + default { + switch -glob -- $pnext { + @??@* - @?@* - @@* { + #all 4 or 3 len prefixes bounded by @ are dict + set substructure dict + } + default { + if {[string match @* $pnext]} { + set substructure list + } elseif {[string match %* $pnext]} { + set substructure string + } else { + #set substructure $opt_roottype + #set substructure [dict get $pattern_this_structure $pattern_nest] + set substructure [lindex $pattern_this_structure end] + } } } } } + } else { + #e.g /@0,%str,.../ + #doesn't matter what the individual types are - we have a list result + set substructure list } #puts "--pattern_nest: $pattern_nest substructure: $substructure" dict set pattern_next_substructure $pattern_nest $substructure @@ -904,10 +1040,14 @@ namespace eval punk::lib { } } if {$int_keyset} { - set keyset [lsort -integer $keyset] + set sortindices [lsort -indices -integer $keyset] + #set keyset [lsort -integer $keyset] } else { - set keyset [lsort -$opt_keysorttype $keyset] + #set keyset [lsort -$opt_keysorttype $keyset] + set sortindices [lsort -indices -$opt_keysorttype $keyset] } + set keyset [lmap i $sortindices {lindex $keyset $i}] + set keyset_structure [lmap i $sortindices {lindex $keyset_structure $i}] } foreach k $keyset { @@ -915,6 +1055,7 @@ namespace eval punk::lib { } lappend filtered_keys {*}$keyset + lappend all_keyset_structure {*}$keyset_structure #puts stderr "--->pattern_nest:$pattern_nest keyset:$keyset" } @@ -929,7 +1070,6 @@ namespace eval punk::lib { #both keys and values could have newline characters. #simple use of 'format' won't cut it for more complex dict keys/values #use block::width or our columns won't align in some cases - set RST [a] switch -- $opt_return { "tailtohead" { #last line of key is side by side (possibly with separator) with first line of value @@ -945,12 +1085,16 @@ namespace eval punk::lib { set maxl [::tcl::mathfunc::max {*}[lmap v $display_keys {textblock::width $v}]] set kidx 0 + set last_hidekey 0 foreach keydisplay $display_keys key $filtered_keys { + set thisval "?" set hidekey 0 set pattern_nest [lindex $pattern_key_index $kidx] set pattern_nest_list [split $pattern_nest /] - #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest" - set this_type [dict get $pattern_this_structure $pattern_nest] + #set this_type [dict get $pattern_this_structure $pattern_nest] + #set this_type [dict get $pattern_this_structure $key] + set this_type [lindex $all_keyset_structure $kidx] + #puts stderr "---> kidx:$kidx key:$key - pattern_nest:$pattern_nest this_type:$this_type" set is_match 1 ;#whether to display the normal separator or bad-match separator switch -- $this_type { @@ -1030,7 +1174,7 @@ namespace eval punk::lib { } } string { - set hidekey 0 + set hidekey 1 if {$key eq "%string"} { set hidekey 1 set thisval $dval @@ -1043,11 +1187,21 @@ namespace eval punk::lib { lassign [split $key -] _ extra set width [expr {[textblock::width $dval] + $extra}] set thisval [textblock::pad $dval -which left -width $width] + } elseif {[string match *lpadstr-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] + set thisval [textblock::pad $dval -which left -width $width -padchar $extra] } elseif {[string match *rpad-* $key]} { set hidekey 1 lassign [split $key -] _ extra set width [expr {[textblock::width $dval] + $extra}] set thisval [textblock::pad $dval -which right -width $width] + } elseif {[string match *rpadstr-* $key]} { + set hidekey 1 + lassign [split $key -] _ extra + set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}] + set thisval [textblock::pad $dval -which right -width $width -padchar $extra] } else { if {[lindex $key 1] eq "query"} { set qry [lindex $key 0] @@ -1082,7 +1236,9 @@ namespace eval punk::lib { lassign [textblock::size $thisval] _vw vwidth _vh vheight #set blanks_above [string repeat \n [expr {$kheight -1}]] set vblock $opt_ansibase_values$thisval$RST - append result [textblock::join_basic -- $vblock] \n + #append result [textblock::join_basic -- $vblock] + #review - we wouldn't need this space if we had a literal %sp %sp-x ?? + append result " $vblock" } else { set ansibase_key [lindex $opt_ansibase_keys 0] @@ -1096,7 +1252,7 @@ namespace eval punk::lib { if {$is_match} { set use_sep $opt_sep } else { - set use_sep " [a+ Web-red undercurly underline undert-white]mismatch[a] " + set use_sep $opt_mismatch_sep } @@ -1105,8 +1261,12 @@ namespace eval punk::lib { set sblock [textblock::pad $blanks_above$use_sep$blanks_below -width $sepwidth] set vblock $blanks_above$opt_ansibase_values$thisval$RST #only vblock is ragged - we can do a basic join because we don't care about rhs whitespace + if {$last_hidekey} { + append result \n + } append result [textblock::join_basic -- $kblock $sblock $vblock] \n } + set last_hidekey $hidekey incr kidx } } diff --git a/src/modules/punk/mix/base-0.1.tm b/src/modules/punk/mix/base-0.1.tm index 6eec4d8d..8a4456d1 100644 --- a/src/modules/punk/mix/base-0.1.tm +++ b/src/modules/punk/mix/base-0.1.tm @@ -351,8 +351,14 @@ namespace eval punk::mix::base { continue } set testfolder [file join $candidate src $sub] - set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm] - if {[llength $tmfiles]} { + #ensure that if src/modules exists - it is always included even if empty + if {[string tolower $sub] eq "modules"} { + lappend tm_folders $testfolder + continue + } + #set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm] + #set podfolders [glob -nocomplain -dir $testfolder -type d -tail #modpod-*] + if {[llength [glob -nocomplain -dir $testfolder -type f -tail *.tm]] || [llength [glob -nocomplain -dir $testfolder -type d -tail #modpod-*]]} { lappend tm_folders $testfolder } } @@ -428,9 +434,10 @@ namespace eval punk::mix::base { } #crc::cksum is extremely slow in tcllib as at 2023 e.g 20x slower (no c implementation?) + # - try builtin zlib crc instead? #sha1 is performant - and this is not being used in a cryptographic or adversarial context - so performance and practical unlikelihood of accidental collisions should be the main consideration. #adler32 is fastest for some larger files of a few MB but slower on small files (possibly due to Tcl-based file load?) - #sha1 as at 2023 seems a good default + #sha1 as at 2023 seems a reasonable default proc cksum_algorithms {} { variable sha3_implementation #sha2 is an alias for sha256 @@ -459,10 +466,16 @@ namespace eval punk::mix::base { #adler32 via file-slurp proc cksum_adler32_file {filename} { package require zlib; #should be builtin anyway - set data [punk::mix::util::fcat -translation binary $filename] + set data [punk::mix::util::fcat -translation binary -encoding iso8859-1 $filename] #set data [fileutil::cat -translation binary $filename] ;#no significant performance diff on windows - and doesn't handle win-illegal names zlib adler32 $data } + #zlib crc vie file-slurp + proc cksum_crc_file {filename} { + package require zlib + set data [punk::mix::util::fcat -translation binary -encoding iso8859-1 $filename] + zlib crc $data + } #required to be able to accept relative paths @@ -614,6 +627,9 @@ namespace eval punk::mix::base { package require cksum ;#tcllib set cksum_command [list crc::cksum -format 0x%X -file] } + crc { + set cksum_command [list cksum_crc_file] + } adler32 { set cksum_command [list cksum_adler32_file] } diff --git a/src/modules/punk/mix/cli-999999.0a1.0.tm b/src/modules/punk/mix/cli-999999.0a1.0.tm new file mode 100644 index 00000000..949de9cb --- /dev/null +++ b/src/modules/punk/mix/cli-999999.0a1.0.tm @@ -0,0 +1,1119 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'deck make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2023 +# +# @@ Meta Begin +# Application punk::mix::cli 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz +package require punk::repo +package require punk::ansi +package require punkcheck ;#checksum and/or timestamp records + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#review +#deck - rename to dev +namespace eval punk::mix::cli { + namespace eval temp_import { + } + namespace ensemble create + + package require punk::overlay + catch { + punk::overlay::import_commandset module . ::punk::mix::commandset::module + } + punk::overlay::import_commandset debug . ::punk::mix::commandset::debug + punk::overlay::import_commandset repo . ::punk::mix::commandset::repo + punk::overlay::import_commandset lib . ::punk::mix::commandset::loadedlib + + catch { + package require punk::mix::commandset::project + punk::overlay::import_commandset project . ::punk::mix::commandset::project + punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection + } + if {[catch { + package require punk::mix::commandset::layout + punk::overlay::import_commandset project.layout . ::punk::mix::commandset::layout + punk::overlay::import_commandset project.layouts . ::punk::mix::commandset::layout::collection + } errM]} { + puts stderr "error loading punk::mix::commandset::layout" + puts stderr $errM + } + if {[catch { + package require punk::mix::commandset::buildsuite + punk::overlay::import_commandset buildsuite . ::punk::mix::commandset::buildsuite + punk::overlay::import_commandset buildsuites . ::punk::mix::commandset::buildsuite::collection + } errM]} { + puts stderr "error loading punk::mix::commandset::buildsuite" + puts stderr $errM + } + punk::overlay::import_commandset scriptwrap . ::punk::mix::commandset::scriptwrap + if {[catch { + package require punk::mix::commandset::doc + punk::overlay::import_commandset doc . ::punk::mix::commandset::doc + punk::overlay::import_commandset "" "" ::punk::mix::commandset::doc::collection + } errM]} { + puts stderr "error loading punk::mix::commandset::doc" + puts stderr $errM + } + + + proc help {args} { + #set basehelp [punk::mix::base::help -extension [namespace current] {*}$args] + set basehelp [punk::mix::base help {*}$args] + #puts stdout "punk::mix help" + return $basehelp + } + + proc stat {{workingdir ""} args} { + dict set args -v 0 + punk::mix::cli::lib::get_status $workingdir {*}$args + } + proc status {{workingdir ""} args} { + dict set args -v 1 + punk::mix::cli::lib::get_status $workingdir {*}$args + } + + + + + + + +} + + +namespace eval punk::mix::cli { + + + #interp alias {} ::punk::mix::cli::project.new {} ::punk::mix::cli::new + + + proc make {args} { + set startdir [pwd] + set project_base "" ;#empty for unknown + if {[punk::repo::is_git $startdir]} { + set project_base [punk::repo::find_git] + set sourcefolder $project_base/src + } elseif {[punk::repo::is_fossil $startdir]} { + set project_base [punk::repo::find_fossil] + set sourcefolder $project_base/src + } else { + if {[punk::repo::is_candidate $startdir]} { + set project_base [punk::repo::find_candidate] + set sourcefolder $project_base/src + puts stderr "WARNING - project not under git or fossil control" + puts stderr "Using base folder $project_base" + } else { + set sourcefolder $startdir + } + } + + #review - why can't we be anywhere in the project? + #also - if no make.tcl - can we use the running shell's make.tcl ? (after prompting user?) + if {([file tail $sourcefolder] ne "src") || (![file exists $sourcefolder/make.tcl])} { + puts stderr "dev make must be run from src folder containing make.tcl - unable to proceed (cwd: [pwd])" + if {[string length $project_base]} { + if {[file exists $project_base/src] && [string tolower [pwd]] ne [string tolower $project_base/src]} { + puts stderr "Try cd to $project_base/src" + } + } else { + if {[file exists $startdir/Makefile]} { + puts stdout "A Makefile exists at $startdir/Makefile." + if {"windows" eq $::tcl_platform(platform)} { + puts stdout "Try running: msys2 -ucrt64 -here -c \"make build\" or bash -c \"make build\"" + } else { + puts stdout "Try runing: make build" + } + } + } + return false + } + + if {![string length $project_base]} { + puts stderr "WARNING no git or fossil repository detected." + puts stderr "Using base folder $startdir" + set project_base $startdir + } + + set lc_this_exe [string tolower [info nameofexecutable]] + set lc_proj_bin [string tolower $project_base/bin] + set lc_build_bin [string tolower $project_base/src/_build] + + if {"project" in $args} { + set is_own_exe 0 + if {[string match "${lc_proj_bin}*" $lc_this_exe] || [string match "${lc_build_bin}" $lc_this_exe]} { + set is_own_exe 1 + puts stderr "WARNING - running make using executable that may be created by the project being built" + set answer [util::askuser "Do you want to proceed using this executable? (build will probably stop when it is unable to update the executable) Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -confirm 0 to avoid prompts." + return + } + } + } + cd $sourcefolder + #use run so that stdout visible as it goes + if {![catch {run --timeout=55000 -debug [info nameofexecutable] $sourcefolder/make.tcl {*}$args} exitinfo]} { + #todo - notify if exit because of timeout! + puts stderr "exitinfo: $exitinfo" + set exitcode [dict get $exitinfo exitcode] + } else { + puts stderr "Error unable to determine exitcode. err: $exitinfo" + cd $startdir + return false + } + + cd $startdir + if {$exitcode != 0} { + puts stderr "FAILED with exitcode $exitcode" + return false + } else { + puts stdout "OK make finished " + return true + } + } + + proc Kettle {args} { + tailcall lib::kettle_call lib {*}$args + } + proc KettleShell {args} { + tailcall lib::kettle_call shell {*}$args + } + + + + namespace eval lib { + namespace path ::punk::mix::util + + + proc module_types {} { + #first in list is default for unspecified -type when creating new module + #return [list plain tarjar zipkit] + return [list plain tarjar zip] + } + + proc validate_modulename {modulename args} { + set opts [list\ + -errorprefix validate_modulename\ + ] + if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"} + foreach {k v} $args { + switch -- $k { + -errorprefix { + dict set opts $k $v + } + default { + error "validate_modulename error: unknown option '$k'. known options: [dict keys $opts]" + } + } + } + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set opt_errorprefix [dict get $opts -errorprefix] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + + validate_name_not_empty_or_spaced $modulename -errorprefix $opt_errorprefix + set testname [string map {:: {}} $modulename] + if {[string first : $testname] >=0} { + error "$opt_errorprefix '$modulename' can only contain paired colons" + } + set badchars [list - "$" "?" "*"] + foreach bc $badchars { + if {[string first $bc $modulename] >= 0} { + error "$opt_errorprefix '$modulename' can not contain character '$bc'" + } + } + return $modulename + } + + proc validate_projectname {projectname args} { + set defaults [list\ + -errorprefix projectname\ + ] + if {[llength $args] %2 != 0} {error "validate_modulename 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" + } + } + set opts [dict merge $defaults $args] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set opt_errorprefix [dict get $opts -errorprefix] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + validate_name_not_empty_or_spaced $projectname -errorprefix $opt_errorprefix + set reserved_words [list etc lib bin modules src doc vendorlib vendormodules embedded runtime _aside _build] + if {$projectname in $reserved_words } { + error "$opt_errorprefix '$projectname' cannot be one of reserved_words: $reserved_words" + } + if {[string first "::" $projectname] >= 0} { + error "$opt_errorprefix '$projectname' cannot contain namespace separator '::'" + } + return $projectname + } + proc validate_name_not_empty_or_spaced {name args} { + set opts [list\ + -errorprefix projectname\ + ] + if {[llength $args] %2 != 0} {error "validate_name_not_empty_or_spaced args must be name-value pairs: received '$args'"} + foreach {k v} $args { + switch -- $k { + -errorprefix { + dict set opts $k $v + } + default { + error "validate_name_not_empty_or_spaced error: unknown option $k. known options: [dict keys $opts]" + } + } + } + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set opt_errorprefix [dict get $opts -errorprefix] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + if {![string length $name]} { + error "$opt_errorprefix cannot be empty" + } + if {[string length [string map [list " " "" \n "" \r "" \t ""] $name]] != [string length $name]} { + error "$opt_errorprefix cannot contain whitespace" + } + 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 {modulename} { + set lastpart [namespace tail $modulename] + 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 $modulename] ".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 "" + } + return [list $namesegment $versionsegment] + } + + proc get_status {{workingdir ""} args} { + set result "" + if {$workingdir ne ""} { + if {[file pathtype $workingdir] ne "absolute"} { + set workingdir [file normalize $workingdir] + } + set active_dir $workingdir + } else { + set active_dir [pwd] + } + set defaults [dict create\ + -v 1\ + ] + set opts [dict merge $defaults $args] + # -- --- --- --- --- --- --- --- --- + set opt_v [dict get $opts -v] + # -- --- --- --- --- --- --- --- --- + + + set repopaths [punk::repo::find_repos [pwd]] + set repos [dict get $repopaths repos] + if {![llength $repos]} { + append result [dict get $repopaths warnings] + } else { + append result [dict get $repopaths warnings] + lassign [lindex $repos 0] repopath repotypes + if {"fossil" in $repotypes} { + #review - multiple process launches to fossil a bit slow on windows.. + #could we query global db in one go instead? + # + set fossil_prog [auto_execok fossil] + append result "FOSSIL project based at $repopath with revision: [punk::repo::fossil_revision $repopath]" \n + set fosinfo [exec {*}$fossil_prog info] + append result [join [punk::repo::grep {repository:*} $fosinfo] \n] \n + + set fosrem [exec {*}$fossil_prog remote ls] + if {[string length $fosrem]} { + append result "Remotes:\n" + append result " " $fosrem \n + } + + + append result [join [punk::repo::grep {tags:*} $fosinfo] \n] \n + + set dbinfo [exec {*}$fossil_prog dbstat] + append result [join [punk::repo::grep {project-name:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {tickets:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {project-age:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {latest-change:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {files:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {check-ins:*} $dbinfo] \n] \n + if {"project" in $repotypes} { + #punk project + if {![catch {package require textblock; package require patternpunk}]} { + set result [textblock::join -- [>punk . logo] " " $result] + append result \n + } + } + + set timeline [exec fossil timeline -n 5 -t ci] + set timeline [string map {\r\n \n} $timeline] + append result $timeline + if {$opt_v} { + set repostate [punk::repo::workingdir_state $repopath -repopaths $repopaths -repotypes fossil] + append result \n [punk::repo::workingdir_state_summary $repostate] + } + + } + #repotypes *could* be both git and fossil - so report both if so + if {"git" in $repotypes} { + append result "GIT project based at $repopath with revision: [punk::repo::git_revision $repopath]" \n + if {[string length [set git_prog [auto_execok git]]]} { + set git_remotes [exec {*}$git_prog remote -v] + append result $git_remotes + if {$opt_v} { + set repostate [punk::repo::workingdir_state $repopath -repopaths $repopaths -repotypes git] + append result \n [punk::repo::workingdir_state_summary $repostate] + } + } + } + + } + + return $result + } + + + proc build_modules_from_source_to_base {srcdir basedir args} { + set antidir [list "#*" "_build" "_aside" ".git" ".fossil*"] ;#exact or glob patterns for folders (at any level) we don't want to search in or copy. + set defaults [list\ + -installer punk::mix::cli::build_modules_from_source_to_base\ + -call-depth-internal 0\ + -max_depth 1000\ + -subdirlist {}\ + -punkcheck_eventobj "\uFFFF"\ + -glob *.tm\ + -podglob #modpod-*\ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set installername [dict get $opts -installer] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set CALLDEPTH [dict get $opts -call-depth-internal] + set max_depth [dict get $opts -max_depth] + set subdirlist [dict get $opts -subdirlist] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set fileglob [dict get $opts -glob] + set podglob [dict get $opts -podglob] + if {![string match "*.tm" $fileglob]} { + error "build_modules_from_source_to_base -glob '$fileglob' doesn't seem to target tcl modules." + } + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set opt_punkcheck_eventobj [dict get $opts -punkcheck_eventobj] + + set magicversion [punk::mix::util::magic_tm_version] ;#deliberately large so given load-preference when testing + set module_list [list] + + if {[file tail [file dirname $srcdir]] ne "src"} { + puts stderr "ERROR build_modules_from_source_to_base can only be called with a srcdir that is a subfolder of your 'src' directory" + puts stderr "The .tm modules are namespaced based on their directory depth - so we need to start at the root" + puts stderr "To build a subtree of your modules - use an appropriate src/modules folder and pass in the -subdirlist." + puts stderr "e.g if your modules are based at /x/src/modules2 and you wish to build only the .tm files at /x/src/modules2/skunkworks/lib" + puts stderr "Use: >build_modules_from_source_to_base /x/src/modules2 /x/modules2 -subdirlist {skunkworks lib}" + exit 2 + } + set srcdirname [file tail $srcdir] + + set build [file dirname $srcdir]/_build/$srcdirname ;#relative to *original* srcdir - not current_source_dir + if {[llength $subdirlist] == 0} { + set target_module_dir $basedir + set current_source_dir $srcdir + } else { + set target_module_dir $basedir/[file join {*}$subdirlist] + set current_source_dir $srcdir/[file join {*}$subdirlist] + } + if {![file exists $target_module_dir]} { + error "build_modules_from_source_to_base from current source dir: '$current_source_dir'. Basedir:'$current_module_dir' doesn't exist or is empty" + } + if {![file exists $current_source_dir]} { + error "build_modules_from_source_to_base from current source dir:'$current_source_dir' doesn't exist or is empty" + } + + #---------------------------------------- + set punkcheck_file [file join $basedir/.punkcheck] + if {$CALLDEPTH == 0} { + + set config [dict create\ + -glob $fileglob\ + -max_depth 0\ + ] + #lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list + # -- --- + set installer [punkcheck::installtrack new $installername $punkcheck_file] + $installer set_source_target $srcdir $basedir + set event [$installer start_event $config] + # -- --- + + } else { + set event $opt_punkcheck_eventobj + } + #---------------------------------------- + + + set process_modules [dict create] + #put pods first in processing order + set src_pods [glob -nocomplain -dir $current_source_dir -type d -tail $podglob] + foreach podpath $src_pods { + dict set process_modules $podpath [dict create -type pod] + } + set src_modules [glob -nocomplain -dir $current_source_dir -type f -tail $fileglob] + foreach modulepath $src_modules { + dict set process_modules $modulepath [dict create -type file] + } + + set did_skip 0 ;#flag for stdout/stderr formatting only + dict for {modpath modinfo} $process_modules { + set modtype [dict get $modinfo -type] + + set is_interesting 0 + if {[string match "foobar" $current_source_dir]} { + set is_interesting 1 + } + if {$is_interesting} { + puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath" + } + set fileparts [split [file rootname $modpath] -] + #set tmfile_versionsegment [lindex $fileparts end] + lassign [split_modulename_version $modpath] basename tmfile_versionsegment + if {$tmfile_versionsegment eq ""} { + #split_modulename_version version part will be empty if not valid tcl version + #last segment doesn't look even slightly versiony - fail. + puts stderr "ERROR: Unable to confirm file $current_source_dir/$modpath is a reasonably versioned .tm module - ABORTING." + exit 1 + } + switch -- $modtype { + pod { + #basename still contains leading #modpod- + if {[string match #modpod-* $basename]} { + set basename [string range $basename 8 end] + } else { + error "build_modules_from_source_to_base, pod, unexpected basename $basename" ;#shouldn't be possible with default podglob - review - why is podglob configurable? + } + set versionfile $current_source_dir/$basename-buildversion.txt ;#needs to be added in targetset_addsource to trigger rebuild if changed (only when magicversion in use) + if {$tmfile_versionsegment eq $magicversion} { + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + } else { + set module_build_version $tmfile_versionsegment + } + + set buildfolder $current_source_dir/_build + file mkdir $buildfolder + # -- --- + set config [dict create\ + -glob *\ + -max_depth 100\ + ] + # -max_depth -1 for no limit + set build_installername pods_in_$current_source_dir + set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck] + $build_installer set_source_target $current_source_dir/$modpath $buildfolder + set build_event [$build_installer start_event $config] + # -- --- + set podtree_copy $buildfolder/#modpod-$basename-$module_build_version + set modulefile $buildfolder/$basename-$module_build_version.tm + + + $build_event targetset_init INSTALL $podtree_copy + $build_event targetset_addsource $current_source_dir/$modpath + if {$tmfile_versionsegment eq $magicversion} { + $build_event targetset_addsource $versionfile + } + if {\ + [llength [dict get [$build_event targetset_source_changes] changed]]\ + || [llength [$build_event get_targets_exist]] < [llength [$build_event get_targets]]\ + } { + $build_event targetset_started + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + + set delete_failed 0 + if {[file exists $buildfolder/]} { + puts stderr "deleting existing _build copy at $podtree_copy" + if {[catch { + file delete -force $podtree_copy + } errMsg]} { + puts stderr "[punk::ansi::a+ red]deletion of _build copy at $podtree_copy failed: $errMsg[punk::ansi::a]" + set delete_failed 1 + } + } + if {!$delete_failed} { + puts stdout "copying.." + puts stdout "$current_source_dir/$modpath" + puts stdout "to:" + puts stdout "$podtree_copy" + file copy $current_source_dir/$modpath $podtree_copy + if {$tmfile_versionsegment eq $magicversion} { + set tmfile $buildfolder/#modpod-$basename-$module_build_version/$basename-$magicversion.tm + if {[file exists $tmfile]} { + set newname $buildfolder/#modpod-$basename-$module_build_version/$basename-$module_build_version.tm + file rename $tmfile $newname + set tmfile $newname + } + set fd [open $tmfile r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $tmfile w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + } + #delete and regenerate zip and modpod stubbed zip + set had_error 0 + set notes [list] + if {[catch { + file delete $buildfolder/$basename-$module_build_version.zip + } err] } { + set had_error 1 + lappend notes "zip_delete_failed" + } + if {[catch { + file delete $buildfolder/$basename-$module_build_version.tm + } err]} { + set had_error 1 + lappend notes "tm_delete_failed" + } + #create ordinary zip file without using external executable + package require punk::zip + set zipfile $buildfolder/$basename-$module_build_version.zip ;#ordinary zip file (deflate) + + if 0 { + #use -base $buildfolder so that -directory is included in the archive - the modpod stub relies on this - and extraction would be potentially messy otherwise + punk::zip::mkzip -base $buildfolder -directory $buildfolder/#modpod-$basename-$module_build_version -- $zipfile * + #punk::zip::mkzip stores permissions - (unix style) which confuses zipfs when reading - it misidentifies dirs as files + } + #zipfs mkzip does exactly what we need anyway in this case + set wd [pwd] + cd $buildfolder + puts "zipfs mkzip $zipfile #modpod-$basename-$module_build_version" + zipfs mkzip $zipfile #modpod-$basename-$module_build_version + cd $wd + + package require modpod + modpod::lib::make_zip_modpod $zipfile $modulefile + + + if {$had_error} { + $build_event targetset_end FAILED -note [join $notes ,] + } else { + # -- ---------- + $build_event targetset_end OK + # -- ---------- + } + } else { + $build_event targetset_end FAILED -note "could not delete $podtree_copy" + } + + } else { + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $build_event targetset_end SKIPPED + } + $build_event destroy + $build_installer destroy + + $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm + $event targetset_addsource $modulefile + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + $event targetset_started + # -- --- --- --- --- --- + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + lappend module_list $modulefile + file copy -force $modulefile $target_module_dir + puts stderr "Copied zip modpod module $modulefile to $target_module_dir" + # -- --- --- --- --- --- + $event targetset_end OK -note "zip modpod" + } else { + puts -nonewline stderr "." + set did_skip 1 + if {$is_interesting} { + puts stderr "$modulefile [$event targetset_source_changes]" + } + $event targetset_end SKIPPED + } + } + tarjar { + #basename may still contain #tarjar- + #to be obsoleted - update modpod to (optionally) use vfs::tar + } + file { + set m $modpath + if {$tmfile_versionsegment eq $magicversion} { + #set basename [join [lrange $fileparts 0 end-1] -] + set versionfile $current_source_dir/$basename-buildversion.txt + set versionfiledata "" + if {![file exists $versionfile]} { + puts stderr "\nWARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning\n" + set module_build_version "0.1" + } else { + set fd [open $versionfile r] + set versionfiledata [read $fd]; close $fd + set ln0 [lindex [split $versionfiledata \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![util::is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 + } + + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { + #rebuild the .tm from the #tarjar + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { + + } else { + + } + #REVIEW - should be in same structure/depth as $target_module_dir in _build? + + #TODO + set buildfolder $current_sourcedir/_build + file mkdir $buildfolder + + set tmfile $buildfolder/$basename-$module_build_version.tm + file delete -force $buildfolder/#tarjar-$basename-$module_build_version + file delete -force $tmfile + + + file copy -force $current_source_dir/#tarjar-$basename-$magicversion $buildfolder/#tarjar-$basename-$module_build_version + # + #bsdtar doesn't seem to work.. or I haven't worked out the right options? + #exec tar -cvf $buildfolder/$basename-$module_build_version.tm $buildfolder/#tarjar-$basename-$module_build_version + package require tar + tar::create $tmfile $buildfolder/#tarjar-$basename-$module_build_version + if {![file exists $tmfile]} { + puts stdout "ERROR: failed to build tarjar file $tmfile" + exit 4 + } + #copy the file? + #set target $target_module_dir/$basename-$module_build_version.tm + #file copy -force $tmfile $target + + lappend module_list $tmfile + } else { + #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. + if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { + puts stderr "\nWarning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" + } + + #------------------------------ + # + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$basename-$module_build_version.tm] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] + $event targetset_init INSTALL $target_module_dir/$basename-$module_build_version.tm + $event targetset_addsource $versionfile + $event targetset_addsource $current_source_dir/$m + + #set changed_list [list] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $versionfile] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] + #set changed_list [dict get $changed_unchanged changed] + + + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + #set file_record [punkcheck::installfile_started_install $basedir $file_record] + $event targetset_started + # -- --- --- --- --- --- + set target $target_module_dir/$basename-$module_build_version.tm + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" + set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + set data [string map [list $magicversion $module_build_version] $data] + set fdout [open $target w] + fconfigure $fdout -translation binary + puts -nonewline $fdout $data + close $fdout + #file copy -force $srcdir/$m $target + lappend module_list $target + # -- --- --- --- --- --- + #set file_record [punkcheck::installfile_finished_install $basedir $file_record] + $event targetset_end OK + } else { + if {$is_interesting} { + puts stdout "skipping module $current_source_dir/$m - no change in sources detected" + } + puts -nonewline stderr "." + set did_skip 1 + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $event targetset_end SKIPPED + } + + #------------------------------ + + } + + continue + } + ##------------------------------ + ## + #set target_relpath [punkcheck::lib::path_relative $basedir $target_module_dir/$m] + #set file_record [punkcheck::installfile_begin $basedir $target_relpath $installername -eventid $punkcheck_eventid] + #set changed_list [list] + ## -- --- --- --- --- --- + #set source_relpath [punkcheck::lib::path_relative $basedir $current_source_dir/$m] + #set file_record [punkcheck::installfile_add_source_and_fetch_metadata $basedir $source_relpath $file_record] + ## -- --- --- --- --- --- + #set changed_unchanged [punkcheck::recordlist::file_install_record_source_changes [lindex [dict get $file_record body] end]] + #set changed_list [dict get $changed_unchanged changed] + #---------- + $event targetset_init INSTALL $target_module_dir/$m + $event targetset_addsource $current_source_dir/$m + if {\ + [llength [dict get [$event targetset_source_changes] changed]]\ + || [llength [$event get_targets_exist]] < [llength [$event get_targets]]\ + } { + + #set file_record [punkcheck::installfile_started_install $basedir $file_record] + $event targetset_started + # -- --- --- --- --- --- + if {$did_skip} {set did_skip 0; puts -nonewline stdout \n} + lappend module_list $current_source_dir/$m + file copy -force $current_source_dir/$m $target_module_dir + puts stderr "Copied already versioned module $current_source_dir/$m to $target_module_dir" + # -- --- --- --- --- --- + #set file_record [punkcheck::installfile_finished_install $basedir $file_record] + $event targetset_end OK -note "already versioned module" + } else { + puts -nonewline stderr "." + set did_skip 1 + if {$is_interesting} { + puts stderr "$current_source_dir/$m [$event targetset_source_changes]" + } + #set file_record [punkcheck::installfile_skipped_install $basedir $file_record] + $event targetset_end SKIPPED + } + } + } + } ;#end dict for {modpath modinfo} $process_modules + + + if {$CALLDEPTH >= $max_depth} { + set subdirs [list] + } else { + set subdirs [glob -nocomplain -dir $current_source_dir -type d -tail *] + } + #puts stderr "subdirs: $subdirs" + foreach d $subdirs { + set skipdir 0 + foreach dg $antidir { + if {[string match $dg $d]} { + set skipdir 1 + continue + } + } + if {$skipdir} { + continue + } + if {![file exists $target_module_dir/$d]} { + file mkdir $target_module_dir/$d + } + lappend module_list {*}[build_modules_from_source_to_base $srcdir $basedir\ + -call-depth-internal [expr {$CALLDEPTH +1}]\ + -subdirlist [list {*}$subdirlist $d]\ + -punkcheck_eventobj $event\ + -glob $fileglob\ + -podglob $podglob\ + ] + } + if {$did_skip} { + puts -nonewline stdout \n + } + if {$CALLDEPTH == 0} { + $event destroy + $installer destroy + } + return $module_list + } + + variable kettle_reset_bodies [dict create] + variable kettle_reset_args [dict create] + #We are abusing kettle to run in-process. + # when we change to another project we need recipes to be reloaded. + # Kettle rewrites some of it's own procs - stopping reloading of recipes when we change folders + #kettle_init stores the original proc bodies & args + proc kettle_init {} { + variable kettle_reset_bodies ;#dict + variable kettle_reset_args + set reset_procs [list\ + ::kettle::benchmarks\ + ::kettle::doc\ + ::kettle::figures\ + ::kettle::meta::scan\ + ::kettle::testsuite\ + ] + foreach p $reset_procs { + set b [info body $p] + if {[string match "*Overwrite self*" $b]} { + dict set kettle_reset_bodies $p $b + set argnames [info args $p] + set arglist [list] + foreach a $argnames { + if {[info default $p $a dval]} { + lappend arglist [list $a $dval] + } else { + lappend arglist $a + } + } + dict set kettle_reset_args $p $arglist + } + } + + } + #call kettle_reinit to ensure recipes point to current project + proc kettle_reinit {} { + variable kettle_reset_bodies + variable kettle_reset_args + dict for {p b} $kettle_reset_bodies { + #set b [dict get $kettle_reset_bodies $p] + set argl [dict get $kettle_reset_args $p] + uplevel 1 [list ::proc $p $argl $b] + } + #todo - determine standard recipes by examining standard.tcl instead of hard coding? + set standard_recipes [list\ + null\ + forever\ + list-recipes\ + help-recipes\ + help-dump\ + help-recipes\ + help\ + list\ + list-options\ + help-options\ + show-configuration\ + show-state\ + show\ + meta-status\ + gui\ + ] + #set ::kettle::recipe::recipe [dict create] + dict for {r -} $::kettle::recipe::recipe { + if {$r ni $standard_recipes} { + dict unset ::kettle::recipe::recipe $r + } + } + } + proc kettle_call {calltype args} { + variable kettle_reset_bodies + switch -- $calltype { + lib {} + shell { + set kettleappfile [file dirname [info nameofexecutable]]/kettle + set kettlebatfile [file dirname [info nameofexecutable]]/kettle.bat + + if {(![file exists $kettleappfile]) && (![file exists $kettlebatfile])} { + error "deck kettle_call unable to find installed kettle application file '$kettleappfile' (or '$kettlebatfile' if on windows)" + } + if {[file exists $kettleappfile]} { + set kettlescript $kettleappfile + } + if {$::tcl_platform(platform) eq "windows"} { + if {[file exists $kettlebatfile]} { + set kettlescript $kettlebatfile + } + } + } + default { + error "deck kettle_call 1st argument must be one of: 'lib' for direct use of kettle module or 'shell' to call as separate process" + } + } + set startdir [pwd] + if {![file exists $startdir/build.tcl]} { + error "deck kettle must be run from a folder containing build.tcl (cwd: [pwd])" + } + if {[package provide kettle] eq ""} { + puts stdout "Loading kettle package - may be delay on first load ..." + package require kettle + kettle_init ;#store original procs for those kettle procs that rewrite themselves + } else { + if {[dict size $kettle_reset_bodies] == 0} { + #presumably package require kettle was called without calling our kettle_init hack. + kettle_init + } else { + #undo proc rewrites + kettle_reinit + } + } + set first [lindex $args 0] + if {[string match @* $first]} { + error "deck kettle doesn't support special operations - try calling tclsh kettle directly" + } + if {$first eq "-f"} { + set args [lassign $args __ path] + } else { + set path $startdir/build.tcl + } + set opts [list] + + if {[lindex $args 0] eq "-trace"} { + set args [lrange $args 1 end] + lappend opts --verbose on + } + set goals [list] + + if {$calltype eq "lib"} { + file mkdir ~/.kettle + set dotfile ~/.kettle/config + if {[file exists $dotfile] && + [file isfile $dotfile] && + [file readable $dotfile]} { + ::kettle io trace {Loading dotfile $dotfile ...} + set args [list {*}[::kettle path cat $dotfile] {*}$args] + } + } + + #hardcoded kettle option names (::kettle option names) - retrieved using kettle::option names + #This is done so we don't have to load kettle lib for shell call (both loading as module and running shell are annoyingly SLOW) + #REVIEW - needs to be updated to keep in sync with kettle. + set knownopts [list\ + --exec-prefix --bin-dir --lib-dir --prefix --man-dir --html-dir --markdown-dir --include-dir \ + --ignore-glob --dry --verbose --machine --color --state --config --with-shell --log \ + --log-append --log-mode --with-dia --constraints --file --limitconstraints --tmatch --notfile --single --valgrind --tskip --repeats \ + --iters --collate --match --rmatch --with-doc-destination --with-git --target --test-include \ + ] + + while {[llength $args]} { + set o [lindex $args 0] + switch -glob -- $o { + --* { + #instead of using: kettle option known + if {$o ni $knownopts} { + error "Unable to process unknown option $o." {} [list KETTLE (deck)] + } + lappend opts $o [lindex $args 1] + #::kettle::option set $o [lindex $args 1] + set args [lrange $args 2 end] + } + default { + lappend goals $o + set args [lrange $args 1 end] + } + } + } + + if {![llength $goals]} { + lappend goals help + } + if {"--prefix" ni [dict keys $opts]} { + dict set opts --prefix [file dirname $startdir] + } + if {$calltype eq "lib"} { + ::kettle status clear + ::kettle::option::set @kettle $startdir + foreach {o v} $opts { + ::kettle option set $o $v + } + ::kettle option set @srcscript $path + ::kettle option set @srcdir [file dirname $path] + ::kettle option set @goals $goals + #load standard recipes as listed in build.tcl + ::source $path + puts stderr "recipes: [::kettle recipe names]" + ::kettle recipe run {*}[::kettle option get @goals] + + set state [::kettle option get --state] + if {$state ne {}} { + puts stderr "saving kettle state: $state" + ::kettle status save $state + } + + } else { + #shell + puts stdout "Running external kettle process with args: $opts $goals" + run -n tclsh $kettlescript -f $path {*}$opts {*}$goals + } + + } + proc kettle_punk_recipes {} { + set txtdst ... + } + + } +} + + +namespace eval punk::mix::cli { + proc _cli {args} { + #don't use tailcall - base uses info level to determine caller + ::punk::mix::base::_cli {*}$args + } + variable default_command help + package require punk::mix::base + package require punk::overlay + if {[catch { + punk::overlay::custom_from_base [namespace current] ::punk::mix::base + } errM]} { + puts stderr "punk::mix::cli load error: Failed to overlay punk::mix::base $errM" + error "punk::mix::cli error: $errM" + } +} + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::mix::cli [namespace eval punk::mix::cli { + variable version + set version 999999.0a1.0 +}] +return diff --git a/src/modules/punk/mix/cli-buildversion.txt b/src/modules/punk/mix/cli-buildversion.txt new file mode 100644 index 00000000..8a16d4cb --- /dev/null +++ b/src/modules/punk/mix/cli-buildversion.txt @@ -0,0 +1,3 @@ +0.3.1 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm index 7bdce9ac..09ca2d70 100644 --- a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm @@ -247,7 +247,8 @@ namespace eval punk::mix::commandset::loadedlib { set projectdir [dict get $pathinfo closest] if {$projectdir ne ""} { set modulefolders [punk::mix::cli::lib::find_source_module_paths $projectdir] - foreach k [list modules vendormodules] { + set majorv [lindex [split [info tclversion] .] 0] + foreach k [list modules modules_tcl$majorv vendormodules vendormodules_tcl$majorv] { set knownfolder [file join $projectdir src $k] if {$knownfolder ni $modulefolders} { lappend modulefolders $knownfolder @@ -261,7 +262,7 @@ namespace eval punk::mix::commandset::loadedlib { #special case bootsupport/modules so it can be referred to as just bootsupport or bootsupport/modules lappend modulefolders [file join $projectdir src bootsupport/modules] - if {$modulefoldername ni $mtails && $modulefoldername ni "bootsupport bootsupport/modules"} { + if {$modulefoldername ni $mtails && $modulefoldername ni "bootsupport bootsupport/modules bootsupport/modules_tcl$majorv"} { set msg "Suplied modulefoldername '$modulefoldername' doesn't appear to be a known module folder within the project at $projectdir\n" append msg "Known module folders: [lsort $mtails]\n" append msg "Use a name from the above list, or a fully qualified path\n" diff --git a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm index 7382c688..f4dfc714 100644 --- a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm @@ -159,7 +159,7 @@ namespace eval punk::mix::commandset::module { #set opts [dict merge $defaults $args] #todo - review compatibility between -template and -type - #-type is the wrapping technology e.g 'plain' for none or tarjar/zipkit etc (consider also snappy/snappy-tcl) + #-type is the wrapping technology e.g 'plain' for none or tarjar or zip (modpod) etc (consider also snappy/snappy-tcl) #-template may be a folder - but only if the selected -type suports it @@ -293,6 +293,7 @@ namespace eval punk::mix::commandset::module { } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- set opt_quiet [dict get $opts -quiet] + set opt_force [dict get $opts -force] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -378,13 +379,39 @@ namespace eval punk::mix::commandset::module { } set template_filedata [string map $strmap $template_filedata] - set modulefile $modulefolder/${moduletail}-$infile_version.tm - if {[file exists $modulefile]} { - set errmsg "module.new error: module file $modulefile already exists - aborting" - if {[string match "*$magicversion*" $modulefile]} { - append errmsg \n "If you are attempting to create a module file with a specific version in the source-file name - you will need to use a template that doesn't contain the string '$magicversion' e.g the provided template moduleexactversion-0.0.1.tm" + set tmfile $modulefolder/${moduletail}-$infile_version.tm + set podfile $modulefolder/#modpod-$moduletail-$infile_version/$moduletail-$infile_version.tm + set has_tm [file exists $tmfile] + set has_pod [file exists $podfile] + if {$has_tm && $has_pos} { + #invalid configuration - bomb out + error "module.new error: Invalid target configuration found. module folder has both a .tm file $tmfile and a modpod file $podfile. Please delete one of them before trying again." + } + if {$opt_type eq "plain"} { + set modulefile $tmfile + } else { + set modulefile $podfile + } + if {$has_tm || $has_pod} { + if {!$opt_force} { + if {$has_tm} { + set errmsg "module.new error: module file $tmfile already exists - aborting" + } else { + set errmsg "module.new error: module file $podfile already exists - aborting" + } + if {[string match "*$magicversion*" $tmfile]} { + append errmsg \n "If you are attempting to create a module file with a specific version in the source-file name - you will need to use a template that doesn't contain the string '$magicversion' e.g the provided template moduleexactversion-0.0.1.tm" + } + error $errmsg + } else { + #review - prompt here vs caller? + #we are committed to overwriting/replacing if there was a pre-existing module of same version + if {$has_pod} { + file delete -force [file dirname $podfile] + } elseif {$has_tm} { + file delete -force $tmfile + } } - error $errmsg } @@ -407,13 +434,20 @@ namespace eval punk::mix::commandset::module { } } - set existing_versions [glob -nocomplain -dir $modulefolder -tails ${moduletail}-*.tm] + set existing_tm_versions [glob -nocomplain -dir $modulefolder -tails ${moduletail}-*.tm] #it shouldn't be possible to overmatch with the glob - because '-' is not valid in a Tcl module name + set existing_pod_versions [glob -nocomplain -dir $modulefolder -tails #modpod-$moduletail-*] + set existing_versions [concat $existing_tm_versions $existing_pod_versions] + if {[llength $existing_versions]} { set name_version_pairs [list] lappend name_version_pairs [list $moduletail $infile_version] foreach existing $existing_versions { - lappend name_version_pairs [punk::mix::cli::lib::split_modulename_version $existing] ;# .tm is stripped and ignored + lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored + if {[string match #modpod-* $namepart]} { + set namepart [string range $namepart 8 end] + } + lappend name_version_pairs [list $namepart $version] } set name_version_pairs [lsort -command {package vcompare} -index 1 $name_version_pairs] ;#while plain lsort will often work with versions - it can get order wrong with some - so use package vcompare if {[lindex $name_version_pairs end] ne [list $moduletail $infile_version]} { @@ -436,6 +470,8 @@ namespace eval punk::mix::commandset::module { if {!$opt_quiet} { puts stdout "Creating $modulefile from template $moduletemplate" } + file mkdir [file dirname $modulefile] + set fd [open $modulefile w] fconfigure $fd -translation binary puts -nonewline $fd $template_filedata diff --git a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm index 430149cc..d119126d 100644 --- a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm @@ -320,6 +320,8 @@ namespace eval punk::mix::commandset::project { puts stderr "-force 1 or -update 1 not specified - aborting" return } + #review + set fossil_repo_file $repodb_folder/$projectname.fossil } if {$fossil_repo_file eq ""} { @@ -415,12 +417,30 @@ namespace eval punk::mix::commandset::project { if {[file exists $projectdir/src/modules]} { foreach m $opt_modules { - if {![file exists $projectdir/src/modules/$m-[punk::mix::util::magic_tm_version].tm]} { + #check if mod-ver.tm file or #modpod-mod-ver folder exist + set tmfile $projectdir/src/modules/$m-[punk::mix::util::magic_tm_version].tm + set podfile $projectdir/src/modules/#modpod-$m-[punk::mix::util::magic_tm_version]/$m-[punk::mix::util::magic_tm_version].tm + + set has_tm [file exists $tmfile] + set has_pod [file exists $podfile] + #puts stderr "=====> has_tm: $has_tm has_pod: $has_pod" + if {!$has_tm && !$has_pod} { #todo - option for -module_template - and check existence at top? or change opt_modules to be a list of dicts with configuration info -template -type etc - punk::mix::commandset::module::new $m -project $projectname -type $opt_type + punk::mix::commandset::module::new -project $projectname -type $opt_type $m } else { + #we should rarely if ever want to force any src/modules to be overwritten if {$opt_force} { - punk::mix::commandset::module::new $m -project $projectname -type $opt_type -force 1 + if {$has_pod} { + set answer [util::askuser "OVERWRITE the src/modules file $podfile ?? (generally not desirable) Y|N"] + set overwrite_type zip + } else { + set answer [util::askuser "OVERWRITE the src/modules file $tmfile ?? (generally not desirable) Y|N"] + set overwrite_type $opt_type + } + if {[string tolower $answer] eq "y"} { + #REVIEW - all pods zip - for now + punk::mix::commandset::module::new -project $projectname -type $overwrite_type -force 1 $m + } } } } diff --git a/src/modules/punk/mix/templates/layouts/project/src/make.tcl b/src/modules/punk/mix/templates/layouts/project/src/make.tcl index c53315e9..20b0c29f 100644 --- a/src/modules/punk/mix/templates/layouts/project/src/make.tcl +++ b/src/modules/punk/mix/templates/layouts/project/src/make.tcl @@ -13,7 +13,7 @@ namespace eval ::punkmake { variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] variable non_help_flags [list -k] variable help_flags [list -help --help /?] - variable known_commands [list project get-project-info shell bootsupport] + variable known_commands [list project get-project-info shell vendor bootsupport] } if {"::try" ni [info commands ::try]} { puts stderr "Tcl interpreter possibly too old - 'try' command not found - aborting" @@ -134,6 +134,8 @@ proc punkmake_gethelp {args} { append h " - the optional -k flag will terminate processes running as the executable being built (if applicable)" \n \n append h " $scriptname bootsupport" \n append h " - update the src/bootsupport modules as well as the mixtemplates/layouts//src/bootsupport modules if the folder exists" \n \n + append h " $scriptname vendor" \n + append h " - update the src/vendormodules based on src/vendormodules/include_modules.config" \n \n append h " $scriptname get-project-info" \n append h " - show the name and base folder of the project to be built" \n append h "" \n @@ -251,6 +253,100 @@ if {$::punkmake::command eq "shell"} { exit 1 } +if {$::punkmake::command eq "vendor"} { + puts "projectroot: $projectroot" + puts "script: [info script]" + #puts "-- [tcl::tm::list] --" + puts stdout "Updating vendor modules" + proc vendor_localupdate {projectroot} { + set local_modules [list] + set git_modules [list] + set fossil_modules [list] + #todo vendor/lib ? + set vendor_config $projectroot/src/vendormodules/include_modules.config + if {[file exists $vendor_config]} { + set targetroot $projectroot/src/vendormodules/modules + source $vendor_config ;#populate $local_modules $git_modules $fossil_modules with project-specific list + if {![llength $local_modules]} { + puts stderr "No local vendor modules configured for updating (config file: $vendor_config)" + } else { + if {[catch { + #---------- + set vendor_installer [punkcheck::installtrack new make.tcl $projectroot/src/vendormodules/.punkcheck] + $vendor_installer set_source_target $projectroot $projectroot/src/vendormodules + set installation_event [$vendor_installer start_event {-make_step vendor}] + #---------- + } errM]} { + puts stderr "Unable to use punkcheck for vendor update. Error: $errM" + set installation_event "" + } + foreach {relpath module} $local_modules { + set module [string trim $module :] + set module_subpath [string map {:: /} [namespace qualifiers $module]] + set srclocation [file join $projectroot $relpath $module_subpath] + #puts stdout "$relpath $module $module_subpath $srclocation" + set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] + #lsort won't sort version numbers properly e.g with -dictionary 0.1.1 comes before 0.1 + if {![llength $pkgmatches]} { + puts stderr "Missing source for vendor module $module - not found in $srclocation" + continue + } + set latestfile [lindex $pkgmatches 0] + set latestver [lindex [split [file rootname $latestfile] -] 1] + foreach m $pkgmatches { + lassign [split [file rootname $m] -] _pkg ver + #puts "comparing $ver vs $latestver" + if {[package vcompare $ver $latestver] == 1} { + set latestver $ver + set latestfile $m + } + } + set srcfile [file join $srclocation $latestfile] + set tgtfile [file join $targetroot $module_subpath $latestfile] + if {$installation_event ne ""} { + #---------- + $installation_event targetset_init INSTALL $tgtfile + $installation_event targetset_addsource $srcfile + #---------- + if {\ + [llength [dict get [$installation_event targetset_source_changes] changed]]\ + || [llength [$installation_event get_targets_exist]] < [llength [$installation_event get_targets]]\ + } { + file mkdir [file dirname $tgtfile] ;#ensure containing folder for target exists + $installation_event targetset_started + # -- --- --- --- --- --- + puts "VENDOR update: $srcfile -> $tgtfile" + if {[catch { + file copy -force $srcfile $tgtfile + } errM]} { + $installation_event targetset_end FAILED + } else { + $installation_event targetset_end OK + } + # -- --- --- --- --- --- + } else { + puts -nonewline stderr "." + $installation_event targetset_end SKIPPED + } + $installation_event end + } else { + file copy -force $srcfile $tgtfile + } + } + + } + } else { + puts stderr "No config at $vendor_config - nothing configured to update" + } + } + + + puts stdout " vendor package update done " + flush stderr + flush stdout + ::exit 0 +} + if {$::punkmake::command eq "bootsupport"} { puts "projectroot: $projectroot" puts "script: [info script]" @@ -275,7 +371,7 @@ if {$::punkmake::command eq "bootsupport"} { set boot_event [$boot_installer start_event {-make_step bootsupport}] #---------- } errM]} { - puts stderr "Unable to use punkcheck for bootsupport error: $errM" + puts stderr "Unable to use punkcheck for bootsupport. Error: $errM" set boot_event "" } @@ -441,7 +537,7 @@ if {[file exists $sourcefolder/vendorlib]} { if {[file exists $sourcefolder/vendormodules]} { #install .tm *and other files* puts stdout "VENDORMODULES: copying from $sourcefolder/vendormodules to $target_modules_base (if source file changed)" - set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md}] + set resultdict [punkcheck::install $sourcefolder/vendormodules $target_modules_base -installer make.tcl -overwrite installedsourcechanged-targets -antiglob_paths {README.md include_modules.config}] puts stdout [punkcheck::summarize_install_resultdict $resultdict] } else { puts stderr "VENDORMODULES: No src/vendormodules folder found." diff --git a/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl b/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl new file mode 100644 index 00000000..9487b6e6 --- /dev/null +++ b/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/modpod-loadscript.tcl @@ -0,0 +1,53 @@ +apply {code { + set scriptpath [file normalize [info script]] + if {[string match "#modpod-loadscript*.tcl" [file tail $scriptpath]]} { + #jump up an extra dir level if we are within a #modpod-loadscript file. + set mypath [file dirname [file dirname $scriptpath]] + #expect to be in folder #modpod-- + #Now we need to test if we are in a mounted folder vs an extracted folder + set container [file dirname $mypath] + if {[string match "#mounted-modpod-*" $container]} { + set mypath [file dirname $container] + } + set modver [string range [file tail [file dirname $scriptpath]] 8 end] ;# the containing folder is named #modpod-- + } else { + set mypath [file dirname $scriptpath] + set modver [file root [file tail [info script]]] + } + set mysegs [file split $mypath] + set overhang [list] + foreach libpath [tcl::tm::list] { + set libsegs [file split $libpath] ;#split and rejoin with '/' because sometimes module paths may have mixed \ & / + if {[file join $mysegs /] eq [file join [lrange $libsegs 0 [llength $mysegs]] /]} { + #mypath is below libpath + set overhang [lrange $mysegs [llength $libsegs]+1 end] + break + } + } + lassign [split $modver -] moduletail version + set ns [join [concat $overhang $moduletail] ::] + #if {![catch {package require modpod}]} { + # ::modpod::disconnect [info script] + #} + package provide $ns $version + namespace eval $ns $code +} ::} { + # + # Module procs here, where current namespace is that of the module. + # Package version can, if needed, be accessed as [uplevel 1 {set version}] + # Last element of module name: [uplevel 1 {set moduletail}] + # Full module name: [uplevel 1 {set ns}] + + # + # + # + + # + # + # + + # + # + # + +} diff --git a/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z b/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z new file mode 100644 index 00000000..a8f7b05a --- /dev/null +++ b/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/modpod-module-version/z @@ -0,0 +1,2 @@ +#Do not remove the trailing ctrl-z character from this file + \ No newline at end of file diff --git a/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip b/src/modules/punk/mix/templates/modpod/template_modpod-0.0.1/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..665234dec0d8e52074344321c6b9813e78b18ed9 GIT binary patch literal 1275 zcmWIWW@Zs#00D&^uOh$k`3i4BQfmCTus%}|oQE_H|o_+vS69>b~ZLcCS zZ!j;O&&0s6osEG(7q=#uSvmQMDaFY}nFS?!CCNFpAqVqr8}RHEe_>zXyMZbC_L}Vn zwX5!AGMAmNNl;Y_)AnAcqW-q&&uPG1=Nxnl~O^07{emEb@ zpg-r8Q~SckuWp%4$+6v0f0ZM&H*d6DxK7w5_rZp52Q@dhiZy3E$hjDGGqQ4}?x`t% zo@#N_PW#EE#d|t%*}8sD`@dq}Yi~Q=XOvp7{DHudq6L}mR$Xs7F9^N~&6vX2vQxrk z>S~t=pRMN?FFcXHVd=~LnXbVm-fvDgZhUzm=jGIy{T&aiCrmW`6XB4-(PG+TFflcG zYf8{l#YIoLCraB@`);3ZzNT_EmnBo?`QTqCuEhL}6}P+_DXk`^ws`I49Gk9b%h#Ug zxSuJSF!Rf6!;?n!&2MtoPUW}Uu(2<|WWm)~Epv|8(1mMPDZX5*={H+DH76%|m!tB* zi+e-UZm?Ly&pj;5`NlRm$J=r16U9;?r78a(7@O=+&bTV<`Gb3Y;?yOlrPNP0mdyOS zE^C3i1oImm-}=Z`e~yLEI%8(S8giXSI(*lf(s|Q9#jM!&`%=GQ_O+vHLMz-CD#g|B zZA;v{Or^Z6q)Y0;6Z z0io;Xwfbip+Hkb<9cA7m=-$7#h$ZuK|70~oli2X7A@f3cIt^k^clIB;X}LRi((EIu zdg`Ue&mSz7VtgC0yyeG@x$|Tq-?&y!9|M?LqJ#cQI@js92+iZX-+!lx>@uc*s;Ifn9Cr;}JYIvPJtM47EagyQD zq9bSKoY_;-`ef1~nY&u8PkN3lI`U-F6{XfKi~htcm~ $last_char_info_width} { append summary " ..." @@ -842,7 +842,7 @@ namespace eval punk::repl::class { #append combined \n append new0 \n } - set underlay [punk::ansi::stripansi $activeline] + set underlay [punk::ansi::ansistrip $activeline] set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] if {$o_cursor_col > $line_nextchar_col} { set o_cursor_col $line_nextchar_col @@ -1103,7 +1103,7 @@ namespace eval punk::repl::class { set suffix [string repeat " " [expr {$linecols -$col1}]] #capitalised INDEX - for grapheme/control-char index e.g a with diacritic a\u0300 has a single index set char_at_cursor [ansistring INDEX $cursorline $charindex_at_cursor] ;#this is the char with appropriate ansireset codes - set rawchar [punk::ansi::stripansi $char_at_cursor] + set rawchar [punk::ansi::ansistrip $char_at_cursor] if {$rawchar eq " "} { set charhighlight "[punk::ansi::a+ White]_[a]" } else { @@ -1865,7 +1865,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config } if {[string match "\x1b*" $line]} { rputs stderr "${debugprompt}esc - '[punk::ansi::ansistring::VIEW $line]'" - #set commandstr [punk::ansi::stripansi $commandstr] + #set commandstr [punk::ansi::ansistrip $commandstr] } } @@ -2069,8 +2069,8 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config #screen_last_char_add [string index $lastoutchar_codethread$lasterrchar_codethread end] "stdout/stderr" - #set lastoutchar [string index [punk::ansi::stripansi $::repl::output_stdout] end] - #set lasterrchar [string index [punk::ansi::stripansi $::repl::output_stderr] end] + #set lastoutchar [string index [punk::ansi::ansistrip $::repl::output_stdout] end] + #set lasterrchar [string index [punk::ansi::ansistrip $::repl::output_stderr] end] #to determine whether cursor is back at col0 of newline #screen_last_char_add [string index $lastoutchar$lasterrchar end] "stdout/stderr" diff --git a/src/modules/punk/repl/codethread-999999.0a1.0.tm b/src/modules/punk/repl/codethread-999999.0a1.0.tm index 1c73b3a4..bfbe976c 100644 --- a/src/modules/punk/repl/codethread-999999.0a1.0.tm +++ b/src/modules/punk/repl/codethread-999999.0a1.0.tm @@ -177,8 +177,8 @@ tcl::namespace::eval punk::repl::codethread { #interp transfer code $errhandle "" #flush $errhandle - set lastoutchar [string index [punk::ansi::stripansi $output_stdout] end] - set lasterrchar [string index [punk::ansi::stripansi $output_stderr] end] + set lastoutchar [string index [punk::ansi::ansistrip $output_stdout] end] + set lasterrchar [string index [punk::ansi::ansistrip $output_stderr] end] #puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]" set tid [thread::id] diff --git a/src/modules/punk/repo-999999.0a1.0.tm b/src/modules/punk/repo-999999.0a1.0.tm index dcd37719..436dcdc4 100644 --- a/src/modules/punk/repo-999999.0a1.0.tm +++ b/src/modules/punk/repo-999999.0a1.0.tm @@ -1447,6 +1447,7 @@ namespace eval punk::repo { #Must accept empty prefix - which is effectively noop. #MUCH faster version for absolute path prefix (pre-normalized) + #review - will error on file join if lrange returns empty list ie if prefix longer than path proc path_strip_alreadynormalized_prefixdepth {path prefix} { if {$prefix eq ""} { return $path @@ -1488,11 +1489,11 @@ namespace eval punk::repo { interp alias {} git_revision {} ::punk::repo::git_revision - interp alias {} gs {} git status -sb - interp alias {} gr {} ::punk::repo::git_revision - interp alias {} gl {} git log --oneline --decorate ;#decorate so stdout consistent with what we see on console - interp alias {} glast {} git log -1 HEAD --stat - interp alias {} gconf {} git config --global -l + interp alias {} gs {} shellrun::runconsole git status -sb + interp alias {} gr {} ::punk::repo::git_revision + interp alias {} gl {} shellrun::runconsole git log --oneline --decorate ;#decorate so stdout consistent with what we see on console + interp alias {} glast {} shellrun::runconsole git log -1 HEAD --stat + interp alias {} gconf {} shellrun::runconsole git config --global -l } namespace eval punk::repo::lib { diff --git a/src/modules/punk/zip-999999.0a1.0.tm b/src/modules/punk/zip-999999.0a1.0.tm new file mode 100644 index 00000000..d9573d2b --- /dev/null +++ b/src/modules/punk/zip-999999.0a1.0.tm @@ -0,0 +1,632 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2024 JMN +# (C) 2009 Path Thoyts +# +# @@ Meta Begin +# Application punk::zip 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_punk::zip 0 999999.0a1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require punk::zip] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of punk::zip +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by punk::zip +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6}] +#[item] [package {punk::args}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval punk::zip::class { + #*** !doctools + #[subsection {Namespace punk::zip::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::zip { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + #variable xyz + + #*** !doctools + #[subsection {Namespace punk::zip}] + #[para] Core API functions for punk::zip + #[list_begin definitions] + + proc Path_a_atorbelow_b {path_a path_b} { + return [expr {[StripPath $path_b $path_a] ne $path_a}] + } + proc Path_a_at_b {path_a path_b} { + return [expr {[StripPath $path_a $path_b] eq "." }] + } + + proc Path_strip_alreadynormalized_prefixdepth {path prefix} { + if {$prefix eq ""} { + return $path + } + set pathparts [file split $path] + set prefixparts [file split $prefix] + if {[llength $prefixparts] >= [llength $pathparts]} { + return "" + } + return [file join \ + {*}[lrange \ + $pathparts \ + [llength $prefixparts] \ + end]] + } + + #StripPath - borrowed from tcllib fileutil + # ::fileutil::stripPath -- + # + # If the specified path references/is a path in prefix (or prefix itself) it + # is made relative to prefix. Otherwise it is left unchanged. + # In the case of it being prefix itself the result is the string '.'. + # + # Arguments: + # prefix prefix to strip from the path. + # path path to modify + # + # Results: + # path The (possibly) modified path. + + if {[string equal $tcl_platform(platform) windows]} { + # Windows. While paths are stored with letter-case preserved al + # comparisons have to be done case-insensitive. For reference see + # SF Tcllib Bug 2499641. + + proc StripPath {prefix path} { + # [file split] is used to generate a canonical form for both + # paths, for easy comparison, and also one which is easy to modify + # using list commands. + + set prefix [file split $prefix] + set npath [file split $path] + + if {[string equal -nocase $prefix $npath]} { + return "." + } + + if {[string match -nocase "${prefix} *" $npath]} { + set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]] + } + return $path + } + } else { + proc StripPath {prefix path} { + # [file split] is used to generate a canonical form for both + # paths, for easy comparison, and also one which is easy to modify + # using list commands. + + set prefix [file split $prefix] + set npath [file split $path] + + if {[string equal $prefix $npath]} { + return "." + } + + if {[string match "${prefix} *" $npath]} { + set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]] + } + return $path + } + } + + proc Timet_to_dos {time_t} { + #*** !doctools + #[call] [fun Timet_to_dos] [arg time_t] + #[para] convert a unix timestamp into a DOS timestamp for ZIP times. + #[example { + # DOS timestamps are 32 bits split into bit regions as follows: + # 24 16 8 0 + # +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + # |Y|Y|Y|Y|Y|Y|Y|m| |m|m|m|d|d|d|d|d| |h|h|h|h|h|m|m|m| |m|m|m|s|s|s|s|s| + # +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+ + #}] + set s [clock format $time_t -format {%Y %m %e %k %M %S}] + scan $s {%d %d %d %d %d %d} year month day hour min sec + expr {(($year-1980) << 25) | ($month << 21) | ($day << 16) + | ($hour << 11) | ($min << 5) | ($sec >> 1)} + } + + proc walk {args} { + #*** !doctools + #[call] [fun walk] [arg ?options?] [arg base] + #[para] Walk a directory tree rooted at base + #[para] the -excludes list can be a set of glob expressions to match against files and avoid + #[para] e.g + #[example { + # punk::zip::walk -exclude {CVS/* *~.#*} library + #}] + + set argd [punk::args::get_dict { + *proc -name punk::zip::walk + -excludes -default "" -help "list of glob expressions to match against files and exclude" + -subpath -default "" + *values -min 1 -max -1 + base + fileglobs -default {*} -multiple 1 + } $args] + set base [dict get $argd values base] + set fileglobs [dict get $argd values fileglobs] + set subpath [dict get $argd opts -subpath] + set excludes [dict get $argd opts -excludes] + + + set imatch [list] + foreach fg $fileglobs { + lappend imatch [file join $subpath $fg] + } + + set result {} + #set imatch [file join $subpath $match] + set files [glob -nocomplain -tails -types f -directory $base -- {*}$imatch] + foreach file $files { + set excluded 0 + foreach glob $excludes { + if {[string match $glob $file]} { + set excluded 1 + break + } + } + if {!$excluded} {lappend result $file} + } + foreach dir [glob -nocomplain -tails -types d -directory $base -- [file join $subpath *]] { + set subdir [walk -subpath $dir -excludes $excludes $base {*}$fileglobs] + if {[llength $subdir]>0} { + set result [concat $result $dir $subdir] + } + } + return $result + } + + # Mkzipfile -- + # + # FIX ME: should handle the current offset for non-seekable channels + # + proc Mkzipfile {zipchan base path {comment ""}} { + #*** !doctools + #[call] [fun Mkzipfile] [arg zipchan] [arg base] [arg path] [arg ?comment?] + #[para] Add a single file to a zip archive + #[para] The zipchan channel should already be open and binary. + #[para] You can provide a -comment for the file. + #[para] The return value is the central directory record that will need to be used when finalizing the zip archive. + + set fullpath [file join $base $path] + set mtime [Timet_to_dos [file mtime $fullpath]] + set utfpath [encoding convertto utf-8 $path] + set utfcomment [encoding convertto utf-8 $comment] + set flags [expr {(1<<11)}] ;# utf-8 comment and path + set method 0 ;# store 0, deflate 8 + set attr 0 ;# text or binary (default binary) + set version 20 ;# minumum version req'd to extract + set extra "" + set crc 0 + set size 0 + set csize 0 + set data "" + set seekable [expr {[tell $zipchan] != -1}] + if {[file isdirectory $fullpath]} { + set attrex 0x41ff0010 ;# 0o040777 (drwxrwxrwx) + #set attrex 0x40000010 + } elseif {[file executable $fullpath]} { + set attrex 0x81ff0080 ;# 0o100777 (-rwxrwxrwx) + } else { + set attrex 0x81b60020 ;# 0o100666 (-rw-rw-rw-) + if {[file extension $fullpath] in {".tcl" ".txt" ".c"}} { + set attr 1 ;# text + } + } + + if {[file isfile $fullpath]} { + set size [file size $fullpath] + if {!$seekable} {set flags [expr {$flags | (1 << 3)}]} + } + + + set offset [tell $zipchan] + set local [binary format a4sssiiiiss PK\03\04 \ + $version $flags $method $mtime $crc $csize $size \ + [string length $utfpath] [string length $extra]] + append local $utfpath $extra + puts -nonewline $zipchan $local + + if {[file isfile $fullpath]} { + # If the file is under 2MB then zip in one chunk, otherwize we use + # streaming to avoid requiring excess memory. This helps to prevent + # storing re-compressed data that may be larger than the source when + # handling PNG or JPEG or nested ZIP files. + if {$size < 0x00200000} { + set fin [open $fullpath rb] + set data [read $fin] + set crc [zlib crc32 $data] + set cdata [zlib deflate $data] + if {[string length $cdata] < $size} { + set method 8 + set data $cdata + } + close $fin + set csize [string length $data] + puts -nonewline $zipchan $data + } else { + set method 8 + set fin [open $fullpath rb] + set zlib [zlib stream deflate] + while {![eof $fin]} { + set data [read $fin 4096] + set crc [zlib crc32 $data $crc] + $zlib put $data + if {[string length [set zdata [$zlib get]]]} { + incr csize [string length $zdata] + puts -nonewline $zipchan $zdata + } + } + close $fin + $zlib finalize + set zdata [$zlib get] + incr csize [string length $zdata] + puts -nonewline $zipchan $zdata + $zlib close + } + + if {$seekable} { + # update the header if the output is seekable + set local [binary format a4sssiiii PK\03\04 \ + $version $flags $method $mtime $crc $csize $size] + set current [tell $zipchan] + seek $zipchan $offset + puts -nonewline $zipchan $local + seek $zipchan $current + } else { + # Write a data descriptor record + set ddesc [binary format a4iii PK\7\8 $crc $csize $size] + puts -nonewline $zipchan $ddesc + } + } + + #PK\x01\x02 Cdentral directory file header + #set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3 + set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems) + + set hdr [binary format a4ssssiiiisssssii PK\01\02 $v1 \ + $version $flags $method $mtime $crc $csize $size \ + [string length $utfpath] [string length $extra]\ + [string length $utfcomment] 0 $attr $attrex $offset] + append hdr $utfpath $extra $utfcomment + return $hdr + } + # zip::mkzip -- + # + # eg: zip my.zip -directory Subdir -runtime unzipsfx.exe *.txt + # + proc mkzip {args} { + #*** !doctools + #[call] [fun mkzip] [arg ?options?] [arg filename] + #[para] Create a zip archive in 'filename' + #[para] If a file already exists, an error will be raised. + set argd [punk::args::get_dict { + *proc -name punk::zip::mkzip -help "Create a zip archive in 'filename'" + *opts + -return -default "pretty" -choices {pretty list none} -help "mkzip can return a list of the files and folders added to the archive + the option -return pretty is the default and uses the punk::lib pdict/plist system + to return a formatted list for the terminal + " + -zipkit -default 0 -type none -help "" + -runtime -default "" -help "specify a prefix file + e.g punk::zip::mkzip -runtime unzipsfx.exe -directory subdir output.zip + will create a self-extracting zip archive from the subdir/ folder. + " + -comment -default "" -help "An optional comment for the archive" + -directory -default "" -help "The new zip archive will scan for contents within this folder or current directory if not provided" + -base -default "" -help "The new zip archive will be rooted in this directory if provided + it must be a parent of -directory" + -exclude -default {CVS/* */CVS/* *~ ".#*" "*/.#*"} + *values -min 1 -max -1 + filename -default "" -help "name of zipfile to create" + globs -default {*} -multiple 1 -help "list of glob patterns to match. + Only directories with matching files will be included in the archive" + } $args] + + set filename [dict get $argd values filename] + if {$filename eq ""} { + error "mkzip filename cannot be empty string" + } + if {[regexp {[?*]} $filename]} { + #catch a likely error where filename is omitted and first glob pattern is misinterpreted as zipfile name + error "mkzip filename should not contain glob characters ? *" + } + if {[file exists $filename]} { + error "mkzip filename:$filename already exists" + } + dict for {k v} [dict get $argd opts] { + switch -- $k { + -comment { + dict set argd opts $k [encoding convertto utf-8 $v] + } + -directory - -base { + dict set argd opts $k [file normalize $v] + } + } + } + + array set opts [dict get $argd opts] + + + if {$opts(-directory) ne ""} { + if {$opts(-base) ne ""} { + #-base and -directory have been normalized already + if {![Path_a_atorbelow_b $opts(-directory) $opts(-base)]} { + error "punk::zip::mkzip -base $opts(-base) must be above -directory $opts(-directory)" + } + set base $opts(-base) + set relpath [Path_strip_alreadynormalized_prefixdepth $opts(-directory) $opts(-base)] + } else { + set base $opts(-directory) + set relpath "" + } + set paths [walk -exclude $opts(-exclude) -subpath $relpath -- $base {*}[dict get $argd values globs]] + + set norm_filename [file normalize $filename] + set norm_dir [file normalize $opts(-directory)] ;#we only care if filename below -directory (which is where we start scanning) + if {[Path_a_atorbelow_b $norm_filename $norm_dir]} { + #check that we aren't adding the zipfile to itself + #REVIEW - now that we open zipfile after scanning - this isn't really a concern! + #keep for now in case we can add an -update or a -force facility (or in case we modify to add to zip as we scan for members?) + #In the case of -force - we may want to delay replacement of original until scan is done? + + #try to avoid looping on all paths and performing (somewhat) expensive file normalizations on each + #1st step is to check the patterns and see if our zipfile is already excluded - in which case we need not check the paths + set self_globs_match 0 + foreach g [dict get $argd values globs] { + if {[string match $g [file tail $filename]]} { + set self_globs_match 1 + break + } + } + if {$self_globs_match} { + #still dangerous + set self_excluded 0 + foreach e $opts(-exclude) { + if {[string match $e [file tail $filename]]} { + set self_excluded 1 + break + } + } + if {!$self_excluded} { + #still dangerous - likely to be in resultset - check each path + #puts stderr "zip file $filename is below directory $opts(-directory)" + set self_is_matched 0 + set i 0 + foreach p $paths { + set norm_p [file normalize [file join $opts(-directory) $p]] + if {[Path_a_at_b $norm_filename $norm_p]} { + set self_is_matched 1 + break + } + incr i + } + if {$self_is_matched} { + puts stderr "WARNING - zipfile being created '$filename' was matched. Excluding this file. Relocate the zip, or use -exclude patterns to avoid this message" + set paths [lremove $paths $i] + } + } + } + } + } else { + set paths [list] + set dir [pwd] + if {$opts(-base) ne ""} { + if {![Path_a_atorbelow_b $dir $opts(-base)]} { + error "punk::zip::mkzip -base $opts(-base) must be above current directory" + } + set relpath [Path_strip_alreadynormalized_prefixdepth [file normalize $dir] [file normalize $opts(-base)]] + } else { + set relpath "" + } + set base $opts(-base) + + set matches [glob -nocomplain -type f -- {*}[dict get $argd values globs]] + foreach m $matches { + if {$m eq $filename} { + #puts stderr "--> excluding $filename" + continue + } + set isok 1 + foreach e [concat $opts(-exclude) $filename] { + if {[string match $e $m]} { + set isok 0 + break + } + } + if {$isok} { + lappend paths [file join $relpath $m] + } + } + } + + if {![llength $paths]} { + return "" + } + + set zf [open $filename wb] + if {$opts(-runtime) ne ""} { + set rt [open $opts(-runtime) rb] + fcopy $rt $zf + close $rt + } elseif {$opts(-zipkit)} { + set zkd "#!/usr/bin/env tclkit\n\# This is a zip-based Tcl Module\n" + append zkd "package require vfs::zip\n" + append zkd "vfs::zip::Mount \[info script\] \[info script\]\n" + append zkd "if {\[file exists \[file join \[info script\] main.tcl\]\]} {\n" + append zkd " source \[file join \[info script\] main.tcl\]\n" + append zkd "}\n" + append zkd \x1A + puts -nonewline $zf $zkd + } + set count 0 + set cd "" + + set members [list] + foreach path $paths { + #puts $path + lappend members $path + append cd [Mkzipfile $zf $base $path] ;#path already includes relpath + incr count + } + set cdoffset [tell $zf] + set endrec [binary format a4ssssiis PK\05\06 0 0 \ + $count $count [string length $cd] $cdoffset\ + [string length $opts(-comment)]] + append endrec $opts(-comment) + puts -nonewline $zf $cd + puts -nonewline $zf $endrec + close $zf + + set result "" + switch -exact -- $opts(-return) { + list { + set result $members + } + pretty { + if {[info commands showlist] ne ""} { + set result [plist -channel none members] + } else { + set result $members + } + } + none { + set result "" + } + } + return $result + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::zip ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval punk::zip::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace punk::zip::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace punk::zip::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +#tcl::namespace::eval punk::zip::system { + #*** !doctools + #[subsection {Namespace punk::zip::system}] + #[para] Internal functions that are not part of the API + + + +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::zip [tcl::namespace::eval punk::zip { + variable pkg punk::zip + variable version + set version 999999.0a1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/modules/punk/zip-buildversion.txt b/src/modules/punk/zip-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/punk/zip-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punkcheck-0.1.0.tm b/src/modules/punkcheck-0.1.0.tm index 56d42b23..5d4f5c27 100644 --- a/src/modules/punkcheck-0.1.0.tm +++ b/src/modules/punkcheck-0.1.0.tm @@ -37,7 +37,7 @@ namespace eval punkcheck { start_installer_event installfile_* #antiglob_dir & antiglob_file entries match the pattern at any level - should not contain path separators - variable default_antiglob_dir_core [list "#*" "_aside" ".git" ".fossil*"] + variable default_antiglob_dir_core [list "#*" "_aside" "_build" ".git" ".fossil*"] variable default_antiglob_file_core "" proc uuid {} { set has_twapi 0 @@ -1196,7 +1196,7 @@ namespace eval punkcheck { #and may be less error prone than doing slightly more opaue path manipulations at each recursion level to determine where we started #For consistency - we'll use the same mechanism in various recursive directory walking procedures such as this one. set CALLDEPTH [dict get $opts -call-depth-internal] ;#added for extra debug/sanity checking - clearer test for initial function call ie CALLDPEPTH = 0 - set max_depth [dict get $opts -max_depth] + set max_depth [dict get $opts -max_depth] ;# -1 for no limit set subdirlist [dict get $opts -subdirlist] ;# generally should be same length as CALLDEPTH - but user could prefill set fileglob [dict get $opts -glob] set createdir [dict get $opts -createdir] ;#defaults to zero to help avoid mistakes with initial target dir - required target subdirs are created regardless of this setting @@ -1598,7 +1598,7 @@ namespace eval punkcheck { } - if {$CALLDEPTH >= $max_depth} { + if {$max_depth != -1 && $CALLDEPTH >= $max_depth} { #don't process any more subdirs set subdirs [list] } else { diff --git a/src/modules/shellfilter-0.1.9.tm b/src/modules/shellfilter-0.1.9.tm index 861b66ff..e1983653 100644 --- a/src/modules/shellfilter-0.1.9.tm +++ b/src/modules/shellfilter-0.1.9.tm @@ -135,8 +135,9 @@ namespace eval shellfilter::pipe { namespace eval shellfilter::ansi { #maint warning - - #stripansi from punk::ansi is better/more comprehensive + #ansistrip from punk::ansi is better/more comprehensive proc stripcodes {text} { + #obsolete? #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic @@ -522,7 +523,7 @@ namespace eval shellfilter::chan { #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::stripansi converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #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 @@ -554,7 +555,7 @@ namespace eval shellfilter::chan { } method read {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] - set outstring [punk::ansi::stripansi $instring] + set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } method flush {transform_handle} { @@ -562,7 +563,7 @@ namespace eval shellfilter::chan { } method write {transform_handle bytes} { set instring [encoding convertfrom $o_enc $bytes] - set outstring [punk::ansi::stripansi $instring] + set outstring [punk::ansi::ansistrip $instring] return [encoding convertto $o_enc $outstring] } method meta_is_redirection {} { diff --git a/src/modules/shellrun-0.1.1.tm b/src/modules/shellrun-0.1.1.tm index 3cfdcd39..ace56e9c 100644 --- a/src/modules/shellrun-0.1.1.tm +++ b/src/modules/shellrun-0.1.1.tm @@ -178,6 +178,41 @@ namespace eval shellrun { return $exitinfo } + #run in the way tcl unknown does - but without regard to auto_noexec + proc runconsole {args} { + if {![llength $args]} { + error "no commandline specified" + return + } + set name [lindex $args 0] + set new [auto_execok $name] + set repl_runid [punk::get_repl_runid] + #set ::punk::last_run_display [list] + + set redir ">&@stdout <@stdin" + uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr + #todo - there is probably no way around this but to somehow exec in the context of a completely separate console + #This is probably a tricky problem - especially to do cross-platform + # + # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit + if {[dict get $::tcl::UnknownOptions -code] == 0} { + set c green + set m "ok" + } else { + set c yellow + set m "errorCode $::errorCode" + } + set chunklist [list] + lappend chunklist [list "info" "[a $c]$m[a] " ] + if {$repl_runid != 0} { + tsv::lappend repl runchunks-$repl_runid {*}$chunklist + } + + dict incr ::tcl::UnknownOptions -level + return -options $::tcl::UnknownOptions $::tcl::UnknownResult + } proc runout {args} { #set_last_run_display [list] variable runout @@ -720,6 +755,7 @@ namespace eval shellrun { interp alias {} runx {} shellrun::runx interp alias {} sh_runx {} shellrun::sh_runx + interp alias {} runc {} shellrun::runconsole interp alias {} runraw {} shellrun::runraw diff --git a/src/modules/textblock-999999.0a1.0.tm b/src/modules/textblock-999999.0a1.0.tm index 05ad5bc0..51240eb3 100644 --- a/src/modules/textblock-999999.0a1.0.tm +++ b/src/modules/textblock-999999.0a1.0.tm @@ -3840,21 +3840,24 @@ tcl::namespace::eval textblock { } set cat_reactive_nonmetal [list H C N O F P S Cl Se Br I] - set ansi [a+ {*}$fc web-black Web-lightgreen] + #set ansi [a+ {*}$fc web-black Web-lightgreen] + set ansi [a+ {*}$fc black Term-113] set val [list ansi $ansi cat reactive_nonmetal] foreach e $cat_reactive_nonmetal { tcl::dict::set ecat $e $val } set cat [list Li Na K Rb Cs Fr] - set ansi [a+ {*}$fc web-black Web-Khaki] + #set ansi [a+ {*}$fc web-black Web-Khaki] + set ansi [a+ {*}$fc black Term-lightgoldenrod2] set val [list ansi $ansi cat alkali_metals] foreach e $cat { tcl::dict::set ecat $e $val } set cat [list Sc Ti V Cr Mn Fe Co Ni Cu Zn Y Zr Nb Mo Tc Ru Rh Pd Ag Cd Hf Ta W Re Os Ir Pt Au Hg Rf Db Sg Bh Hs] - set ansi [a+ {*}$fc web-black Web-lightsalmon] + #set ansi [a+ {*}$fc web-black Web-lightsalmon] + set ansi [a+ {*}$fc black Term-orange1] set val [list ansi $ansi cat transition_metals] foreach e $cat { tcl::dict::set ecat $e $val @@ -3868,7 +3871,8 @@ tcl::namespace::eval textblock { } set cat [list B Si Ge As Sb Te At] - set ansi [a+ {*}$fc web-black Web-turquoise] + #set ansi [a+ {*}$fc web-black Web-turquoise] + set ansi [a+ {*}$fc black Brightcyan] set val [list ansi $ansi cat metalloids] foreach e $cat { tcl::dict::set ecat $e $val @@ -3889,7 +3893,8 @@ tcl::namespace::eval textblock { } set cat [list La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu] - set ansi [a+ {*}$fc web-black Web-tan] + #set ansi [a+ {*}$fc web-black Web-tan] + set ansi [a+ {*}$fc black Term-tan] set val [list ansi $ansi cat lanthanoids] foreach e $cat { tcl::dict::set ecat $e $val @@ -3944,15 +3949,19 @@ tcl::namespace::eval textblock { $t configure \ -frametype_header light\ - -ansiborder_header [a+ {*}$fc web-white]\ - -ansibase_header [a+ {*}$fc Web-black]\ - -ansibase_body [a+ {*}$fc Web-black]\ - -ansiborder_body [a+ {*}$fc web-black]\ + -ansiborder_header [a+ {*}$fc brightwhite]\ + -ansibase_header [a+ {*}$fc Black]\ + -ansibase_body [a+ {*}$fc Black]\ + -ansiborder_body [a+ {*}$fc black]\ -frametype block + #-ansiborder_header [a+ {*}$fc web-white]\ + if {$opt_return eq "table"} { if {[dict get $opts -frame]} { - set output [textblock::frame -ansiborder [a+ {*}$fc Web-black web-cornflowerblue] -type heavy -title "[a+ {*}$fc Web-black] Periodic Table " [$t print]] + #set output [textblock::frame -ansiborder [a+ {*}$fc Black web-cornflowerblue] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] + #set output [textblock::frame -ansiborder [a+ {*}$fc Black term-deepskyblue2] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] + set output [textblock::frame -ansiborder [a+ {*}$fc Black cyan] -type heavy -title "[a+ {*}$fc Black] Periodic Table " [$t print]] } else { set output [$t print] } @@ -4260,8 +4269,8 @@ tcl::namespace::eval textblock { set textblock [textutil::tabify::untabify2 $textblock $tw] } if {[punk::ansi::ta::detect $textblock]} { - #stripansiraw slightly faster than stripansi - and won't affect width (avoid detect_g0/conversions) - set textblock [punk::ansi::stripansiraw $textblock] + #ansistripraw slightly faster than ansistrip - and won't affect width (avoid detect_g0/conversions) + set textblock [punk::ansi::ansistripraw $textblock] } if {[tcl::string::last \n $textblock] >= 0} { return [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] @@ -4277,7 +4286,7 @@ tcl::namespace::eval textblock { set tl $textblock } if {[punk::ansi::ta::detect $tl]} { - set tl [punk::ansi::stripansiraw $tl] + set tl [punk::ansi::ansistripraw $tl] } return [punk::char::ansifreestring_width $tl] } @@ -4312,9 +4321,9 @@ tcl::namespace::eval textblock { } set textblock [textutil::tabify::untabify2 $textblock $tw] } - #stripansiraw on entire block in one go rather than line by line - result should be the same - review - make tests + #ansistripraw on entire block in one go rather than line by line - result should be the same - review - make tests if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansiraw $textblock] + set textblock [punk::ansi::ansistripraw $textblock] } if {[tcl::string::last \n $textblock] >= 0} { #set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]] @@ -4343,16 +4352,16 @@ tcl::namespace::eval textblock { } set block [textutil::tabify::untabify2 $block $tw] if {[tcl::string::last \n $block] >= 0} { - return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [stripansi $v]}]] + return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [ansistrip $v]}]] } if {[catch {llength $block}]} { - return [::punk::char::string_width [stripansi $block]] + return [::punk::char::string_width [ansistrip $block]] } if {[llength $block] == 0} { #could be just a whitespace string return [tcl::string::length $block] } - return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [stripansi $v]}]] + return [tcl::mathfunc::max {*}[lmap v $block {::punk::char::string_width [ansistrip $v]}]] } #we shouldn't make textblock depend on the punk pipeline system @@ -4433,9 +4442,21 @@ tcl::namespace::eval textblock { set lines [list] + set padcharsize [punk::ansi::printing_length $padchar] + set pad_has_ansi [punk::ansi::ta::detect $padchar] if {$block eq ""} { #we need to treat as a line - return [tcl::string::repeat $padchar $width] + set repeats [expr {int(ceil($width / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + #TODO + #review - what happens when padchar has ansi, or the width would split a double-wide unicode char? + #we shouldn't be using string range if there is ansi - (overtype? ansistring range?) + #we should use overtype with suitable replacement char (space?) for chopped double-wides + if {!$pad_has_ansi} { + return [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $width-1] + } else { + set base [tcl::string::repeat " " $width] + return [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } } #review - tcl format can only pad with zeros or spaces? @@ -4475,6 +4496,7 @@ tcl::namespace::eval textblock { } set line_chunks [list] set line_len 0 + set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad foreach {pt ansi} $parts { if {$pt ne ""} { set has_nl [expr {[tcl::string::last \n $pt]>=0}] @@ -4489,12 +4511,26 @@ tcl::namespace::eval textblock { foreach pl $partlines { lappend line_chunks $pl #incr line_len [punk::char::ansifreestring_width $pl] - incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak + incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW if {$p != $last} { #do padding set missing [expr {$width - $line_len}] if {$missing > 0} { - set pad [tcl::string::repeat $padchar $missing] + #commonly in a block - many lines will have the same pad - cache based on missing + + #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] + } else { + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } + dict set pad_cache $missing $pad + } switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad @@ -4551,7 +4587,18 @@ tcl::namespace::eval textblock { #pad last line set missing [expr {$width - $line_len}] if {$missing > 0} { - set pad [tcl::string::repeat $padchar $missing] + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] + } else { + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] + } + } + #set pad [tcl::string::repeat $padchar $missing] switch -- $which-$opt_withinansi { r-0 { lappend line_chunks $pad @@ -7156,7 +7203,7 @@ tcl::namespace::eval textblock { #return [list $b1 $b2 $result] return [ansistring VIEW $result] } - tcl::namespace::import ::punk::ansi::stripansi + tcl::namespace::import ::punk::ansi::ansistrip } diff --git a/src/punk86.vfs/boot.tcl b/src/punk86.vfs/boot.tcl new file mode 100644 index 00000000..7e22b443 --- /dev/null +++ b/src/punk86.vfs/boot.tcl @@ -0,0 +1,130 @@ +proc tclInit {} { + rename tclInit {} + + global auto_path tcl_library tcl_libPath tcl_version tclkit_system_encoding + + # find the file to mount. + set noe $::tcl::kitpath + # resolve symlinks + set noe [file dirname [file normalize [file join $noe __dummy__]]] + set tcl_library [file join $noe lib tcl$tcl_version] + set tcl_libPath [list $tcl_library [file join $noe lib]] + + # get rid of a build residue + unset -nocomplain ::tclDefaultLibrary + + # The following code only gets executed if we don't have our exe + # already mounted. This should only happen once per thread. + # We could use [vfs::filesystem info], but that would require + # loading vfs into every interp. + if {![file isdirectory $noe]} { + load {} vfs + + # lookup and emulate "source" of lib/vfs1*/{vfs*.tcl,mk4vfs.tcl} + if {[llength [info command mk::file]]} { + set driver mk4 + + # must use raw Metakit calls because VFS is not yet in place + set d [mk::select exe.dirs parent 0 name lib] + set d [mk::select exe.dirs parent $d -glob name vfs1*] + + foreach x {vfsUtils vfslib mk4vfs} { + set n [mk::select exe.dirs!$d.files name $x.tcl] + if {[llength $n] != 1} { error "$x: cannot find startup script"} + + set s [mk::get exe.dirs!$d.files!$n contents] + catch {set s [zlib decompress $s]} + uplevel #0 $s + } + + # use on-the-fly decompression, if mk4vfs understands that + # Note: 8.6 core zlib does not support this for mk4vfs + if {![package vsatisfies [package require Tcl] 8.6]} { + set mk4vfs::zstreamed 1 + } + } else { + set driver mkcl + + # use raw Vlerq calls if Mk4tcl is not available + # $::vlerq::starkit_root is set in the init script in kitInit.c + set rootv [vlerq get $::vlerq::starkit_root 0 dirs] + set dname [vlerq get $rootv * name] + set prows [vlerq get $rootv * parent] + foreach r [lsearch -int -all $prows 0] { + if {[lindex $dname $r] eq "lib"} break + } + + # glob for a subdir in "lib", then source the specified file inside it + foreach {d f} { + vfs1* vfsUtils.tcl vfs1* vfslib.tcl vqtcl4* mkclvfs.tcl + } { + foreach z [lsearch -int -all $prows $r] { + if {[string match $d [lindex $dname $z]]} break + } + + set files [vlerq get $rootv $z files] + set names [vlerq get $files * name] + + set n [lsearch $names $f] + if {$n < 0} { error "$d/$f: cannot find startup script"} + + set s [vlerq get $files $n contents] + catch {set s [zlib decompress $s]} + uplevel #0 $s + } + + # hack the mkcl info so it will know this mount point as "exe" + set vfs::mkcl::v::rootv(exe) $rootv + set vfs::mkcl::v::dname(exe) $dname + set vfs::mkcl::v::prows(exe) $prows + } + + # mount the executable, i.e. make all runtime files available + vfs::filesystem mount $noe [list ::vfs::${driver}::handler exe] + + # alter path to find encodings + if {[info tclversion] eq "8.4"} { + load {} pwb + librarypath [info library] + } else { + encoding dirs [list [file join [info library] encoding]] ;# TIP 258 + } + # if the C code passed us a system encoding, apply it here. + if {[info exists tclkit_system_encoding]} { + # It is possible the chosen encoding is unavailable in which case + # we will be left with 'identity' to be handled below. + catch {encoding system $tclkit_system_encoding} + unset tclkit_system_encoding + } + # fix system encoding, if it wasn't properly set up (200207.004 bug) + if {[encoding system] eq "identity"} { + switch $::tcl_platform(platform) { + windows { encoding system cp1252 } + macintosh { encoding system macRoman } + default { encoding system iso8859-1 } + } + } + + # now remount the executable with the correct encoding + vfs::filesystem unmount $noe + set noe $::tcl::kitpath + # resolve symlinks + set noe [file dirname [file normalize [file join $noe __dummy__]]] + + set tcl_library [file join $noe lib tcl$tcl_version] + set tcl_libPath [list $tcl_library [file join $noe lib]] + vfs::filesystem mount $noe [list ::vfs::${driver}::handler exe] + } + + # load config settings file if present + namespace eval ::vfs { variable tclkit_version 1 } + catch { uplevel #0 [list source [file join $noe config.tcl]] } + + uplevel #0 [list source [file join $tcl_library init.tcl]] + + # reset auto_path, so that init.tcl's search outside of tclkit is cancelled + set auto_path $tcl_libPath + # Ditto for Tcl module search path + tcl::tm::path remove {*}[tcl::tm::path list] + tcl::tm::roots [list [file join $noe lib]] +} diff --git a/src/punk86.vfs/config.tcl b/src/punk86.vfs/config.tcl new file mode 100644 index 00000000..1069b037 --- /dev/null +++ b/src/punk86.vfs/config.tcl @@ -0,0 +1 @@ +set ::vfs::tclkit_version 200611.001 diff --git a/src/punk86.vfs/lib/app-punk/repl.tcl b/src/punk86.vfs/lib/app-punk/repl.tcl index 3fc2b640..6e9d5ca5 100644 --- a/src/punk86.vfs/lib/app-punk/repl.tcl +++ b/src/punk86.vfs/lib/app-punk/repl.tcl @@ -14,6 +14,8 @@ package provide app-punk 1.0 set original_tm_list [tcl::tm::list] tcl::tm::remove {*}$original_tm_list +set module_folders [list] + #tm list first added end up later in the list - and then override earlier ones if version the same - so add pwd-relative 1st to give higher priority #(only if Tcl has scanned all paths - see below bogus package load) #1 @@ -21,6 +23,8 @@ if {[file isdirectory [pwd]/modules]} { catch {tcl::tm::add [pwd]/modules} } +set tclmajorv [lindex [split [info tclversion] .] 0] + #2) if {[string match "*.vfs/*" [file normalize [info script]]]} { #src/xxx.vfs/lib/app-punk/repl.tcl @@ -28,18 +32,24 @@ if {[string match "*.vfs/*" [file normalize [info script]]]} { #set srcmodulefolder [file dirname [file dirname [file dirname [file dirname [file normalize [info script]]]]]]/modules # - the src/modules folder doesn't contain important modules such as vendormodules - so the above probably isn't that useful set srcfolder [file dirname [file dirname [file dirname [file dirname [file normalize [info script]]]]]] - set modulefolder [file join [file dirname $srcfolder] modules] ;#modules folder at same level as src folder + lappend module_folders [file join [file dirname $srcfolder] modules] ;#modules folder at same level as src folder + lappend module_folders [file join [file dirname $srcfolder] modules_tcl$tclmajorv] } else { # .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder) - set modulefolder [file dirname [file dirname [info nameofexecutable]]]/modules + lappend module_folders [file dirname [file dirname [info nameofexecutable]]]/modules + lappend module_folders [file dirname [file dirname [info nameofexecutable]]]/modules_tcl$tclmajorv } - -if {[file isdirectory $modulefolder]} { - tcl::tm::add $modulefolder -} else { - puts stderr "Warning unable to find module folder at: $modulefolder" +foreach modulefolder $module_folders { + if {[file isdirectory $modulefolder]} { + tcl::tm::add $modulefolder + } else { + puts stderr "Warning unable to find module folder at: $modulefolder" + } } + +#TODO! lib_tcl8 lib_tcl9 etc based on tclmajorv + #libs are appended to end - so add higher prioriy libraries last (opposite to modules) #auto_path - add exe-relative after exe-relative path if {"windows" eq $::tcl_platform(platform)} { diff --git a/src/punk86.vfs/lib/gridplus2.11/LICENSE.GRIDPLUS b/src/punk86.vfs/lib/gridplus2.11/LICENSE.GRIDPLUS deleted file mode 100644 index 668f818b..00000000 --- a/src/punk86.vfs/lib/gridplus2.11/LICENSE.GRIDPLUS +++ /dev/null @@ -1,36 +0,0 @@ -This software (GRIDPLUS) is Copyright (c) 2004-2015 by Adrian Davis (adrian@satisoft.com). - -The author hereby grants permission to use, copy, modify, distribute, -and license this software and its documentation for any purpose, provided -that existing copyright notices are retained in all copies and that -this notice is included verbatim in any distributions. No written agreement, -license, or royalty fee is required for any of the authorized uses. -Modifications to this software may be copyrighted by their authors -and need not follow the licensing terms described here, provided that -the new terms are clearly indicated on the first page of each file -where they apply. - -IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY -FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY -DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE POSSIBILITY -OF SUCH DAMAGE. - -THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE -IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE -NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, -OR MODIFICATIONS. - -GOVERNMENT USE: If you are acquiring this software on behalf of the -U.S. government, the Government shall have only "Restricted Rights" -in the software and related documentation as defined in the Federal -Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you -are acquiring the software on behalf of the Department of Defense, -the software shall be classified as "Commercial Computer Software" -and the Government shall have only "Restricted Rights" as defined in -Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, -the authors grant the U.S. Government and others acting in its behalf -permission to use and distribute the software in accordance with the -terms specified in this license. \ No newline at end of file diff --git a/src/punk86.vfs/lib/gridplus2.11/gridplus.tcl b/src/punk86.vfs/lib/gridplus2.11/gridplus.tcl deleted file mode 100644 index f57ba821..00000000 --- a/src/punk86.vfs/lib/gridplus2.11/gridplus.tcl +++ /dev/null @@ -1,6871 +0,0 @@ -#========================================================================# -# SCRIPT : gridplus.tcl # -# PURPOSE: Gridplus layout manager. # -# AUTHOR : Adrian Davis # -# : Incudes code from tile "combobox.tcl" by Joe English. # -# VERSION: 2.11 # -# DATE : 27/11/2015 # -#------------------------------------------------------------------------# -# HISTORY: 2.0 07/10/2006 - First release of Tile based GRIDPLUS. # -# : 2.1 24/02/2007 - Enchanced gpmap: Array mapping. # -# : - Documents gpinsert and gpselect. # -# : - Adds Container. # -# : - Removes special main/title condition. # -# : - Adds notebook "-command" option. # -# : - Fix tablelist sort problem. # -# : - Adds text "-font" option. # -# : 2.2 22/07/2007 - Change gpmap to set dropdown value not list.# -# : - Adds "-icons" option for tree. # -# : - Fix padding problem in layout. # -# : - Fix "container". # -# : - Changes "gridplus window" for container. # -# : 2.3 15/05/2008 - Adds Find dialog to text pop-up menu. # -# : - Adds "-labelanchor" option. # -# : - Adds "-validateauto" option. # -# : - Adds "-validate" for tablelist/tree. # -# : - Adds option to specify an event to "-ecmd". # -# : - Adds option to fix maximum entry characters.# -# : - Adds "popup" validation error messages. # -# : - Adds "?!" help text set to validation text. # -# : - Adds menu "underline" option. # -# : - Adds gpfind_dialog. # -# : - Adds gpfind, gpclear, gpcut, gpcopy and # -# : gppaste. # -# : - Adds "-topmost" option to "gridplus window".# -# : - Adds "-columnformat & -cfmt". # -# : - Change menu "=" as separator. # -# : - Change menu allow "~" to indicate command. # -# : - Fix problem with date validations. # -# : - Fix Validation in contained window problem. # -# : 2.4 05/02/2009 - Adds "-columnstretch". # -# : - Adds "-basename". # -# : - Adds new syntax for embedded grids. # -# : - Adds #style" widget option. # -# : - Adds radiobutton groups. # -# : - Adds "gridplus define". # -# : - Adds resize options to layout and "pack" # -# : command mode. # -# : - Adds "-command" to text - Triggered when # -# : text is modified. # -# : - Fix validate popup for toplevel windows. # -# : - Fix for "gpEditMenu" in contained windows. # -# : - Fix problem with validation for command # -# : invoked by pressing enter in entry. If a # -# : field has both a command and a validation # -# : specified, the validation will always be # -# : done when a command specified for the entry # -# : is invoked. # -# : - Fix problem setting dropdown using gpmap. # -# : - "gpselect" modified to "see" tablelist row. # -# : - Fix date validations. # -# : - Fix validation popup in notebooks. # -# : - Fix problem displaying label text when # -# : default widget is button/link/menubutton. # -# : 2.5 25/10/2009 - Adds "calendar" gridplus command mode. # -# : - Adds "dateselector" gridplus command mode. # -# : - Adds "gpnav" command. # -# : - Adds extra pre-defined entry validations. # -# : - Adds "trim:" option for entry validations. # -# : - Adds "!+" button wigdet option. # -# : - Adds "-overrideredirect" option for window. # -# : - Adds default (".") optionset. # -# : - Change gpset and gpselect to set values for # -# : "calendar" and "dateselector". # -# : - Change button widget so that Enter key will # -# : invoke the button command. # -# : - Change entry validation behaviour to work # -# : better losing focus to toplevel windows. # -# : - Fix entry validation losing focus to a # -# : toplevel window. # -# : - Fix entry validation popup messages in # -# : notebooks. # -# : - Fix "num" validation pattern. # -# : - Fix "expected integer" font problem due to # -# : Tcl/Tk bug. # -# : 2.6 23/10/2010 - Adds "single/space" option to tree. # -# : - Adds "ISO" date format. # -# : - Fix Unix container problem. # -# : 2.7 26/02/2012 - Adds option to set locale. # -# : - Adds "gpdefault" command. # -# : - Adds "gpdate" command. # -# : - Adds "=inline" entry/date default option. # -# : - Adds "tablelist" sort options. # -# : - Adds label width option. # -# : - Adds "Gridplus.optionsetDefaultStyle". # -# : - Fix date selector problem in topmost window.# -# : - Fix problem clearing radiobutton groups. # -# : - Fix modal flag clear problem. # -# : 2.8 28/03/2012 - Adds "=inline" dropdown default option. # -# : - Adds "~command" link option. # -# : - Change "checkbutton" so that the "+" option # -# : always results in a checked button. # -# : - Fix "gpset" to make sure window is updated. # -# : - Fix problem clearing "radiobutton" groups. # -# : - Fix link indent problem. # -# : - Fix gap in "theme" style border caused by # -# : ttk::labelframe bug. # -# : 2.9 04/07/2012 - Fix problem with value of tree node # -# : containing spaces. # -# : - Fix problem with "container" frame sizing. # -# : - Fix problem with some validations in # -# : "contained" toplevels. # -# : - Fix "clear" to withdraw validation pop-up # -# : message. # -# : 2.10 01/07/2013 - Adds "spinbox" gridplus command mode. # -# : - Adds "pane" gridplus command mode. # -# : - Adds "gpoptions" command. # -# : - Adds interface (and supporting procedures) # -# : to create user defined widget types for # -# : "widget" grid. # -# : - Adds "dateselector" option to display icon # -# : instead of downarrow. # -# : - Adds "-menu" option to "text". # -# : - Adds "-seeinsert" option to "text". # -# : - Adds "-seeinsert" option to "tablelist". # -# : - Adds "-takefocus" option to "tablelist". # -# : - Adds "-selectpage" option to "tablelist". # -# : - Adds "+" (focus) button widget option. # -# : - Adds new "gpselect" syntax/options. # -# : - Adds "-title" option to "gpset". # -# : - Adds "-name" option to "gpset". # -# : - Adds "gpmap" option to map to dict. # -# : - Adds Grid/Layout and Notebook command # -# : substitution. # -# : - Adds Popup/Balloon help display duration. # -# : - Change Popup/Balloon help to display at # -# : pointer position. # -# : - Change to allow "@" embedded widgets to # -# : work in embedded grids. # -# : - Change: Support for old "&w" embedded # -# : widget grid syntax removed. # -# : - Fix problem setting tablelist sort column # -# : when first column is integer/real. # -# : - Fix problem with tablelist row selection. # -# : - Fix problem with clipboard operations when # -# : widget with focus not of suitable type. # -# : - Fix problem with "gpfind" with patterns # -# : begining with "-". # -# : - Fix problem when selecting tablelist row # -# : using (Up and Down) cursor keys. # -# : - Fix menu separator problem with cascade # -# : style menus. # -# : - Code Tidy:- # -# : gpWidget rewritten/retructured/modularised. # -# : Four namespace variables eliminated. # -# : 2.11 27/11/2015 - Adds "gpdb" command. # -# : - Adds "gpdelete" command. # -# : - Adds "gpupdate" command. # -# : - Adds "gpget" command. # -# : - Adds "-save", "-restore", # -# : "-max", "-min", # -# : "-first", "-last", # -# : "-row" and "|" options to "gpselect". # -# : - Adds "-maintainsort" to "tablelist". # -# : - Adds true/false options for "tablelist" # -# : "-insertoptions". # -# : - Adds "tablelist" proc to return column # -# : values for selected row. # -# : - Adds "tablelist" "asciinocase" and # -# : "dictionary" column sort options. # -# : - Adds new "tree" "-selectfirst" option. # -# : - Adds new "tree" "-selectmode" option. # -# : - Adds widget option subsitution in embedded # -# : widget grid. # -# : - Adds new "layout" column/row weight setting # -# : syntax. # -# : - Adds new "notebook" "-padding" and # -# : "-tabpadding" options. # -# : - Adds new "grid" row "ns" stretch option. # -# : - Adds new "grid" "-attach ns" option. # -# : - Adds "buttonWidth" and "entryWidth" option # -# : database options. # -# : - Adds "gpset" "-|" dedent option. # -# : - Change "tablelist" to automatically set # -# : default column names. # -# : - Change "-insertexpr" to allow use of column # -# : names. # -# : - Change "gpselect" to allow use of column # -# : names. # -# : - Change "gpset" so that "-sortfirst" is # -# : disabled if there is a "saved" selection. # -# : - Change "gpunset" to allow patterns. # -# : - Fix "tree" keyboard traversal selection. # -# : - Fix problem with entry validation when # -# : using right-click menu in another entry. # -# : - Fix setting "checkbutton" default selected # -# : when "-state" is "disabled". # -# : - Fix "checkbutton" command options. # -# : - Fix setting "radiobutton" default selected # -# : when "-state" is "disabled". # -# : - Fix "dropdown" to use "-state" correctly. # -# : - Fix notebook pane name problem. # -# : - Fix problem with Text find dialog with # -# : patterns begining with "-". # -# : - Fix problem with "Date" clearing when # -# : "dateIcon" specified. # -########################################################################## - -package require Tk 8.5 - -package require msgcat -namespace import msgcat::* - -catch {package require icons} -catch {package require tablelist_tile} - -package provide gridplus 2.11 - -#=======================================================================# -# Export the public interface. # -#=======================================================================# - -namespace eval ::gridplus:: { - namespace export gridplus - namespace export gpcopy - namespace export gpclear - namespace export gpcut - namespace export gpdate - namespace export gpdb - namespace export gpdefault - namespace export gpdelete - namespace export gpfind - namespace export gpfind_dialog - namespace export gpget - namespace export gpinsert - namespace export gpmap - namespace export gpnav - namespace export gpoptions - namespace export gppaste - namespace export gpselect - namespace export gpset - namespace export gpunset - namespace export gpupdate - variable gpWidgetHelp - variable gpConfig - variable gpInfo - variable gpOptionSets - variable gpTabOrder - variable gpValidate - variable gpValidateError - variable gpValidations -} - -#=======================================================================# -# PROC : ::gridplus::gridplus # -# PURPOSE: Exported command. # -#=======================================================================# - -proc ::gridplus::gridplus {args} { - variable gpConfig - variable gpInfo - - # If first call run initialisation. - if {! [info exists gpConfig]} { - gpInit - } - - # Set array of valid/default options. - array set options [list \ - -action none \ - -anchor [=< anchor s] \ - -autogroup [=< autoGroup] \ - -attach [=< attach] \ - -background [=< background] \ - -borderwidth [=< borderWidth 2] \ - -basename {} \ - -calcolor [=< calColor black/white] \ - -calrelief [=< calRelief flat] \ - -calselectcolor [=< calSelectColor black/gray] \ - -ccmd {} \ - -century $gpConfig(date:century) \ - -cfmt [=< columnFormat] \ - -checkbuttoncommand {} \ - -columnformat [=< columnFormat] \ - -columnsort [=< columnSort 1] \ - -command {} \ - -compound left \ - -date {} \ - -dateclear [=< dateClear 1] \ - -datecommand {} \ - -dateformat $gpConfig(dateformat) \ - -dcmd {} \ - -Dcmd {} \ - -dropdowncommand {} \ - -ecmd [=< entryCommand] \ - -entrycommand [=< entryCommand] \ - -errormessage $gpConfig(errormessage) \ - -fileicon [=< fileIcon file] \ - -fixed [=< fixed 999999] \ - -foldericon [=< folderIcon folder] \ - -font [=< font] \ - -foreground [=< foreground black] \ - -from [=< from] \ - -group {} \ - -height [=< height 10] \ - -icon [=< icon] \ - -iconfile $gpConfig(iconfile) \ - -iconpath $gpConfig(iconpath) \ - -icons [=< icons 1] \ - -in {} \ - -increment [=< increment 1] \ - -insertexpr {} \ - -insertoptions {} \ - -justify left \ - -labelanchor [=< labelAnchor] \ - -labelcolor [=< labelColor /] \ - -labelstyle [=< labelStyle /] \ - -linerelief [=< lineRelief sunken] \ - -linewidth [=< lineWidth 2] \ - -linkcolor [=< linkColor black/black] \ - -linkcursor [=< linkCursor arrow] \ - -linkstyle [=< linkStyle /underline] \ - -listvariable {} \ - -locale $gpConfig(locale) \ - -maintainsort [=< mantainSort 0] \ - -menu {} \ - -minx 100 \ - -miny 100 \ - -modal 0 \ - -names {} \ - -navbar [=< navBar 1] \ - -navcommand {} \ - -navselect [=< navSelect 0] \ - -open [=< open 0] \ - -optionset {} \ - -overrideredirect 0 \ - -pad [=< pad 5] \ - -padding [=< padding {5 5 5 5}] \ - -padx [=< padX [=< pad 5]] \ - -pady [=< padY [=< pad 5]] \ - -pattern {} \ - -prefix $gpConfig(prefix) \ - -proc $gpConfig(proc) \ - -radiobuttoncommand {} \ - -rcmd {} \ - -relief [=< relief flat] \ - -resize {} \ - -scroll none \ - -seeinsert [=< seeInsert 0] \ - -selectfirst 0 \ - -selectmode [=< selectMode browse] \ - -selectpage [=< selectPage 0] \ - -selecttoday [=< selectToday 0] \ - -show [=< show tree] \ - -sortfirst 0 \ - -sortorder increasing \ - -space [=< space 20] \ - -spacestretch {} \ - -spinformat [=< spinFormat] \ - -state normal \ - -sticky [=< sticky] \ - -stretch {} \ - -style {} \ - -subst [=< subst 1] \ - -tableoptions {} \ - -taborder column \ - -tabpadding [=< tabPadding] \ - -takefocus 1 \ - -tags 0 \ - -text {} \ - -title {} \ - -to [=< to] \ - -topmost [=< topmost 0] \ - -validate [=< validate 0] \ - -validateauto [=< validateAuto 1] \ - -validatepopup [=< validatePopup 0] \ - -validation {} \ - -variable {} \ - -variables 1 \ - -wcmd {} \ - -weekstart [=< weekStart 1] \ - -widget [=< widget grid] \ - -width [=< width 40] \ - -windowcommand {} \ - -wrap word \ - -wraplength 0 \ - -wtitle {} \ - ] - - # Read mode. - set mode [lindex $args 0] - - # Validate mode and set parameter template. - switch -- $mode { - add {set argTemplate [list "name 1" "options 2 end"]} - button {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< buttonWidth [=< widgetWidth 10]]} - calendar {set argTemplate [list "name 1" "options 2 end"]} - checkbutton {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - clear {set argTemplate [list "name 1" "options 2 end"]} - container {set argTemplate [list "name 1" "options 2 end"];set options(-height) [=< containerHeight 200];set options(-width) [=< containerWidth 250]} - date {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - define {set argTemplate [list "layout 1"]} - dropdown {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - entry {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< entryWidth [=< widgetWidth 10]]} - goto {set argTemplate [list "name 1" "options 2 end-1" "layout end"]} - grid {set argTemplate [list "name 1" "options 2 end-1" "layout end"]} - init {set argTemplate [list "options 1 end"]} - layout {set argTemplate [list "name 1" "options 2 end-1" "layout end"]} - line {set argTemplate [list "name 1" "options 2 end"]} - link {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - menu {set argTemplate [list "name 1" "options 2 end-1" "layout end"]} - menubutton {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - notebook {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-padding) [=< notebookPadding]} - optionset {set argTemplate [list "name 1" "options 2 end-1" "layout end"]} - pack {set argTemplate [list "name 1" "options 2 end"]} - pane {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-height) [=< paneHeight 0];set options(-width) [=< paneWidth 0]} - radiobutton {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - set {set argTemplate [list "options 1 end"]} - spinbox {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - tablelist {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< tableWidth 40];set options(-takefocus) 0} - text {set argTemplate [list "name 1" "options 2 end"];set options(-width) [=< textWidth 40]} - tree {set argTemplate [list "name 1" "options 2 end"];set options(-width) [=< treeWidth 200];set options(-selectmode) [=< treeSelectMode extended]} - widget {set argTemplate [list "name 1" "options 2 end-1" "layout end"];set options(-width) [=< widgetWidth 10]} - window {set argTemplate [list "name 1" "options 2 end"]} - default {error "GRIDPLUS ERROR: Invalid mode ($mode)."} - } - - # Check if sufficient args. - if {[llength $args] < [llength $argTemplate]} { - error "GRIDPLUS ERROR: Wrong number of Args." - } - - # Check if sufficient args remain for option/value pairs. - if {$mode ne "define" && [expr {([llength $args] - [llength $argTemplate]) % 2}] != 0} { - error "GRIDPLUS ERROR: Unmatched option/value." - } - - # Unset gpUnknown. - foreach unknownItem [array names gpInfo *] { - unset gpInfo($unknownItem) - } - - # Read/validate arguments. - foreach template $argTemplate { - set argName [lindex $template 0] - set argStart [lindex $template 1] - set argEnd [lindex $template 2] - # If argName is "options" read option/value pairs. - if {$argName eq "options"} { - foreach {option value} [lrange $args $argStart $argEnd] { - if {[info exists options($option)]} { - switch -- $option { - -pad { - set options(-padx) $value - set options(-pady) $value - } - -title { - set options(-title) $value - if {$options(-title) ne ""} { - set options(-relief) theme - } - } - default { - set options($option) $value - } - } - } else { - if {[=< unknown 1]} { - set gpInfo($option) $value - } else { - error "GRIDPLUS ERROR: Invalid option ($option)." - } - } - } - } else { - set options($argName) [lindex $args $argStart] - } - } - - # Set optionset. - gpSetOptionset - - # Remove blank lines from "layout". - if {[info exists options(layout)]} { - regsub -all -- {\n\n} $options(layout) "\n" options(layout) - regsub -all -- {(^\n)|(\n$)|(\n +$)} $options(layout) "" options(layout) - } - - # Call appropriate procedure according to specified mode. - switch -- $mode { - add {gpAdd} - button {set options(-widget) b;gpWidget} - calendar {gpCalendar} - checkbutton {set options(-widget) c;gpWidget} - clear {gpClear} - container {gpContainer} - date {set options(-widget) D;gpWidget} - define {gpDefine} - dropdown {set options(-widget) d;gpWidget} - entry {set options(-widget) e;gpWidget} - goto {gpGoto} - grid {gpGrid} - layout {gpLayout} - line {gpLine} - link {set options(-widget) l;gpWidget} - menu {gpMenu} - menubutton {set options(-widget) m;gpWidget} - notebook {gpNotebook} - optionset {gpOptionset} - pack {gpPack} - pane {gpPane} - radiobutton {set options(-widget) r;gpWidget} - set {gpSet} - spinbox {set options(-widget) s;gpWidget} - tablelist {gpTablelist} - text {gpText} - tree {gpTree} - widget {gpWidget} - window {gpWindow} - } - -} - -#=======================================================================# -# PROC : ::gridplus::gpWidget # -# PURPOSE: Create widget grid. # -#=======================================================================# - -proc ::gridplus::gpWidget {} { - upvar 1 options globaloptions - - array set options [array get globaloptions] - - global {} - - variable gpConfig - variable gpInfo - variable gpValidation - variable gpValidations - - if {$options(-fixed) eq ""} { - set defaultFixed $options(-width) - } else { - set defaultFixed $options(-fixed) - } - - if {$options(-basename) eq ""} { - set basename $options(name) - } else { - set basename $options(-basename) - } - - set defaultWidget [string range $options(-widget) 0 0] - set gridData {} - set rowCount 0 - set widgetID 1 - - if {! [regexp -- {^[.]([^.]+)[.]} $options(name) -> window]} { - set window {} - } - - foreach row [split $options(layout) "\n"] { - set columnCount 0 - foreach column $row { - set action 0 - set createWidget 0 - set errorMessage {} - set fixed $defaultFixed - set gridColumn {} - set itemFixed {} - set itemWidth {} - set state $options(-state) - set style $options(-style) - set widget $defaultWidget - set widgetHelp {} - set widgetOptions [dict create widget options] - set width $options(-width) - - if {$options(-autogroup) ne ""} {} - - set column [::gridplus::gpDefineWidget $column] - set column [::gridplus::gpParseEmbeddedGrid $column] - - foreach item $column { - switch -regexp -- $item { - - ^[&]=[a-zA-Z] { - set widget "=" - set userWidget [string range $item 2 2] - set widgetLayout [lrange $item 2 end] - regexp {^[&]=[^: ]+:([^ ]*)} $item -> style - } - ^[&] { - set widgetLayout [lrange $item 1 end] - if {! [regexp {^[&]([^: ]+):([^ ]*)} $item -> widget style]} { - set widget [lindex [string range $item 1 end] 0] - } - if {$widget eq "&" && $style eq ""} { - set style "{}" - } - if {$widget eq "d" && $options(-state) eq "normal"} { - set state readonly - } - } - ^[.] { - set createWidget 1 - if {[regexp -- {(^[.]$)|(^[.]:)} $item]} { - if {$widget eq "&"} { - regsub -- {[.]} $item $options(name)-$widgetID item - } else { - regsub -- {[.]} $item [regsub -- {([^.]+)[.]} $options(name)-$widgetID {\1_-_}] item - } - incr widgetID - } - if {! [regexp {(^[^:]+)(:[nsewc]+$)} $item -> item sticky]} {set sticky {}} - if {$widget in "g &"} { - set widgetName $item - } else { - set widgetName $basename,[string range $item 1 end] - } - if {$options(-autogroup) ne ""} {dict set widgetOptions > "::gridplus::gpAutoGroup $widgetName $options(-autogroup) normal"} - if {$options(-group) ne ""} {set gpInfo($widgetName:group) $options(-group)} - lappend gridColumn $widgetName$sticky - } - ^: { - dict set widgetOptions : [string range $item 1 end] - if {$widget in "b m"} { - if {! $createWidget} { - set createWidget 1 - set widgetName $options(name),[= $widgetOptions :] - if {$options(-group) ne ""} {set gpInfo($widgetName:group) $options(-group)} - lappend gridColumn $widgetName - } - } elseif {! $createWidget && $widget ne "l"} { - lappend gridColumn $item%% - } - } - ^[0-9]+$ { - set width $item - } - ^([0-9]*)/([0-9]*)$ { - regexp -- {^([0-9]*)/([0-9]*)$} $item -> itemWidth itemFixed - if {$itemWidth eq ""} { - set width $options(-width) - } else { - set width $itemWidth - } - if {$itemFixed eq ""} { - set fixed $width - } else { - set fixed $itemFixed - } - } - ^@ { - set gridName .[string range $item 1 end] - lappend gridColumn $gridName - } - ^% { - set gpInfo($widgetName:group) [string range $item 1 end] - } - ^[-+*~!] { - dict set widgetOptions [string range $item 0 0] [string range $item 1 end] - } - ^[?] { - set widgetHelp [mc [string range $item 1 end]] - } - ^[|]$ { - lappend gridColumn $item - } - ^[=]$ { - lappend gridColumn $item - } - ^[=].+ { - dict set widgetOptions = [string range $item 1 end] - } - ^<$ { - set state disabled - } - ^>$ { - set state normal - } - ^<.+ { - ::gridplus::gridplus set -group [string range $item 1 end] -state normal - dict set widgetOptions < "::gridplus::gpAutoGroup $widgetName [string range $item 1 end] disabled" - } - ^>.+ { - ::gridplus::gridplus set -group [string range $item 1 end] -state disabled - dict set widgetOptions > "::gridplus::gpAutoGroup $widgetName [string range $item 1 end] normal" - } - ^[#].* { - set style [string range $item 1 end] - } - default { - if {$widget in "b l m"} { - if {[llength $column] > 1} { - dict set widgetOptions text [mc $item] - } else { - lappend gridColumn $item - } - } else { - lappend gridColumn $item - } - } - } - } - - switch -glob -- $widget { - [cbdDelmrs] { - #---------------# - # Create widget # - #---------------# - if {$createWidget} { - ::gridplus::widget:$widget $widgetName $window $basename $style $width $fixed [=% $widgetName $state] $widgetOptions - } - } - [=] { - #----------------------------# - # Create user defined widget # - #----------------------------# - if {$createWidget} { - ::gridplus::widget:=$userWidget $widgetName $window $basename $style $width $fixed [=% $widgetName $state] $widgetOptions - } - } - & { - #-------------------------------# - # Create embedded "widget" grid # - #-------------------------------# - set stretch [lindex $widgetLayout 0] - set widgetWidget [lindex $widgetLayout 1] - set widgetStyle [lindex $widgetLayout 2] - set widgetLayout [lrange $widgetLayout 3 end] - if {$widgetStyle ne ""} { - if {$widgetStyle eq "%"} { - set style "{}" - } else { - set style $widgetStyle - } - } - set widgetCommand "::gridplus::gridplus widget $widgetName -basename $basename -borderwidth 0 -spacestretch [list $stretch] -pad 0 -padding {0 0 0 0} -style $style -widget $widgetWidget [list $widgetLayout]" - eval $widgetCommand - } - } - - if {$widgetHelp ne ""} { - if {$widgetHelp eq "!"} { - set widgetHelp [::gridplus::gpValidateText [= $widgetOptions !]] - } - gpWidgetHelpInit $widgetName $widgetHelp - } - - lappend gridData $gridColumn - incr columnCount - } - lappend gridData !!!! - incr rowCount - } - - regsub -all {!!!!} $gridData \n gridData - - set gridCommand "::gridplus::gridplus grid $options(name)" - - foreach option [array names options -*] { - set gridCommand "$gridCommand $option {$options($option)}" - } - - set gridCommand "$gridCommand {$gridData}" - - eval $gridCommand -} - -#=======================================================================# -# PROC : ::gridplus::widget:b # -# PURPOSE: Create button widget. # -#=======================================================================# - -proc ::gridplus::widget:b {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - variable gpInfo - - set command [= $widgetOptions ~] - set icon [= $widgetOptions :] - set text [= $widgetOptions text] - - set gpInfo($name:validationmode) force - set doValidation $options(-validate) - - if {[=? $widgetOptions !]} { - set doValidation 1 - if {[= $widgetOptions !] eq "+" } { - set gpInfo($name:validationmode) focus - } - } - - if {$command ne ""} { - set buttonCommand $command - } else { - if {[regexp -- {^([^=]*)=(.*)$} $name -> buttonCommand buttonParameter]} { - set buttonCommand "$buttonCommand $buttonParameter" - } else { - set buttonCommand "$name" - } - } - - if {$options(-proc)} { - set command "set gridplus::gpInfo() \[focus\];gpProc [::gridplus::gpCommandFormat $buttonCommand]" - } else { - set command "set gridplus::gpInfo() \[focus\];$options(-prefix)[::gridplus::gpCommandFormat $buttonCommand]" - } - - if {$icon ne ""} { - if {$text eq ""} { - ::ttk::button $name -command "::gridplus::gpCommand {$command} .$window $doValidation" -image [=: $icon] -state $state -style $style -takefocus $options(-takefocus) - } else { - ::ttk::button $name -command "::gridplus::gpCommand {$command} .$window $doValidation" -image [=: $icon] -state $state -style $style -takefocus $options(-takefocus) -text $text -width $width -compound $options(-compound) - } - } else { - ::ttk::button $name -command "::gridplus::gpCommand {$command} .$window $doValidation" -state $state -style $style -takefocus $options(-takefocus) -text $text -width $width - } - - if {$state eq "disabled"} {$name configure -takefocus 0} - - if {[=? $widgetOptions +]} {focus $name} - - bind $name "$name invoke" -} - -#=======================================================================# -# PROC : ::gridplus::widget:c # -# PURPOSE: Create checkbutton widget. # -#=======================================================================# - -proc ::gridplus::widget:c {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - global {} - - set command [= $widgetOptions ~] - set ($name) [= $widgetOptions = [=@ $name 0]] - - set options(-checkbuttoncommand) [::gridplus::gpOptionAlias -checkbuttoncommand -ccmd] - - ::ttk::checkbutton $name -offvalue 0 -onvalue 1 -style $style -takefocus $options(-takefocus) -variable ($name) - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } - - if {[=? $widgetOptions ~]} { - if {$command eq ""} { - set command $name - } - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set command "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - $name configure -command $command - } elseif {$options(-checkbuttoncommand) ne ""} { - if {$options(-proc)} { - set command "gpProc $options(-checkbuttoncommand)" - } else { - set command "$options(-prefix)$options(-checkbuttoncommand)" - } - $name configure -command $command - } - - if {[=? $widgetOptions +]} { - set ($name) 0 - $name invoke - } - - $name configure -state $state -} - -#=======================================================================# -# PROC : ::gridplus::widget:d # -# PURPOSE: Create dropdown widget. # -#=======================================================================# - -proc ::gridplus::widget:d {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - global {} - - set command [= $widgetOptions ~] - set values [= $widgetOptions +] - set ($name) [= $widgetOptions = [=@ $name [lindex [= $widgetOptions +] 0]]] - - set options(-dropdowncommand) [::gridplus::gpOptionAlias -dropdowncommand -dcmd] - - ::ttk::combobox $name -state $state -style $style -takefocus $options(-takefocus) -textvariable ($name) -values $values -width $width - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } - - if {[=? $widgetOptions ~]} { - if {$command eq ""} { - set command $name - } - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set command "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - bind $name <> $command - } elseif {$options(-dropdowncommand) ne ""} { - if {$options(-proc)} { - set command "gpProc $options(-dropdowncommand)" - } else { - set command "$options(-prefix)$options(-dropdowncommand)" - } - bind $name <> "$command" - } -} - -#=======================================================================# -# PROC : ::gridplus::widget:D # -# PURPOSE: Create dateselector widget. # -#=======================================================================# - -proc ::gridplus::widget:D {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - variable gpInfo - - global {} - - set command [= $widgetOptions ~] - set ($name) [::gridplus::gpdate [= $widgetOptions = [=@ $name]]] - - if {$state eq "normal"} { - set state readonly - } - - set options(-datecommand) [::gridplus::gpOptionAlias -datecommand -Dcmd] - - if {[=< dateIcon] ne ""} { - if {"GridplusDate.downarrow" ni [ttk::style element names]} { - set normalIcon [=: [=< dateIcon]] - set disabledIcon [image create photo] - - ::ttk::combobox .gpComboboxHeight - set height [winfo reqheight .gpComboboxHeight] - destroy .gpComboboxHeight - - $disabledIcon copy $normalIcon - $disabledIcon configure -palette 16 -gamma 1.5 - - ::ttk::style element create GridplusDate.downarrow image [list $normalIcon disabled $disabledIcon] -height $height -sticky e - - ::ttk::style layout GridplusDate.TCombobox { - Combobox.field -sticky nswe -children { - GridplusDate.downarrow -side right -sticky ns - Combobox.padding -expand 1 -sticky nswe -children { - Combobox.textarea -sticky nswe - } - } - } - } - - set style "GridplusDate.TCombobox" - } - - ::ttk::combobox $name -state $state -style $style -takefocus $options(-takefocus) -textvariable ($name) -width $width - - bind $name "::gridplus::gpDateSelectorKeyPress $name %W post" - bind $name "::gridplus::gpDateSelectorKeyPress $name %W unpost" - bind $name "::gridplus::gpDateSelectorToggle $name %W" - bind $name "$name selection range 0 end" - bind $name "::gridplus::gpEntryEdit {} %X %Y" - - if {$options(-dateclear)} { - bind $name "::gridplus::gpDateSelectorClear $name %K" - } - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } - - set gpInfo($name:datecommand) {} - - if {[=? $widgetOptions ~]} { - if {$command eq ""} { - set gpInfo($name:datecommand) $name - } - if {$options(-proc)} { - set gpInfo($name:datecommand) "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set gpInfo($name:datecommand) "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - } elseif {$options(-datecommand) ne ""} { - if {$options(-proc)} { - set gpInfo($name:datecommand) "gpProc $options(-datecommand)" - } else { - set gpInfo($name:datecommand) "$options(-prefix)$options(-datecommand)" - } - } -} - -#=======================================================================# -# PROC : ::gridplus::widget:e # -# PURPOSE: Create entry widget. # -#=======================================================================# - -proc ::gridplus::widget:e {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - variable gpInfo - variable gpValidations - - global {} - - set autoGroupCommand [= $widgetOptions > [= $widgetOptions <]] - set command [= $widgetOptions ~ $name] - set validation [= $widgetOptions !] - set ($name) [= $widgetOptions = [=@ $name]] - - set options(-entrycommand) [::gridplus::gpOptionAlias -entrycommand -ecmd] - - if {$state eq "disabled"} { - set state [=< entryDisabled readonly] - } - - if {[=? $widgetOptions !]} { - set doValidation 1 - lappend gpValidations(.$window) $name:$validation - } else { - set doValidation 0 - } - - if {$validation eq ""} { - set validation "__gpFixed__" - } else { - ::gridplus::gpValidateErrorInit $name [::gridplus::gpValidateText $validation] - } - - ::ttk::entry $name -invalidcommand "::gridplus::gpValidateFailed %W" -state $state -style $style -takefocus $options(-takefocus) -textvariable ($name) -validate all -validatecommand "::gridplus::gpValidate %W \"$validation\" %V %P $fixed $options(-validateauto)" -width $width - - if {$state eq "disabled"} { - $name configure -background lightgray -takefocus 0 - } - - if {[=? $widgetOptions ~]} { - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set command "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - if {[string match <*> $command]} { - bind $name "event generate $name $command" - } elseif {[string match "<*> *" $command]} { - regsub -all {:} $command "." command - bind $name "event generate [lindex $command 1] [lindex $command 0]" - } else { - bind $name "::gridplus::gpCommand {$command} .$window $doValidation" - } - } elseif {$options(-entrycommand) ne ""} { - if {$options(-proc)} { - set command "gpProc $options(-entrycommand)" - } else { - set command "$options(-prefix)$options(-entrycommand)" - } - if {[string match <*> $command]} { - bind $name "event generate $name $command" - } elseif {[string match "<*> *" $command]} { - regsub -all {:} $command "." command - bind $name "event generate [lindex $command 1] [lindex $command 0]" - } else { - bind $name "::gridplus::gpCommand {$command} .$window $doValidation" - } - } - - if {$autoGroupCommand ne ""} { - trace add variable ($name) write $autoGroupCommand - } - - if {$options(-validatepopup) && $validation ne "__gpFixed__"} { - ::gridplus::gpValidateErrorInit $name [::gridplus::gpValidateText $validation] popup - } - - if {[=? $widgetOptions *]} {$name configure -show "*"} - if {[=? $widgetOptions +]} {focus $name} - - bind $name "::gridplus::gpEntryEdit {$window} %X %Y" - -} - -#=======================================================================# -# PROC : ::gridplus::widget:l # -# PURPOSE: Create link widget. # -#=======================================================================# - -proc ::gridplus::widget:l {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - set command [= $widgetOptions ~ $name] - set icon [= $widgetOptions :] - set text [= $widgetOptions text] - - foreach {normalColor overColor} [split $options(-linkcolor) /] {} - foreach {normalStyle overStyle} [split $options(-linkstyle) /] {} - - regsub -- {[&]} $overStyle $normalStyle, overStyle - regsub -all -- {,} $normalStyle { } normalStyle - regsub -all -- {,} $overStyle { } overStyle - - if {! [string match */* $options(-linkcolor)]} {set overColor $normalColor} - - if {$normalColor eq ""} {set normalColor "black"} - if {$overColor eq ""} {set overColor "black"} - - if {[=? $widgetOptions !]} { - set doValidation 1 - } else { - set doValidation 0 - } - - if {[=? $widgetOptions -]} { - set indent " " - } elseif {[=? $widgetOptions +]} { - set indent "\u2022 " - } else { - set indent "" - } - - if {$options(-proc)} { - set linkCommand "set gridplus::gpInfo() \[focus\];gpProc [::gridplus::gpCommandFormat $command]" - } else { - set linkCommand "set gridplus::gpInfo() \[focus\];$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - - ::ttk::frame $name - ::ttk::label $name.link -background $options(-background) -foreground $options(-foreground) -text [mc $text] - - set normalFont [::gridplus::gpSetFont $normalStyle] - set overFont [::gridplus::gpSetFont $overStyle] - - $name.link configure -font $normalFont -foreground $normalColor - - bind $name.link "$name.link configure -font {$overFont} -foreground $overColor -cursor $options(-linkcursor)" - bind $name.link "$name.link configure -font {$normalFont} -foreground $normalColor -cursor {}" - bind $name.link "eval \"::gridplus::gpCommand {$linkCommand} .$window $doValidation\"" - - if {[=? $widgetOptions :]} { - if {$icon eq ""} {set icon $options(-icon)} - ::ttk::label $name.icon -image [=: $icon] - bind $name.icon "$name.icon configure -cursor $options(-linkcursor)" - bind $name.icon "$name.icon configure -cursor {}" - bind $name.icon "eval \"::gridplus::gpCommand {$linkCommand} .$window $doValidation\"" - grid $name.icon $name.link - } else { - ::ttk::label $name.indent -background $options(-background) -foreground $options(-foreground) -text $indent - grid $name.indent $name.link - } -} - -#=======================================================================# -# PROC : ::gridplus::widget:m # -# PURPOSE: Create menubutton widget. # -#=======================================================================# - -proc ::gridplus::widget:m {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - set icon [= $widgetOptions :] - set text [= $widgetOptions text] - - set menu "$name:menu" - - if {$icon ne ""} { - if {$text eq ""} { - ::ttk::menubutton $name -menu $menu -image [=: $icon] -state $state -style $style -takefocus $options(-takefocus) - } else { - ::ttk::menubutton $name -menu $menu -image [=: $icon] -state $state -style $style -takefocus $options(-takefocus) -text $text -width $width -compound $options(-compound) - } - } else { - ::ttk::menubutton $name -menu $menu -state $state -style $style -takefocus $options(-takefocus) -text $text -width $width - } - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } -} - -#=======================================================================# -# PROC : ::gridplus::widget:r # -# PURPOSE: Create radiobutton widget. # -#=======================================================================# - -proc ::gridplus::widget:r {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - variable gpInfo - - global {} - - set command [= $widgetOptions ~] - set group [= $widgetOptions *] - set value [= $widgetOptions + [= $widgetOptions -]] - - if {[=? $widgetOptions *]} { - set group ",$group" - } else { - set group {} - } - if {$basename eq ""} { - set variable "$options(name)$group" - if {$group ne ""} {set gpInfo($options(name):radiobuttonGroups) [lappend gpInfo($options(name):radiobuttonGroups) $group]} - } else { - set variable "$basename$group" - if {$group ne ""} {set gpInfo($basename:radiobuttonGroups) [lappend gpInfo($basename:radiobuttonGroups) $group]} - } - - set ($variable) {} - - set options(-radiobuttoncommand) [::gridplus::gpOptionAlias -radiobuttoncommand -rcmd] - - ::ttk::radiobutton $name -style $style -takefocus $options(-takefocus) -value $value -variable ($variable) - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } - - if {[=? $widgetOptions +] || [=@ $variable] eq $value} { - after idle "$name invoke; $name configure -state $state" - } else { - $name configure -state $state - } - - if {[=? $widgetOptions ~]} { - if {$command eq ""} { - set command $name - } - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set command "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - $name configure -command $command - } elseif {$options(-radiobuttoncommand) ne ""} { - if {$options(-proc)} { - set command "gpProc $options(-radiobuttoncommand)" - } else { - set command "$options(-prefix)$options(-radiobuttoncommand)" - } - $name configure -command $command - } -} - -#=======================================================================# -# PROC : ::gridplus::widget:s # -# PURPOSE: Create spinbox widget. # -#=======================================================================# - -proc ::gridplus::widget:s {name window basename style width fixed state widgetOptions} { - upvar 1 options options - - variable gpInfo - - global {} - - set value [= $widgetOptions +] - set ($name) [= $widgetOptions = [=@ $name]] - - if {$state eq "normal"} { - set state readonly - } - - set from {} - set to {} - set increment {} - set format {} - - if {[string match */* $value]} { - foreach {from to increment format} [split $value /] {} - - if {$from eq ""} { - if {$options(-from) eq ""} { - error "GRIDPLUS ERROR: 'From' value not specified for spinbox \"$name\"." - } else { - set from $options(-from) - } - } - if {$to eq ""} { - if {$options(-to) eq ""} { - error "GRIDPLUS ERROR: 'To' value not specified for spinbox \"$name\"." - } else { - set to $options(-to) - } - } - if {$increment eq ""} { - if {$options(-increment) eq ""} { - error "GRIDPLUS ERROR: 'Increment' value not specified for spinbox \"$name\"." - } else { - set increment $options(-increment) - } - } - if {$format eq ""} { - set format $options(-spinformat) - } - - if {$($name) eq ""} { - set ($name) $from - } - - ::ttk::spinbox $name -state $state -style $style -takefocus $options(-takefocus) -textvariable ($name) -from $from -to $to -increment $increment -format $format -width $width - } else { - if {$($name) eq ""} { - set ($name) [lindex $value 0] - } - - ::ttk::spinbox $name -state $state -style $style -takefocus $options(-takefocus) -textvariable ($name) -values $value -width $width - } - - if {$state eq "disabled"} { - $name configure -takefocus 0 - } - - bind $name "::gridplus::gpEntryEdit {$window} %X %Y" -} - -#=======================================================================# -# PROC : ::gridplus::gpAdd # -# PURPOSE: Add non-gridplus widget to group. # -#=======================================================================# - -proc ::gridplus::gpAdd {} { - upvar 1 options options - - variable gpInfo - - set gpInfo($options(name):group) $options(-group) -} - -#=======================================================================# -# PROC : ::gridplus::gpAutoGroup # -# PURPOSE: Set group state when entry has been updated. # -#=======================================================================# - -proc ::gridplus::gpAutoGroup {name group state args} { - - global {} - - trace remove variable ($name) write "::gridplus::gpAutoGroup $name $group $state" - - ::gridplus::gridplus set -group $group -state $state -} - -#=======================================================================# -# PROCS : ::gridplus::gpWidgetHelpInit # -# : ::gridplus::gpWidgetHelpDelay # -# : ::gridplus::gpWidgetHelpCancel # -# : ::gridplus::gpWidgetHelpShow # -# PURPOSE: Gridplus widget help. # -#=======================================================================# - -proc ::gridplus::gpWidgetHelpInit {item message} { - variable gpWidgetHelp - - if {! [winfo exists .gpWidgetHelp]} { - toplevel .gpWidgetHelp -background black -borderwidth 1 -relief flat - label .gpWidgetHelp.message -background lightyellow - pack .gpWidgetHelp.message - wm overrideredirect .gpWidgetHelp 1 - wm withdraw .gpWidgetHelp - } - - set gpWidgetHelp($item) $message - bind $item {::gridplus::gpWidgetHelpDelay %W} - bind $item {::gridplus::gpWidgetHelpCancel} -} - -proc ::gridplus::gpWidgetHelpDelay {item} { - variable gpWidgetHelp - - gpWidgetHelpCancel - set gpWidgetHelp(delay) [after 300 [list ::gridplus::gpWidgetHelpShow $item]] -} - -proc ::gridplus::gpWidgetHelpCancel {} { - variable gpWidgetHelp - - if {[info exists gpWidgetHelp(delay)]} { - after cancel $gpWidgetHelp(delay) - unset gpWidgetHelp(delay) - } - - if {[info exists gpWidgetHelp(show)]} { - after cancel $gpWidgetHelp(show) - unset gpWidgetHelp(show) - } - - if {[winfo exists .gpWidgetHelp]} { - wm withdraw .gpWidgetHelp - } -} - -proc ::gridplus::gpWidgetHelpShow {item} { - variable gpWidgetHelp - - .gpWidgetHelp.message configure -text $gpWidgetHelp($item) - - set screenWidth [lindex [wm maxsize .] 0] - set helpWidth [winfo width .gpWidgetHelp] - set helpX [winfo pointerx $item] - set helpY [expr [winfo rooty $item] + [winfo height $item]] - - if {[expr {$helpX + $helpWidth}] > $screenWidth} { - set helpX [expr {$screenWidth - $helpWidth - 8}] - } - - wm geometry .gpWidgetHelp +$helpX+$helpY - wm deiconify .gpWidgetHelp - - raise .gpWidgetHelp - - unset gpWidgetHelp(delay) - - set gpWidgetHelp(show) [after [=< helpDisplayTime 2500] ::gridplus::gpWidgetHelpCancel] -} - -#=======================================================================# -# PROC : ::gridplus::gpCalendar # -# PURPOSE: Create calendar. # -#=======================================================================# - -proc ::gridplus::gpCalendar {} { - upvar 1 options options - - global {} - - variable gpInfo - - set columnWidth 3 - - set gpInfo($options(name):fg) [lindex [split $options(-calcolor) "/"] 0] - set gpInfo($options(name):bg) [lindex [split $options(-calcolor) "/"] 1] - set gpInfo($options(name):selectfg) [lindex [split $options(-calselectcolor) "/"] 0] - set gpInfo($options(name):selectbg) [lindex [split $options(-calselectcolor) "/"] 1] - set gpInfo($options(name):command) $options(-command) - set gpInfo($options(name):navcommand) $options(-navcommand) - set gpInfo($options(name):navselect) $options(-navselect) - set gpInfo($options(name):variable) $options(-variable) - set gpInfo($options(name):selecttoday) $options(-selecttoday) - set gpInfo($options(name):weekstart) $options(-weekstart) - - if {$options(-date) eq ""} { - foreach {month day year} [clock format [clock seconds] -format "%m %d %Y"] {} - } else { - foreach {month day year} [::gridplus::gpFormatDate $options(-date) internal] {} - if {! [::gridplus::gpCalCheckDate $month $day $year]} { - error "GRIDPLUS ERROR: (gridplus calendar) \"$options(-date)\" is not a valid date." - } - } - - ::gridplus::gpLabelframe - - frame $options(name).calendar -bg $gpInfo($options(name):bg) -relief $options(-calrelief) -borderwidth 2 - frame $options(name).calendar.header -bg $gpInfo($options(name):bg) - - label $options(name).calendar.header.month -text "" -font [::gridplus::gpSetFont {+2 bold}] -bg $gpInfo($options(name):bg) -fg $gpInfo($options(name):fg) -padx 0 - label $options(name).calendar.header.year -text "" -font [::gridplus::gpSetFont {+2 bold}] -bg $gpInfo($options(name):bg) -fg $gpInfo($options(name):fg) -padx 0 - - pack $options(name).calendar.header.month -side left -anchor w - pack $options(name).calendar.header.year -side right -anchor e - - grid $options(name).calendar.header -columnspan 7 -sticky ew - - if {$options(-navbar)} { - frame $options(name).calendar.navbar -bg $gpInfo($options(name):bg) - frame $options(name).calendar.navbar.left -bg $gpInfo($options(name):bg) - frame $options(name).calendar.navbar.centre -bg $gpInfo($options(name):bg) - frame $options(name).calendar.navbar.right -bg $gpInfo($options(name):bg) - - ttk::label $options(name).calendar.navbar.left.navbackyear -image gpcal-prev-year -background $gpInfo($options(name):bg) - pack $options(name).calendar.navbar.left.navbackyear -side left - bind $options(name).calendar.navbar.left.navbackyear "::gridplus::gpCalendarNav $options(name) year -1" - - ttk::label $options(name).calendar.navbar.right.navnextyear -image gpcal-next-year -background $gpInfo($options(name):bg) - pack $options(name).calendar.navbar.right.navnextyear -side right - bind $options(name).calendar.navbar.right.navnextyear "::gridplus::gpCalendarNav $options(name) year +1" - - ttk::label $options(name).calendar.navbar.centre.current -image gpcal-today -background $gpInfo($options(name):bg) - pack $options(name).calendar.navbar.centre.current - bind $options(name).calendar.navbar.centre.current "::gridplus::gpCalendarNav $options(name) current" - - ttk::label $options(name).calendar.navbar.left.navbackmonth -image gpcal-prev-month -background $gpInfo($options(name):bg) - pack $options(name).calendar.navbar.left.navbackmonth -side left - bind $options(name).calendar.navbar.left.navbackmonth "::gridplus::gpCalendarNav $options(name) month -1" - - ttk::label $options(name).calendar.navbar.right.navnextmonth -image gpcal-next-month -background $gpInfo($options(name):bg) - pack $options(name).calendar.navbar.right.navnextmonth -side right - bind $options(name).calendar.navbar.right.navnextmonth "::gridplus::gpCalendarNav $options(name) month +1" - - pack $options(name).calendar.navbar.left -side left - pack $options(name).calendar.navbar.centre -side left -expand 1 -fill x - pack $options(name).calendar.navbar.right -side right - - grid $options(name).calendar.navbar -columnspan 7 -sticky ew - } - - set rowData "" - - foreach dayName [::gridplus::gpCalDayNames $options(-weekstart)] { - label $options(name).calendar.days:$dayName -text $dayName -borderwidth 1 -width $columnWidth -font [::gridplus::gpSetFont bold] -bg $gpInfo($options(name):bg) -fg $gpInfo($options(name):fg) - set rowData "$rowData $options(name).calendar.days:$dayName" - } - - grid {*}$rowData -sticky e - - for {set row 1} {$row < 7} {incr row} { - set rowData "" - for {set column 1} {$column < 8} {incr column} { - label $options(name).calendar.$row:$column -text "" -borderwidth 1 -width 3 -fg $gpInfo($options(name):fg) -bg $gpInfo($options(name):bg) - set rowData "$rowData $options(name).calendar.$row:$column" - } - grid {*}$rowData -sticky e - } - - grid columnconfigure $options(name) "all" -uniform allTheSame - - foreach child [winfo children $options(name).calendar] { - bind $child "::gridplus::gpCalendarSelect $options(name) %W" - } - - if {$options(-variable) ne ""} { - set ($options(-variable)) "" - } else { - set ($options(name)) "" - } - - pack $options(name).calendar - - ::gridplus::gpCalendarDisplay $options(name) $day $month $year -} - -#=======================================================================# -# PROC : ::gridplus::gpCalendarDisplay # -# PURPOSE: Display calendar for specified month. # -#=======================================================================# - -proc ::gridplus::gpCalendarDisplay {name day month year} { - - global {} - - variable gpConfig - variable gpInfo - - if {[info exists gpInfo($name:selected)] && $gpInfo($name:selected) ne ""} { - $gpInfo($name:selected) configure -bg $gpInfo($name:bg) -fg $gpInfo($name:fg) - } - - foreach {currentDay currentMonth currentYear} [clock format [clock seconds] -format "%d %m %Y"] {} - - if {$month eq $currentMonth && $year eq $currentYear} { - set current 1 - } else { - set current 0 - } - - if {[info exists gpInfo($name:selectedmonth)] && $month eq $gpInfo($name:selectedmonth) && $year eq $gpInfo($name:selectedyear)} { - set selected 1 - } else { - set selected 0 - } - - foreach {monthName startDay} [clock format [clock scan 01/$month/$year -format %d/%m/%Y] -format "%B %u" -locale $gpConfig(locale)] {} - - if {$gpInfo($name:weekstart) == 0} { - set startColumn [expr {$startDay + 1}] - if {$startColumn == 8} { - set startColumn 1 - } - } else { - set startColumn $startDay - } - - $name.calendar.header.month configure -text $monthName - $name.calendar.header.year configure -text $year - - set output 0 - set outputDay 1 - - set gpInfo($name:displaymonth) $month - set gpInfo($name:displayyear) $year - - for {set row 1} {$row < 7} {incr row} { - set rowData "" - for {set column 1} {$column < 8} {incr column} { - if {$row == 1} { - if {$column == $startColumn} { - set output 1 - } - } - - if {$outputDay > [::gridplus::gpCalMonthDays $month $year]} { - set output 0 - } - - if {$output} { - $name.calendar.$row:$column configure -text $outputDay -relief flat - - if {$current && [format %02d $outputDay] eq $currentDay} { - $name.calendar.$row:$column configure -relief solid - } - - if {$gpInfo($name:selecttoday) && [format %02d $outputDay] eq $day} { - ::gridplus::gpCalendarSelect $name $name.calendar.$row:$column -displayonly - } - - if {$selected && [format %02d $outputDay] eq $gpInfo($name:selectedday)} { - $name.calendar.$row:$column configure -bg $gpInfo($name:selectbg) -fg $gpInfo($name:selectfg) - } - incr outputDay - } else { - $name.calendar.$row:$column configure -text "" -relief flat - } - } - } - - set gpInfo($name:selecttoday) 0 -} - -#=======================================================================# -# PROC : ::gridplus::gpCalendarNav # -# PURPOSE: Calendar navigation. # -#=======================================================================# - -proc ::gridplus::gpCalendarNav {name unit {increment {}}} { - - global {} - - variable gpInfo - - if {$unit eq "current"} { - if {$increment eq ""} { - foreach {month year} [clock format [clock seconds] -format "%m %Y"] {} - } else { - foreach {month year} [clock format [clock add [clock seconds] $increment month] -format "%m %Y"] {} - } - } else { - set month $gpInfo($name:displaymonth) - set year $gpInfo($name:displayyear) - foreach {month year} [clock format [clock add [clock scan 01/$gpInfo($name:displaymonth)/$gpInfo($name:displayyear) -format "%d/%m/%Y"] $increment $unit] -format "%m %Y"] {} - } - - ::gridplus::gpCalendarDisplay $name {} $month $year - - if {$gpInfo($name:navselect)} { - if {$gpInfo($name:variable) ne ""} { - set variable $gpInfo($name:variable) - } else { - set variable $name - } - - if {$($variable) ne ""} { - foreach {varMonth varDay varYear} [::gridplus::gpFormatDate $($variable) internal] {} - if {$month eq $varMonth && $year eq $varYear} { - ::gridplus::gpselect $name $($variable) - } - } - } - - if {$gpInfo($name:navcommand) ne ""} { - eval "$gpInfo($name:navcommand) $name $unit $increment" - } -} - - -#=======================================================================# -# PROC : ::gridplus::gpCalendarSelect # -# PURPOSE: Sets value for calendar selection. # -#=======================================================================# - -proc ::gridplus::gpCalendarSelect {name window {mode {}}} { - - global {} - - variable gpConfig - variable gpInfo - - if {[winfo class $window] ne "Label" || ! [string is integer -strict [$window cget -text]]} {return} - - if {$gpInfo($name:variable) ne ""} { - set variable $gpInfo($name:variable) - } else { - set variable $name - } - - if {[info exists gpInfo($name:selected)] && $gpInfo($name:selected) ne ""} { - $gpInfo($name:selected) configure -bg $gpInfo($name:bg) -fg $gpInfo($name:fg) - } - - $window configure -bg $gpInfo($name:selectbg) -fg $gpInfo($name:selectfg) - - set gpInfo($name:selected) $window - set gpInfo($name:selectedday) [format %02d [$window cget -text]] - set gpInfo($name:selectedmonth) $gpInfo($name:displaymonth) - set gpInfo($name:selectedyear) $gpInfo($name:displayyear) - - switch -- $gpConfig(dateformat) { - eu {set ($variable) "$gpInfo($name:selectedday).$gpInfo($name:selectedmonth).$gpInfo($name:selectedyear)"} - iso {set ($variable) "$gpInfo($name:selectedyear)-$gpInfo($name:selectedmonth)-$gpInfo($name:selectedday)"} - uk {set ($variable) "$gpInfo($name:selectedday)/$gpInfo($name:selectedmonth)/$gpInfo($name:selectedyear)"} - us {set ($variable) "$gpInfo($name:selectedmonth)/$gpInfo($name:selectedday)/$gpInfo($name:selectedyear)"} - } - - if {$mode ne "-displayonly" && $gpInfo($name:command) ne ""} { - eval $gpInfo($name:command) - } -} - -#=======================================================================# -# PROC : ::gridplus::gpClear # -# PURPOSE: Clear window and unset associated variables. # -#=======================================================================# - -proc ::gridplus::gpClear {} { - upvar 1 options options - - global {} - - variable gpWidgetHelp - variable gpInfo - variable gpTabOrder - variable gpValidateError - variable gpValidations - - if {$options(name) ne "."} { - unset -nocomplain gpInfo($options(name):toplevel) - unset -nocomplain gpInfo($options(name):modal) - } - - if {[winfo exists $options(name).container]} { - eval $gpInfo($options(name):wcmd) - unset -nocomplain gpInfo($options(name):in) - set gpInfo($options(name):wcmd) {} - return - } - - $options(name) configure -menu {} - - unset -nocomplain gpInfo(validation:failed) - unset -nocomplain gpValidations($options(name)) - - if {[winfo exists .gpValidateError]} { - wm withdraw .gpValidateError - } - - foreach item [winfo child $options(name)] { - if {! [winfo exists $item]} {continue} - - set class [winfo class $item] - - if {[regexp -- {^[.]_} $item]} { - continue - } - - if {[string match *.gpEditMenu $item]} { - continue - } - - if {$class ne "Toplevel"} { - if {$options(-variables) && [info exists ($item)]} { - if {$class eq "Entry"} { - $item configure -textvariable {} - } - unset ($item) - } - if {$options(-variables) && [info exists gpInfo($item:radiobuttonGroups)]} { - foreach radiobuttonGroup $gpInfo($item:radiobuttonGroups) { - if {[info exists ($item$radiobuttonGroup)]} { - unset ($item$radiobuttonGroup) - } - } - unset gpInfo($item:radiobuttonGroups) - } - if {[info exists gpWidgetHelp($item)]} { - unset gpWidgetHelp($item) - } - if {[info exists gpInfo($item:wcmd)]} { - eval $gpInfo($item:wcmd) - } - foreach infoItem [array names gpInfo $item:*] { - unset gpInfo($infoItem) - } - foreach tabOrderItem [array names gpTabOrder $item:*] { - unset gpTabOrder($tabOrderItem) - } - foreach validateErrorItem [array names gpValidateError $item:*] { - unset gpValidateError($validateErrorItem) - } - if {$gpInfo() eq "$item.text"} { - if {[winfo exists .gpTextFind]} { - ::gridplus::gpTextFind:action,cancel - } - } - if {$class eq "Menu"} { - foreach infoItem [array names gpInfo $item.*:group] { - unset gpInfo($infoItem) - } - } - - destroy $item - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpCommand # -# PURPOSE: Evals command, performing validations if required. # -#=======================================================================# - -proc ::gridplus::gpCommand {command window validate} { - - global {} - - variable gpValidations - variable gpInfo - - if {$window eq "."} { - set containers [array names gpInfo -regexp {^[.][^.]+:in$}] - } else { - set containers [array names gpInfo -regexp "^$window\[.\]\[^.\]+:in$"] - } - - set containedWindows {} - - foreach container $containers { - set containedWindows "$containedWindows $gpInfo($container)" - } - - if {[info exists gpValidations($window)]} { - set validations $gpValidations($window) - } else { - set validations {} - } - - foreach containedWindow $containedWindows { - if {[info exists gpValidations($containedWindow)]} { - set validations "$validations $gpValidations($containedWindow)" - } - } - - if {$validate && $validations ne ""} { - foreach validationInfo $validations { - set entry [lindex [split $validationInfo :] 0] - regexp -- {:(.+)$} $validationInfo -> validation - if {! [::gridplus::gpValidate $entry $validation focusout - - 1]} { - ::gridplus::gpValidateFailed $entry - return - } - } - } - - eval $command -} - -#=======================================================================# -# PROC : ::gridplus::gpCommandFormat # -# PURPOSE: Makes sure "command" is in the correct format. # -#=======================================================================# - -proc ::gridplus::gpCommandFormat {command} { - - set commandProc [lindex $command 0] - set commandParameters [lrange $command 1 end] - - regsub -all {[.]} $commandProc ":" commandProc - regsub {;:} $commandProc ";" commandProc - regsub {^:} $commandProc {} commandProc - - if {[llength $command] eq 1} { - return $commandProc - } else { - return [list $commandProc {*}$commandParameters] - } -} - -#=======================================================================# -# PROC : ::gridplus::gpContainer # -# PURPOSE: Create container for toplevel windows. # -#=======================================================================# - -proc ::gridplus::gpContainer {} { - upvar 1 options options - - variable gpInfo - - if {[regexp -- {(^[.][^.]+)[.]} $options(name) -> window]} { - if {! $gpInfo($window:toplevel)} { - error "GRIDPLUS ERROR: (gridplus container) \"$window\" is a contained toplevel." - } - } - - if {$options(-relief) eq "theme"} { - if {$options(-title) eq ""} { - ::ttk::labelframe $options(name) -height $options(-height) -width $options(-width) -padding $options(-padding) - ::ttk::separator $options(name).separator -orient horizontal - $options(name) configure -labelwidget $options(name).separator -labelanchor s - } else { - if {$options(-labelanchor) eq ""} { - ::ttk::labelframe $options(name) -height $options(-height) -width $options(-width) -padding $options(-padding) -text [mc $options(-title)] - } else { - ::ttk::labelframe $options(name) -height $options(-height) -width $options(-width) -labelanchor $options(-labelanchor) -padding $options(-padding) -text [mc $options(-title)] - } - } - } else { - ::ttk::frame $options(name) -height $options(-height) -width $options(-width) -padding $options(-padding) -relief $options(-relief) - } - - grid propagate $options(name) 0 - pack propagate $options(name) 0 - - set gpInfo($options(name):sticky) $options(-sticky) - set gpInfo($options(name):wcmd) {} - -} - -#=======================================================================# -# PROC : ::gridplus::gpCreateIcons # -# PURPOSE: Creates default icons for GRIDPLUS Tree. # -#=======================================================================# - -proc ::gridplus::gpCreateIcons {} { - - image create photo ::icon::file -data { - R0lGODlhEAAQAIIAAPwCBFxaXISChPz+/MTCxKSipAAAAAAAACH5BAEAAAAA - LAAAAAAQABAAAANCCLrcGzBC4UAYOE8XiCdYF1BMJ5ye1HTfNxTBSpy0QMBy - ++HlXNu8h24X6/2AReHwllRcMtCgs0CtVpsWiRZbqfgTACH+aENyZWF0ZWQg - YnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcs - MTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxj - b3IuY29tADs= - } - - image create photo ::icon::folder -data { - R0lGODlhEAAQAIIAAPwCBFxaXMTCxPz+/KSipAAAAAAAAAAAACH5BAEAAAAA - LAAAAAAQABAAAAM3CLrc/i/IAFcQWFAos56TNYxkOWhKcHossals+64x5qZ0 - fQNwbc++Hy4o2F0IyKTSCGqCKhB/AgAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lG - IFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCBy - aWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7 - } - - image create photo gpcal-prev-year -data { - R0lGODlhCgAFAHcAACH5BAEAAAEALAAAAAAKAAUAhwAAAP//8AAAEQAAEf//9QAAAAAAAAAA - AAACvAAAAAEbhAzQICcB0AoanwTx9Akz2QTx9Akz5wEbhATyEAkz/gzQIATyjAk/ogAAAAAA - AQTyZNUOlwEbhATyECNGkAEbhAAAAAAAEgAADgAABAAABQAAAAAABgAAEAACvAAAAAkfDf3w - xP3gAAAAAAk4tQk4lATzTEB/LAAAAAQNAQANAQk6/gQNAQAAGgTzWATzCCNGmgTy2NhvHgUK - PgAABxQIQAAAAAAAAQk+Qwk+ggggwQAABAT1fAlRCwggwQAABAk+Qwk+ggggwQAABAT1nAlR - CwggwQAABAT1nAAAAAggwQUKPgAABxQIQAAAAEB/LLqrzQAAAATzTEB/LATzbNbM1P3gAATz - bNbM9ATzONbMmgAAB0B/LAT1kAAAAAAAAAAAAQAAFAAAAQAAAAAAAAAAEAAAAAAAUgAAAI0k - kAAAANjWENQkAP///9bMmtRc1gAAAEB/LAUKPgAABxQIQAAAAAAAAAAAAAAAB1bhsATzvNRc - 9UB/LAUKPgAABxQIQAAAAAT0uEB/LAAAAAT1kFvsVlvsXgT2FAk67Qk2DwEgwYUADwAAAFa0 - WAEgwQAAAQAAAAAAUgAAAI0bkAAAAEL8iAT0EAADAAAAAAAAUgAAAI0bkAAAAAAAAAAAAAAA - ALwCAAAAAIQbASDQDNABJ58aCvTxBNkzCfTxBOczCYQbARDyBP4zCSDQDIzyBKI/CQAAAAEA - AGTyBJcO1YQbARDyBJBGI4QbAQAAABIAAA4AAAQAAAUAAAAAAAYAABAAALwCAAAAAA0fCcTw - /QDg/QAAALU4CZQ4CUzzBCx/QAAAAAENBAENAP46CQENBBoAAFjzBAjzBJpGI9jyBB5v2D4K - BQcAAEAIFAAAAAEAAD1G1DnJ1nxa2V9G1FFG1EzzBCx/QAAAAAAAACx/QAEAAAlNzwlaxAgg - wQAAAAABAAT1hAAAAgla9AlNzwlaxAggwQAAAAABAAT1pAAAAgla9Ala5QgXAAMIBABgYEGD - CBMSFMhwYQCHDQ8uDAgAOw== - } - - image create photo gpcal-prev-month -data { - R0lGODlhCgAFAHcAACH5BAEAAAEALAAAAAAKAAUAhwAAAP//8AAAEQAAEf//9QAAAAAAAAAA - AAACvAAAAAEbhAzQIB4MsAoanwTx9Akz2QTx9Akz5wEbhATyEAkz/gzQIATyjAk/ogAAAAAA - AQTyZNUOlwEbhATyECNGkAEbhAAAAAAAEgAADgAABAAABQAAAAAABgAAEAACvAAAAAkfDf3w - xP3gAAAAAAk4tQk4lATzTEB/LAAAAAQfowAfowk6/gQfowAAGgTzWATzCCNGmgTy2NhvHgUK - PgAABxQIQAAAAAAAAQk+Qwk+gggdPQAABAT1fAlRCwgdPQAABAk+Qwk+gggdPQAABAT1nAlR - CwgdPQAABAT1nAAAAAgdPQUKPgAABxQIQAAAAEB/LLqrzQAAAATzTEB/LATzbNbM1P3gAATz - bNbM9ATzONbMmgAAB0B/LAT1kAAAAAAAAAAAAQAAFAAAAQAAAAAAAAAAEAAAAAAAUgAAAI0k - kAAAANjWENQkAP///9bMmtRc1gAAAEB/LAUKPgAABxQIQAAAAAAAAAAAAAAAB1bhsATzvNRc - 9UB/LAUKPgAABxQIQAAAAAT0uEB/LAAAAAT1kFvsVlvsXgT2FAk67Qk2DwEdPYUADwAAAFa0 - WAEdPQAAAQAAAAAAUgAAAI0bkAAAAEL8iAT0EAADAAAAAAAAUgAAAI0bkAAAAAAAAAAAAAAA - ALwCAAAAAIQbASDQDLAMHp8aCvTxBNkzCfTxBOczCYQbARDyBP4zCSDQDIzyBKI/CQAAAAEA - AGTyBJcO1YQbARDyBJBGI4QbAQAAABIAAA4AAAQAAAUAAAAAAAYAABAAALwCAAAAAA0fCcTw - /QDg/QAAALU4CZQ4CUzzBCx/QAAAAKMfBKMfAP46CaMfBBoAAFjzBAjzBJpGI9jyBB5v2D4K - BQcAAEAIFAAAAAEAAD1G1DnJ1nxa2V9G1FFG1EzzBCx/QAAAAAAAACx/QAEAAAlNzwlaxAgd - PQAAAAABAAT1hAAAAgla9AlNzwlaxAgdPQAAAAABAAT1pAAAAgla9Ala5QgXAAMIFAgAwMCD - BQ8OTKhwocGGBB8GCAgAOw== - } - - image create photo gpcal-today -data { - R0lGODlhZAAFAHcAACH5BAEAAAEALAAAAABkAAUAhwAAAP//8AAAAATzyPqI8PU4cP////lE - qPV9cPWKOgv/6AAAI/WKPpgu3dSYsgTxvNZvbTcLzgTyGNZvjgAAAQABEQAABgYLNjcLznPZ - uAAAgjcLziMlONRNoHPQAATx+ATx+DcLznPZuATyiNa44jcLzgAAggAAAAAAAAAAANcbETcL - zgAAggAAAAAAAATznAT5yAAAADcLzgAAggAAAAAAAAAABAAEsNa4nATzSAAAAPlEyww46PWL - zQUHePWQNww5EAw48Ak+Qwk+gggSoQAABAT1fAlRCwgSoQAABAk+Qwk+gggSoQAABAT1nAlR - CwgSoQAABAT1nAAAAAgSodQa2P///9TFCdRHqjcLzgAAggAAAAAAAATzKNgFm9gDDAAAggAA - ACMBeHPZuHP9gHP9iCMBeAHzVAUAAATyjAAAggTzdHPZsAAAAAAAAAAAAPaUVgAAUgAAAI0k - kAAAAAw48HPeAP3gAAHzXCMAAATy0PqI8ATzxPqI8AAAAgcJiP///wAARwcJiCMlONRNoNTL - oATzlACpGAT0dNjWEAT0JNhvHgcJiAAARwAAAAT4kAAAAdcbEQk67Qk2DwESoYUADwAAAFa3 - oAESoQAAAQAAAAAAUgAAAI0bkAAAAEL8iAT0EAADAAAAAAAAUgAAAI0bkAAAAHA49f///6hE - +XB99TqK9ej/CyMAAD6K9d0umLKY1LzxBG1v1s4LNxjyBI5v1gEAABEBAAYAADYLBs4LN7jZ - c4IAAM4LNzglI6BN1ADQc/jxBPjxBM4LN7jZc4jyBOK41s4LN4IAAAAAAAAAAAAAABEb184L - N4IAAAAAAAAAAJzzBMj5BAAAAM4LN4IAAAAAAAAAAAQAALAEAJy41kjzBAAAAMtE+eg4DM2L - 9XgHBTeQ9RA5DPA4DADec7DZc7jyBOU6+LABADDecwAAI7DZcwAAAIzzBMqM9QlNzwlaxAgS - oQAAAAABAAT1hAAAAgla9AlNzwlaxAgSoQAAAAABAAT1pAAAAgla9Ala5Qg/AAMIHEiwoMGD - CBMOBMAQgMKHECNKnEiRIEOBFytqfNiwo8ePIEOK/GhxpMmTKB1uXHkwY0aWMGPKXNhwZsyA - ADs= - } - - image create photo gpcal-next-year -data { - R0lGODlhCgAFAHcAACH5BAEAAAEALAAAAAAKAAUAhwAAAP//8AAAEQAAEf//9QAAAAAAAAAA - AAACvAAAAAEbhAzQICcB0AoanwTx9Akz2QTx9Akz5wEbhATyEAkz/gzQIATyjAk/ogAAAAAA - AQTyZNUOlwEbhATyECNGkAEbhAAAAAAAEgAADgAABAAABQAAAAAABgAAEAACvAAAAAkfDf3w - xP3gAAAAAAk4tQk4lATzTEB/LAAAAAQQGgAQGgk6/gQQGgAAGgTzWATzCCNGmgTy2NhvHgUK - PgAABxQIQAAAAAAAAQk+Qwk+gggb1gAABAT1fAlRCwgb1gAABAk+Qwk+gggb1gAABAT1nAlR - Cwgb1gAABAT1nAAAAAgb1gUKPgAABxQIQAAAAEB/LLqrzQAAAATzTEB/LATzbNbM1P3gAATz - bNbM9ATzONbMmgAAB0B/LAT1kAAAAAAAAAAAAQAAFAAAAQAAAAAAAAAAEAAAAAAAUgAAAI0k - kAAAANjWENQkAP///9bMmtRc1gAAAEB/LAUKPgAABxQIQAAAAAAAAAAAAAAAB1bhsATzvNRc - 9UB/LAUKPgAABxQIQAAAAAT0uEB/LAAAAAT1kFvsVlvsXgT2FAk67Qk2DwEb1oUADwAAAFa0 - WAEb1gAAAQAAAAAAUgAAAI0bkAAAAEL8iAT0EAADAAAAAAAAUgAAAI0bkAAAAAAAAAAAAAAA - ALwCAAAAAIQbASDQDNABJ58aCvTxBNkzCfTxBOczCYQbARDyBP4zCSDQDIzyBKI/CQAAAAEA - AGTyBJcO1YQbARDyBJBGI4QbAQAAABIAAA4AAAQAAAUAAAAAAAYAABAAALwCAAAAAA0fCcTw - /QDg/QAAALU4CZQ4CUzzBCx/QAAAABoQBBoQAP46CRoQBBoAAFjzBAjzBJpGI9jyBB5v2D4K - BQcAAEAIFAAAAAEAAD1G1DnJ1nxa2V9G1FFG1EzzBCx/QAAAAAAAACx/QAEAAAlNzwlaxAgb - 1gAAAAABAAT1hAAAAgla9AlNzwlaxAgb1gAAAAABAAT1pAAAAgla9Ala5QgZAAMAABBAIMGC - BQcmPIhQocGGBx0+nBggIAA7 - } - - image create photo gpcal-next-month -data { - R0lGODlhCgAFAHcAACH5BAEAAAEALAAAAAAKAAUAhwAAAP//8AAAEQAAEf//9QAAAAAAAAAA - AAACvAAAAAEP/gzQIAcAAAoanwTx9Akz2QTx9Akz5wEP/gTyEAkz/gzQIATyjAk/ogAAAAAA - AQTyZNUOlwEP/gTyECNGkAEP/gAAAAAAEgAADgAABAAABQAAAAAABgAAEAACvAAAAAkfDf3w - xP3gAAAAAAk4tQk4lATzTEB/LAAAAAQa7AAa7Ak6/gQa7AAAGgTzWATzCCNGmgTy2NhvHgUK - PgAABxQIQAAAAAAAAQk+Qwk+gggKNgAABAT1fAlRCwgKNgAABAk+Qwk+gggKNgAABAT1nAlR - CwgKNgAABAT1nAAAAAgKNgUKPgAABxQIQAAAAEB/LLqrzQAAAATzTEB/LATzbNbM1P3gAATz - bNbM9ATzONbMmgAAB0B/LAT1kAAAAAAAAAAAAQAAFAAAAQAAAAAAAAAAEAAAAAAAUgAAAI0k - kAAAANjWENQkAP///9bMmtRc1gAAAEB/LAUKPgAABxQIQAAAAAAAAAAAAAAAB1bhsATzvNRc - 9UB/LAUKPgAABxQIQAAAAAT0uEB/LAAAAAT1kFvsVlvsXgT2FAk67Qk2DwEKNoUADwAAAFa0 - WAEKNgAAAQAAAAAAUgAAAI0bkAAAAEL8iAT0EAADAAAAAAAAUgAAAI0bkAAAAAAAAAAAAAAA - ALwCAAAAAP4PASDQDAAAB58aCvTxBNkzCfTxBOczCf4PARDyBP4zCSDQDIzyBKI/CQAAAAEA - AGTyBJcO1f4PARDyBJBGI/4PAQAAABIAAA4AAAQAAAUAAAAAAAYAABAAALwCAAAAAA0fCcTw - /QDg/QAAALU4CZQ4CUzzBCx/QAAAAOwaBOwaAP46CewaBBoAAFjzBAjzBJpGI9jyBB5v2D4K - BQcAAEAIFAAAAAEAAD1G1DnJ1nxa2V9G1FFG1EzzBCx/QAAAAAAAACx/QAEAAAlNzwlaxAgK - NgAAAAABAAT1hAAAAgla9AlNzwlaxAgKNgAAAAABAAT1pAAAAgla9Ala5QgWAAMIBABAoMGD - BA8qTKgwAMOFBQ8GBAA7 - } - -} - -#=======================================================================# -# PROC : ::gridplus::gpDateSelectorClear # -# PURPOSE: Clear Date Selector field for "Delete" key. # -#=======================================================================# - -proc ::gridplus::gpDateSelectorClear {name key} { - - if {$key eq "Delete"} { - gpset $name {} - } -} - -#=======================================================================# -# PROC : ::gridplus::gpDateSelectorKeyPress # -# PURPOSE: Date Selector key press post/unpost # -#=======================================================================# - -proc ::gridplus::gpDateSelectorKeyPress {name widget action} { - - if {$action eq "post" && ! [$name instate pressed]} { - ::gridplus::gpDateSelectorPost $name - return -code break - } elseif {$action eq "unpost" && [$name instate pressed]} { - if {! [string match .gpDateSelector.calendar.* $widget]} { - ::gridplus::gpDateSelectorUnpost - } - } else { - return -code break - } -} - -#=======================================================================# -# PROC : ::gridplus::gpDateSelectorPost # -# PURPOSE: Post Date Selector dropdown/popup. # -#=======================================================================# - -proc ::gridplus::gpDateSelectorPost {name} { - - global {} - - variable gpInfo - - $name instate disabled {return} - - $name state pressed - - set widgetX [winfo rootx $name] - set widgetY [winfo rooty $name] - set widgetWidth [winfo width $name] - set widgetHeight [winfo height $name] - - gridplus window .gpDateSelector -overrideredirect 1 -topmost 1 - - wm transient .gpDateSelector [winfo toplevel $name] - - bind .gpDateSelector "::gridplus::gpDateSelectorToggle $name %W" - - gridplus calendar .gpDateSelector.calendar \ - -command "::gridplus::gpDateSelectorUnpost;$gpInfo($name:datecommand)" \ - -date $($name) \ - -padding 2 \ - -relief solid \ - -selecttoday 1 \ - -variable $name - - pack .gpDateSelector.calendar - - update idletasks - - set calendarWidth [winfo reqwidth .gpDateSelector] - - if {[tk windowingsystem] eq "aqua"} { - # Adjust for platform-specific bordering to ensure the box is - # directly under actual 'entry square' - set xOffset 3 - set yOffset 2 - incr widgetX $xOffset - set widgetWidth [expr {$widgetWidth - $xOffset*2}] - } else { - set yOffset 0 - } - - set calendarHeight [winfo reqheight .gpDateSelector] - - # Added "+ 40" to take into account windows task bar. - if {$widgetY + $widgetHeight + $calendarHeight + 40 > [winfo screenheight .gpDateSelector]} { - set Y [expr {$widgetY - $calendarHeight - $yOffset}] - } else { - set Y [expr {$widgetY + $widgetHeight - $yOffset}] - } - - set X [expr {$widgetX - ($calendarWidth - $widgetWidth)}] - - if {$X < 0} { - set X $widgetX - } - - wm geometry .gpDateSelector +${X}+${Y} - wm deiconify .gpDateSelector - raise .gpDateSelector - - ttk::globalGrab .gpDateSelector - - focus .gpDateSelector.calendar - bind .gpDateSelector.calendar "::gridplus::gpDateSelectorKeyPress $name %W unpost" -} - -#=======================================================================# -# PROC : ::gridplus::gpDateSelectorToggle # -# PURPOSE: Toggle Date Selector dropdown/popup. # -#=======================================================================# - -proc ::gridplus::gpDateSelectorToggle {name widget} { - - if {[$name instate pressed]} { - if {! [string match .gpDateSelector.calendar.* $widget]} { - ::gridplus::gpDateSelectorUnpost - } - } else { - ::gridplus::gpDateSelectorPost $name - return -code break - } -} - -#=======================================================================# -# PROC : ::gridplus::gpDateSelectorUnpost # -# PURPOSE: Unpost Date Selector dropdown/popup. # -#=======================================================================# - -proc ::gridplus::gpDateSelectorUnpost {{testWindow {}}} { - - variable gpInfo - - if {[winfo exists .gpDateSelector.calendar] && $testWindow ne ".gpDateSelector"} { - foreach dateSelector [array names gpInfo *:datecommand] { - set name [lindex [split $dateSelector :] 0] - if {[$name instate pressed]} { - $name state !pressed - - ttk::releaseGrab .gpDateSelector - - gridplus clear .gpDateSelector - destroy .gpDateSelector - - update idletasks - ttk::combobox::Unpost $name - - focus $name - } - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpDefine # -# PURPOSE: Creates GRIDPLUS widget definitions. # -#=======================================================================# - -proc ::gridplus::gpDefine {} { - upvar 1 options options - - variable gpInfo - - foreach {id widget} $options(layout) { - set gpInfo(:$id) $widget - } -} - -#=======================================================================# -# PROC : ::gridplus::gpDefineWidget # -# PURPOSE: Process "defined" widget. # -#=======================================================================# - -proc ::gridplus::gpDefineWidget {column} { - - variable gpInfo - - if {[string match @* $column]} { - if {[winfo exists .[string range [lindex $column 0] 1 end]]} { - return $column - } - set defineID [string range [lindex $column 0] 1 end] - if {[info exists gpInfo(:$defineID)]} { - set defineWidget $gpInfo(:$defineID) - set replacementID 1 - - foreach replacement [lrange $column 1 end] { - regsub -- "%$replacementID" $defineWidget $replacement defineWidget - incr replacementID - } - } - return [::gridplus::gpDefineWidget $defineWidget] - } else { - return $column - } -} - -#=======================================================================# -# PROC : ::gridplus::gpEditMenu # -# PURPOSE: Pop-up menu for entry widgets. # -#=======================================================================# - -proc ::gridplus::gpEditMenu {mode} { - - set widget [focus] - - switch -- $mode { - cut { - clipboard clear - clipboard append [selection get] - $widget delete sel.first sel.last - } - copy { - clipboard clear - clipboard append [selection get] - } - paste { - $widget selection clear - $widget insert insert [clipboard get] - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpEditMenuCreate # -# PURPOSE: Create pop-up menu for entry widgets. # -#=======================================================================# - -proc ::gridplus::gpEditMenuCreate {window} { - - menu $window.gpEditMenu - - $window.gpEditMenu configure -tearoff 0 - - $window.gpEditMenu add command -label [mc "Cut"] -command "::gridplus::gpEditMenu cut" - $window.gpEditMenu add command -label [mc "Copy"] -command "::gridplus::gpEditMenu copy" - $window.gpEditMenu add command -label [mc "Paste"] -command "::gridplus::gpEditMenu paste" -} - -#=======================================================================# -# PROC : ::gridplus::gpEntryEdit # -# PURPOSE: Pop-up menu for entry widgets. # -#=======================================================================# - -proc ::gridplus::gpEntryEdit {editWindow X Y {variable {}}} { - - focus [winfo containing $X $Y] - - after 1 "::gridplus::gpEntryEditPost \{$editWindow\} $X $Y \{$variable\}" -} - -#=======================================================================# -# PROC : ::gridplus::gpEntryEditPost # -# PURPOSE: Post Pop-up menu for entry widgets. # -#=======================================================================# - -proc ::gridplus::gpEntryEditPost {editWindow X Y {variable {}}} { - - global {} - - variable gpInfo - - set widget [winfo containing $X $Y] - - if {[info exists gpInfo(validation:failed)] && $gpInfo(validation:failed) ne $widget} { - return - } - - if {$variable eq ""} { - set variable $widget - } - - if {$editWindow eq ""} { - set window {} - } else { - set window .$editWindow - } - - if {! [$widget selection present]} { - $widget selection range 0 end - } - - if {[$widget cget -state] ne "normal"} { - $window.gpEditMenu entryconfigure 0 -state disabled - $window.gpEditMenu entryconfigure 1 -state normal - $window.gpEditMenu entryconfigure 2 -state disabled - } else { - $window.gpEditMenu entryconfigure 0 -state normal - $window.gpEditMenu entryconfigure 1 -state normal - $window.gpEditMenu entryconfigure 2 -state normal - } - - if {$($variable) eq ""} { - $window.gpEditMenu entryconfigure 0 -state disabled - $window.gpEditMenu entryconfigure 1 -state disabled - } - - if {[$widget cget -state] ne "disabled"} { - $window.gpEditMenu post $X $Y - } -} - -#=======================================================================# -# PROC : ::gridplus::gpGetFontOption # -# PURPOSE: Get font option for specified font. # -#=======================================================================# - -proc ::gridplus::gpGetFontOption {font option} { - - foreach {fontOption value} [font configure $font] { - if {$fontOption eq $option} { - return $value - } - } - - return {} -} - -#=======================================================================# -# PROC : ::gridplus::gpGetFontSize # -# PURPOSE: Get font size for specified font. # -#=======================================================================# - -proc ::gridplus::gpGetFontSize {font} { - - if {[llength $font] == 1} { - return [::gridplus::gpGetFontOption $font -size] - } else { - return [lindex $font 1] - } -} - -#=======================================================================# -# PROC : ::gridplus::gpGoto # -# PURPOSE: Move text widget display to specified label. # -#=======================================================================# - -proc ::gridplus::gpGoto {} { - upvar 1 options options - - global {} - - $options(name).text yview $options(layout) - - set ($options(name)) $options(layout) -} - -#=======================================================================# -# PROC : ::gridplus::gpGrid # -# PURPOSE: Create grid. # -#=======================================================================# - -proc ::gridplus::gpGrid {} { - upvar 1 options options - - global {} - - variable gpInfo - variable gpTabOrder - - set options(-columnformat) [::gridplus::gpOptionAlias -columnformat -cfmt] - - set labelColor(1) [lindex [split $options(-labelcolor) /] 0] - set labelColor(2) [lindex [split $options(-labelcolor) /] 1] - set labelStyle(1) [lindex [split $options(-labelstyle) /] 0] - set labelStyle(2) [lindex [split $options(-labelstyle) /] 1] - - regsub -all -- {,} $labelStyle(1) { } labelStyle(1) - regsub -all -- {,} $labelStyle(2) { } labelStyle(2) - - if {[string match *w* $options(-attach)]} { - set leftStretch 0 - set rightStretch 1 - set defaultStretch 0 - } else { - set leftStretch 0 - set rightStretch 0 - set defaultStretch 1 - } - - if {[llength $options(-spacestretch)] == 1} { - set options(-spacestretch) [lrepeat 100 $options(-spacestretch)] - } - - set attachNS 0 - - if {[string match *n* $options(-attach)]} { - set weightY 0 - if {[string match *s* $options(-attach)]} { - set attachNS 1 - } - } else { - set weightY 1 - } - - ::gridplus::gpLabelframe - - grid anchor $options(name) $options(-anchor) - - set rowID 0 - set rowTotal [llength [split $options(layout) "\n"]] - set rowCount 1 - - if {! [regexp -- {^[.]([^.]+)[.]} $options(name) -> window]} { - set window {} - } - - if {$options(-subst)} { - if {[=< substCommandGrid [=< substCommand 0]]} { - set options(layout) [subst -nobackslashes $options(layout)] - } else { - set options(layout) [subst -nobackslashes -nocommands $options(layout)] - } - } - - foreach row [split $options(layout) "\n"] { - set columnID 0 - set columnTotal [llength $row] - set columnCount 1 - set rowWeight1 0 - - if {$options(-spacestretch) eq ""} { - if {$columnTotal > 1} { - set stretch "$leftStretch [lrepeat [expr {$columnTotal - 1}] $defaultStretch] $rightStretch" - } else { - set stretch "$leftStretch $rightStretch" - } - } else { - set stretch $options(-spacestretch) - } - - ::ttk::frame $options(name).space:$rowID:$columnID -width 0 - grid $options(name).space:$rowID:$columnID -column $columnID -row $rowID -sticky ew - grid columnconfigure $options(name) $columnID -weight [lindex $stretch 0] - incr columnID - - foreach column $row { - switch -- [llength $column] { - 0 { - set columnSpan 2 - set column "{}" - } - 1 { - set columnSpan 2 - } - 2 { - set columnSpan 1 - } - default { - error "GRIDPLUS ERROR: Too many items in column." - } - } - - set columnItem 1 - set formatWidth(1) 0 - set formatWidth(2) 0 - - if {[set columnFormat [lindex $options(-columnformat) [expr {$columnCount - 1}]]] ne ""} { - if {[lindex [split $columnFormat "/"] 0] ne ""} { - set formatWidth(1) [lindex [split $columnFormat "/"] 0] - set formatWidth(2) [lindex [split $columnFormat "/"] 1] - } - if {$formatWidth(1) eq ""} {set formatWidth(1) 0} - if {$formatWidth(2) eq ""} {set formatWidth(2) 0} - } - - foreach item $column { - set bold 0 - set command {} - set labelFont $labelStyle($columnItem) - set labelIcon {} - set labelWidth 0 - set sticky {} - set validate 0 - - if {! [string match "*: " $item]} { - regexp {(^[^:]+)(:(([nsewc]+)?([0-9]+)?$)?)} $item -> item - - sticky labelWidth - } - - if {$labelWidth eq ""} {set labelWidth 0} - - switch -- $sticky { - c {set sticky {}} - "" {set sticky w} - } - - if {[string match "*n*" $sticky] && [string match "*s*" $sticky]} { - set rowWeight1 1 - } - - switch -glob -- $item { - .* { - set itemName $item - ::ttk::frame $options(name).widget:$rowID:$columnID - ::ttk::frame $options(name).widget:$rowID:$columnID.width -height 0 -width [expr {$formatWidth($columnItem) * $gpInfo()}] - - if {! [winfo exists $item]} { - set itemName $options(name),[string range $item 1 end] - - if {$options(-basename) ne ""} { - set textVariable $options(-basename),[string range $item 1 end] - } else { - set textVariable $itemName - } - ::ttk::label $itemName -foreground $labelColor($columnItem) -justify $options(-justify) -wraplength $options(-wraplength) -textvariable ($textVariable) - if {$labelFont ne ""} { - $itemName configure -font [::gridplus::gpSetFont $labelFont] - } - } - - grid $options(name).widget:$rowID:$columnID.width -row 0 -column 0 - grid $itemName -in $options(name).widget:$rowID:$columnID -row 1 -column 0 -sticky $sticky - grid configure $options(name).widget:$rowID:$columnID -in $options(name) -column $columnID -row $rowID -columnspan $columnSpan -sticky $sticky - grid columnconfigure $options(name).widget:$rowID:$columnID 0 -weight 1 - - if {$rowWeight1} { - grid rowconfigure $options(name) $rowID -weight 1 - grid rowconfigure $options(name).widget:$rowID:$columnID 1 -weight 1 - } - - if {$options(-taborder) eq "column"} { - set gpTabOrder([format "%s:%03d%03d%03d" $options(name) $columnCount $rowCount $columnItem]) $itemName - } else { - set gpTabOrder([format "%s:%03d%03d%03d" $options(name) $rowCount $columnCount $columnItem]) $itemName - } - } - | { - ::ttk::separator $options(name).separator:$rowID:$columnID -orient vertical - grid configure $options(name).separator:$rowID:$columnID -in $options(name) -column $columnID -row $rowID -columnspan $columnSpan -sticky ns - } - = { - ::ttk::separator $options(name).separator:$rowID:$columnID -orient horizontal - grid configure $options(name).separator:$rowID:$columnID -in $options(name) -column $columnID -row $rowID -columnspan $columnSpan -sticky ew - } - :* { - if {! [regexp -- {^:([^:]*):([^:]*):([^:]*)$} $item -> labelIcon command validate]} { - set labelIcon [string range $item 1 end] - regsub -- {%%$} $labelIcon {} labelIcon - } - if {$labelIcon eq ""} { - set labelIcon $options(-icon) - } - ::icons::icons create -file [file join $options(-iconpath) $options(-iconfile)] $labelIcon - ::ttk::label $options(name).label:$rowID:$columnID -image ::icon::$labelIcon - grid configure $options(name).label:$rowID:$columnID -in $options(name) -column $columnID -row $rowID -columnspan $columnSpan -sticky $sticky - if {$command ne ""} { - if {$options(-proc)} { - set command "set gridplus::gpInfo() \[focus\];gpProc $command" - } else { - set command "set gridplus::gpInfo() \[focus\];$options(-prefix)$command" - regsub -all {[.]} $command ":" command - regsub {;:} $command ";" command - } - - bind $options(name).label:$rowID:$columnID "eval \"::gridplus::gpCommand {$command} .$window $validate\"" - } - } - default { - if {[string match ^* $item]} { - set labelFont "$labelFont bold" - set item [string range $item 1 end] - } - regsub -all -- " +\n +" $item "\n" item - regsub -all -- "" $item "\n" item - - if {$labelWidth == 0} { - set labelWidth $formatWidth($columnItem) - } - - ::ttk::frame $options(name).label:$rowID:$columnID - ::ttk::frame $options(name).label:$rowID:$columnID.width -height 0 -width [expr {$labelWidth * $gpInfo()}] - ::ttk::label $options(name).label:$rowID:$columnID.text -foreground $labelColor($columnItem) -style $options(-style) -justify $options(-justify) -wraplength $options(-wraplength) -text [mc $item] - if {$labelFont ne ""} { - $options(name).label:$rowID:$columnID.text configure -font [::gridplus::gpSetFont $labelFont] - } - grid $options(name).label:$rowID:$columnID.width -row 0 -column 0 - grid $options(name).label:$rowID:$columnID.text -in $options(name).label:$rowID:$columnID -row 1 -column 0 -sticky $sticky - grid configure $options(name).label:$rowID:$columnID -in $options(name) -column $columnID -row $rowID -columnspan $columnSpan -sticky $sticky - grid columnconfigure $options(name).label:$rowID:$columnID 0 -weight 1 - } - } - incr columnID $columnSpan - incr columnItem - } - - if {$columnCount != $columnTotal} { - ::ttk::frame $options(name).space:$rowID:$columnID -width $options(-space) - grid $options(name).space:$rowID:$columnID -column $columnID -row $rowID -sticky ew - grid columnconfigure $options(name) $columnID -weight [lindex $stretch $columnCount] - incr columnID - } else { - ::ttk::frame $options(name).space:$rowID:$columnID -width 0 - grid $options(name).space:$rowID:$columnID -column $columnID -row $rowID -sticky ew - grid columnconfigure $options(name) $columnID -weight [lindex $stretch $columnCount] - } - - incr columnCount - } - - incr rowID - - if {$rowCount != $rowTotal} { - ::ttk::frame $options(name).space:$rowID:$columnID -height 4 -width 4 - grid $options(name).space:$rowID:$columnID -row $rowID -column 0 -sticky ns -columnspan 3 - grid rowconfigure $options(name) $rowID -weight $weightY - incr rowID - } elseif {! $weightY && ! $attachNS} { - ::ttk::frame $options(name).space:$rowID:$columnID -height 4 -width 4 - grid $options(name).space:$rowID:$columnID -row $rowID -column 0 -sticky ns -columnspan 3 - grid rowconfigure $options(name) $rowID -weight 1 - } - - incr rowCount - } - - foreach stretch $options(-stretch) { - grid columnconfigure $options(name) [expr {(($stretch + 1) * 3) - 1}] -weight 1 - } - - gpSetTabOrder $options(name) - - if {$options(-wtitle) ne ""} { - wm title [winfo toplevel $options(name)] [mc $options(-wtitle)] - } -} - -#=======================================================================# -# PROC : ::gridplus::gpInit # -# PURPOSE: Gridplus initailise. # -#=======================================================================# - -proc ::gridplus::gpInit {} { - variable gpConfig - variable gpInfo - variable gpOptionSets - variable gpValidation - - wm resizable . 0 0 - - set gpInfo(.:toplevel) 1 - set gpInfo(.:modal) 0 - set gpInfo() {} - - ttk::label .gpWidthFactor -width 1 - set gpInfo() [winfo reqwidth .gpWidthFactor] - destroy .gpWidthFactor - - if {[namespace exists "::starkit"]} { - set iconPath [file join $::starkit::topdir lib] - } else { - set iconPath [file join [info library]] - } - - array set gpConfig [list \ - dateformat [=< dateFormat us] \ - errormessage [=< errorMessage %] \ - iconfile [=< iconFile tkIcons] \ - iconpath [=< iconPath $iconPath] \ - locale [=< locale] \ - prefix [=< prefix] \ - proc [=< proc 0] \ - ] - - switch -- $gpConfig(dateformat) { - eu { - set gpConfig(date:day) 0 - set gpConfig(date:month) 1 - set gpConfig(date:year) 2 - set gpConfig(date:separator) . - } - iso { - set gpConfig(date:day) 2 - set gpConfig(date:month) 1 - set gpConfig(date:year) 0 - set gpConfig(date:separator) - - } - uk { - set gpConfig(date:day) 0 - set gpConfig(date:month) 1 - set gpConfig(date:year) 2 - set gpConfig(date:separator) / - } - us { - set gpConfig(date:day) 1 - set gpConfig(date:month) 0 - set gpConfig(date:year) 2 - set gpConfig(date:separator) / - } - } - - set gpConfig(date:century) [=< century 50] - - array set gpValidation { - alpha {^[a-zA-Z]+$} - alphanum {^[a-zA-Z0-9]+$} - date {proc:gpValidateDate} - decimal {trim:^[0-9]+[.][0-9]+$} - -decimal {trim:^(-)?[0-9]+[.][0-9]+$} - money {trim:^[0-9]+[.][0-9][0-9]$} - -money {trim:^(-)?[0-9]+[.][0-9][0-9]$} - num {trim:^[0-9]+([.][0-9]+)?$} - -num {trim:^(-)?[0-9]+([.][0-9]+)?$} - int {trim:^[0-9]+$} - -int {trim:^(-)?[0-9]+$} - notnull {[^\000]} - ! {[^\000]} - alpha:text {Alpha} - alphanum:text {Alphanumeric} - date:text {Date} - decimal:text {Decimal} - -decimal:text {Decimal} - money:text {Money Format} - -money:text {Money Format} - num:text {Numeric} - -num:text {Numeric} - int:text {Integer} - -int:text {Integer} - notnull:text {Not Null} - !:text {Non Blank} - } - - set gpOptionSets(.) { - -space 0 - -style {} - } - - ::gridplus::gpCreateIcons - - ::gridplus::gpEditMenuCreate {} - - bind . "::gridplus::gpWindowBindings . %W 1" - bind . "::gridplus::gpWindowBindings . %W 1" -} - -#=======================================================================# -# PROC : ::gridplus::gpInsertText # -# PURPOSE: Inserts "tagged" data into text widget. # -#=======================================================================# - -proc ::gridplus::gpInsertText {name tag end parameter position text} { - upvar 1 options options - - global {} - - variable gpInfo - - if {! [regexp -- {^[.]([^.]+)[.]} $name -> window]} { - set window {} - } - - set command false - set imageCommand {} - set imageInfo {} - set imageLink {} - set imageParameter {} - set link false - set bgColor $gpInfo($name:bgcolor) - set fgColor $gpInfo($name:fgcolor) - set linkColor $gpInfo($name:link) - set setCommand 0 - set validate 0 - - switch -- $end$tag { - init {set gpInfo($name:font) $gpInfo($name:defaultfont) - set gpInfo($name:size) 10 - set gpInfo($name:weight) normal - set gpInfo($name:slant) roman - set gpInfo($name:underline) false} - b {set gpInfo($name:weight) bold} - /b {set gpInfo($name:weight) normal} - bgcolor {set bgColor [lindex [split $parameter :] 0] - set bgParameter [lindex [split $parameter :] 1] - if {$bgParameter eq "default"} {set gpInfo($name:defaultbg) $bgColor} - set gpInfo($name:bgcolor) $bgColor} - /bgcolor {set bgColor $gpInfo($name:defaultbg) - set gpInfo($name:bgcolor) $gpInfo($name:defaultbg)} - color {set fgColor [lindex [split $parameter :] 0] - set fgParameter [lindex [split $parameter :] 1] - if {$fgParameter eq "default"} {set gpInfo($name:defaultfg) $fgColor} - set gpInfo($name:fgcolor) $fgColor} - /color {set fgColor $gpInfo($name:defaultfg) - set gpInfo($name:fgcolor) $gpInfo($name:defaultfg)} - command {set fgColor $gpInfo($name:normalcolor) - set gpInfo($name:underline) $gpInfo($name:normalstyle) - set command [lindex [split $parameter :] 0] - set commandParameter [lindex [split $parameter :] 1] - if {$commandParameter eq ""} {set commandParameter $text}} - font {set font [lindex [split $parameter :] 0] - set fontParameter [lindex [split $parameter :] 1] - if {$fontParameter eq "default"} {set gpInfo($name:defaultfont) $font} - set gpInfo($name:font) $font} - /font {set gpInfo($name:font) $gpInfo($name:defaultfont)} - i {set gpInfo($name:slant) italic} - /i {set gpInfo($name:slant) roman} - image {set imageInfo $parameter} - indent {set gpInfo($name:indent) $parameter - set tabs [string repeat "\t" $parameter] - set text "$tabs$text"} - /indent {set gpInfo($name:indent) 0} - label {set label [lindex [split $parameter :] 0] - set labelParameter [lindex [split $parameter :] 1] - if {$labelParameter eq "default"} {set ($name) $label} - $name.text mark set $label "insert wordstart" - $name.text mark gravity $label left} - link {set fgColor $gpInfo($name:normalcolor) - set gpInfo($name:underline) $gpInfo($name:normalstyle) - set link $parameter} - size {set size [lindex [split $parameter :] 0] - set sizeParameter [lindex [split $parameter :] 1] - if {$sizeParameter eq "default"} {set gpInfo($name:defaultsize) $size} - set gpInfo($name:size) [gridplus::gpSetFontSize $gpInfo($name:defaultsize) $size]} - /size {set gpInfo($name:size) $gpInfo($name:defaultsize)} - tab {if {$parameter eq ""} {set parameter 1} - set tabs [string repeat "\t" $parameter] - set text "$tabs$text"} - u {set gpInfo($name:underline) true} - /u {set gpInfo($name:underline) false} - } - - set tagName "tag[incr gpInfo($name:tagid)]" - set font "-family $gpInfo($name:font) -size $gpInfo($name:size) -slant $gpInfo($name:slant) -underline $gpInfo($name:underline) -weight $gpInfo($name:weight)" - set indent "[expr {$gpInfo($name:indent) * 0.5}]c" - - $name.text tag configure $tagName -lmargin1 $indent -lmargin2 $indent -background $bgColor -foreground $fgColor -font "$font" - - if {$imageInfo ne ""} { - if {[string match *@* $imageInfo]} { - set image [lindex [split $imageInfo @] 0] - set imageLink [lindex [split $imageInfo @] 1] - } else { - set image [lindex [split $imageInfo ~] 0] - set imageCommand [lindex [split [lindex [split $imageInfo ~] 1] :] 0] - set imageParameter [lindex [split [lindex [split $imageInfo ~] 1] :] 1] - - if {$imageCommand ne ""} { - set setCommand 1 - set imageCommand "$name,$imageCommand" - - if {$gpInfo($name:proc)} { - set imageCommand "set gridplus::gpInfo() \[focus\];gpProc $imageCommand" - } else { - set imageCommand "set gridplus::gpInfo() \[focus\];$gpInfo($name:prefix)$imageCommand" - regsub -all {[.]} $imageCommand ":" imageCommand - regsub {;:} $imageCommand ";" imageCommand - } - } - } - - if {[string match :* $image]} { - set icon [string range $image 1 end] - set image "::icon::$icon" - ::icons::icons create -file $gpInfo($name:iconlibrary) $icon - } - - set imageName [$name.text image create end -image $image] - - $name.text tag add $imageName $imageName - $name.text tag configure $imageName -background $bgColor - - if {$imageLink ne ""} { - $name.text tag bind $imageName "$name.text configure -cursor $gpInfo($name:linkcursor)" - $name.text tag bind $imageName "$name.text configure -cursor {}" - $name.text tag bind $imageName "set ($name) $imageLink; $name.text yview $imageLink" - } elseif {$setCommand} { - $name.text tag bind $imageName "$name.text configure -cursor $gpInfo($name:linkcursor)" - $name.text tag bind $imageName "$name.text configure -cursor {}" - $name.text tag bind $imageName "set ($name) \"$imageParameter\"; ::gridplus::gpCommand {$imageCommand} .$window $validate" - } - } - - if {$command ne "false"} { - - set command "$name,$command" - - if {$gpInfo($name:proc)} { - set command "set gridplus::gpInfo() \[focus\];gpProc $command" - } else { - set command "set gridplus::gpInfo() \[focus\];$gpInfo($name:prefix)$command" - regsub -all {[.]} $command ":" command - regsub {;:} $command ";" command - } - - $name.text tag bind $tagName "$name.text configure -cursor $gpInfo($name:linkcursor); $name.text tag configure $tagName -foreground $gpInfo($name:overcolor) -underline $gpInfo($name:overstyle)" - $name.text tag bind $tagName "$name.text configure -cursor {}; $name.text tag configure $tagName -foreground $gpInfo($name:normalcolor) -underline $gpInfo($name:normalstyle)" - $name.text tag bind $tagName "set ($name) \"$commandParameter\"; ::gridplus::gpCommand {$command} .$window $validate" - - set gpInfo($name:underline) false - } - - if {$link ne "false"} { - $name.text tag bind $tagName "$name.text configure -cursor $gpInfo($name:linkcursor); $name.text tag configure $tagName -foreground $gpInfo($name:overcolor) -underline $gpInfo($name:overstyle)" - $name.text tag bind $tagName "$name.text configure -cursor {}; $name.text tag configure $tagName -foreground $gpInfo($name:normalcolor) -underline $gpInfo($name:normalstyle)" - $name.text tag bind $tagName "set ($name) $link; $name.text yview $link" - set gpInfo($name:underline) false - } - - if {$text ne ""} { - regsub -all {!b:} $text "\u2022" text - regsub -all {!ob:} $text \{ text - regsub -all {!cb:} $text \} text - regsub -all {!bs:} $text {\\} text - regsub -all {!lt:} $text {<} text - regsub -all {!gt:} $text {>} text - $name.text insert $position $text $tagName - } -} - -#=======================================================================# -# PROC : ::gridplus::gpLabelframe # -# PURPOSE: Implements work-around to deal with ttk::labelframe bug. # -#=======================================================================# - -proc ::gridplus::gpLabelframe {} { - upvar 1 options options - - if {$options(-relief) eq "theme"} { - if {$options(-title) eq ""} { - ::ttk::labelframe $options(name) -padding $options(-padding) - ::ttk::separator $options(name).separator -orient horizontal - $options(name) configure -labelwidget $options(name).separator -labelanchor s - } else { - if {$options(-labelanchor) eq ""} { - ::ttk::labelframe $options(name) -padding $options(-padding) -text [mc $options(-title)] - } else { - ::ttk::labelframe $options(name) -labelanchor $options(-labelanchor) -padding $options(-padding) -text [mc $options(-title)] - } - } - } else { - ::ttk::frame $options(name) -padding $options(-padding) -relief $options(-relief) - } -} - -#=======================================================================# -# PROC : ::gridplus::gpLayout # -# PURPOSE: Create layout. # -#=======================================================================# - -proc ::gridplus::gpLayout {} { - upvar 1 options options - - global {} - - variable gpTabOrder - - set rowCount 0 - set layout(items) {} - set toplevel {} - - set setWeights 0 - set columnWeight1 {} - set rowWeight1 {} - - set maxColumn 0 - set maxRow 0 - - if {$options(-subst)} { - if {[=< substCommandLayout [=< substCommand 0]]} { - set options(layout) [subst -nobackslashes $options(layout)] - } else { - set options(layout) [subst -nobackslashes -nocommands $options(layout)] - } - } - - foreach row [split $options(layout) "\n"] { - set columnCount 0 - set rowIncr 1 - foreach column $row { - set columnIncr 1 - set setXweight 0 - set setYweight 0 - set sticky {} - - if {$column eq "="} {set column ".="} - if {$column eq "|"} {set column ".|"} - - regexp -- {(^[^:]+)(:([nsewc]+$)?)} $column -> column -> sticky - - if {[regexp -- {(^[.][^|]+)([|]([nsewc]+$)?)} $column -> column -> sticky]} { - set setXweight 1 - } - if {[regexp -- {(^[.][^=]+)([=]([nsewc]+$)?)} $column -> column -> sticky]} { - set setYweight 1 - } - if {[regexp -- {(^[.][^+]+)([+]([nsewc]+$)?)} $column -> column -> sticky]} { - set setXweight 1 - set setYweight 1 - } - - set layout($column:xweight) 1 - set layout($column:yweight) 1 - - if {$setXweight} {set layout($column:xweight) 0} - if {$setYweight} {set layout($column:yweight) 0} - - switch -- $sticky { - c {set sticky {}} - "" {set sticky w} - } - switch -glob -- $column { - .* { - if {$column eq ".="} { - ::ttk::separator $options(name):line:$columnCount:$rowCount -orient horizontal - set sticky "nsew" - set column $options(name):line:$columnCount:$rowCount - set layout($column:yweight) 0 - } - if {$column eq ".|"} { - ::ttk::separator $options(name):line:$columnCount:$rowCount -orient vertical - set sticky "nsew" - set column $options(name):line:$columnCount:$rowCount - set layout($column:xweight) 0 - } - set column [regsub -all -- {%} $column [string range $options(name) 1 end]] - lappend layout(items) $column - set layout(cell:$columnCount,$rowCount) $column - set layout($column:x) $columnCount - set layout($column:y) $rowCount - set layout($column:xspan) 1 - set layout($column:yspan) 1 - set layout($column:sticky) $sticky - if {$options(-taborder) eq "column"} { - set gpTabOrder([format "%s:%03d%03d001" $options(name) $columnCount $rowCount]) $column - } else { - set gpTabOrder([format "%s:%03d%03d001" $options(name) $rowCount $columnCount]) $column - } - } - - { - if {$columnCount == 0} {error "GRIDPLUS ERROR (layout): Column span not valid in first column"} - set previousColumn [expr {$columnCount - 1}] - set cell $layout(cell:$previousColumn,$rowCount) - set layout(cell:$columnCount,$rowCount) $layout(cell:$previousColumn,$rowCount) - incr layout($cell:xspan) - } - ^ { - if {$rowCount == 0} {error "GRIDPLUS ERROR (layout): Row span not valid in first row"} - set previousRow [expr {$rowCount - 1}] - set previousCell [expr {$columnCount - 1}] - set cell $layout(cell:$columnCount,$previousRow) - set layout(cell:$columnCount,$rowCount) $layout(cell:$columnCount,$previousRow) - if {! ([info exists layout(cell:$previousCell,$rowCount)] && $layout(cell:$previousCell,$rowCount) eq $cell)} { - incr layout($cell:yspan) - } - } - x { - } - > { - set setWeights 1 - set columnIncr 0 - lappend rowWeight1 $rowCount - } - v { - set setWeights 1 - set rowIncr 0 - lappend columnWeight1 $columnCount - } - ~ { - set setWeights 1 - } - default { - error "GRIDPLUS ERROR (layout): Invalid item/option ($column)" - } - } - if {$columnCount > $maxColumn} {set maxColumn $columnCount} - incr columnCount $columnIncr - } - if {$rowCount > $maxRow} {set maxRow $rowCount} - incr rowCount $rowIncr - } - - if {$options(-wtitle) ne "" && [regexp {([.][^.]*)[.].+$} $options(name) -> window]} { - wm title $window [mc $options(-wtitle)] - } - - ::gridplus::gpLabelframe - - foreach item $layout(items) { - set padxLeft $options(-padx) - set padxRight $options(-padx) - - if {$layout($item:x) == 0} { - set padxLeft 0 - } - if {[expr {$layout($item:x) + $layout($item:xspan)}] == $columnCount} { - set padxRight 0 - } - - set padyTop $options(-pady) - set padyBottom $options(-pady) - - if {$layout($item:y) == 0} { - set padyTop 0 - } - if {[expr {$layout($item:y) + $layout($item:yspan)}] == $rowCount} { - set padyBottom 0 - } - - set padx [list $padxLeft $padxRight] - set pady [list $padyTop $padyBottom] - - grid configure $item -in $options(name) -column $layout($item:x) -row $layout($item:y) -columnspan $layout($item:xspan) -rowspan $layout($item:yspan) -sticky $layout($item:sticky) -padx $padx -pady $pady - - if {[info exists layout($item:xweight)]} { - set xweight $layout($item:xweight) - } else { - set xweight 1 - } - if {[info exists layout($item:yweight)]} { - set yweight $layout($item:yweight) - } else { - set yweight 1 - } - - grid columnconfigure $options(name) $layout($item:x) -weight $xweight - grid rowconfigure $options(name) $layout($item:y) -weight $yweight - gpSetTabOrder $options(name) - } - - if {$setWeights} { - for {set rowCount 0} {$rowCount <= $maxRow} {incr rowCount} { - if {[lsearch $rowWeight1 $rowCount] > -1} { - grid rowconfigure $options(name) $rowCount -weight 1 - } else { - grid rowconfigure $options(name) $rowCount -weight 0 - } - } - - for {set columnCount 0} {$columnCount <= $maxColumn} {incr columnCount} { - if {[lsearch $columnWeight1 $columnCount] > -1} { - grid columnconfigure $options(name) $columnCount -weight 1 - } else { - grid columnconfigure $options(name) $columnCount -weight 0 - } - } - } - - if {$options(-wtitle) ne ""} { - wm title [winfo toplevel $options(name)] [mc $options(-wtitle)] - } -} - -#=======================================================================# -# PROC : ::gridplus::gpLine # -# PURPOSE: Gridplus create line. # -#=======================================================================# - -proc ::gridplus::gpLine {} { - upvar 1 options options - - if {$options(-background) eq ""} { - set background [. cget -background] - } else { - set background $options(-background) - } - - if {$options(-title) ne ""} { - frame $options(name) -background $background -padx $options(-padx) -pady $options(-pady) - frame $options(name).left -background $background -borderwidth 2 -height 2 -relief sunken -width 5 - frame $options(name).right -background $background -borderwidth 2 -height 2 -relief sunken - label $options(name).label -background $background -text [mc $options(-title)] -borderwidth 1 - grid configure $options(name).left -column 0 -row 0 -sticky ew - grid configure $options(name).label -column 1 -row 0 - grid configure $options(name).right -column 2 -row 0 -sticky ew - grid columnconfigure $options(name) 2 -weight 1 - } else { - frame $options(name) -background $background -borderwidth $options(-borderwidth) -height $options(-linewidth) -padx $options(-padx) -pady $options(-pady) -relief $options(-linerelief) -width $options(-linewidth) - } -} - -#=======================================================================# -# PROC : ::gridplus::gpMenu # -# PURPOSE: Create menu(bar). # -#=======================================================================# - -proc ::gridplus::gpMenu {} { - upvar 1 options options - - if {$options(name) eq "."} { - set rootMenu .menubar - $options(name) configure -menu $rootMenu - } elseif {[winfo exists $options(name)] && [winfo class $options(name)] eq "Toplevel"} { - set rootMenu $options(name).menubar - $options(name) configure -menu $rootMenu - } else { - set rootMenu $options(name) - } - - menu $rootMenu - - $rootMenu configure -tearoff 0 - - set rootMenuIndex 0 - - foreach {menuLabel menuEntries} $options(layout) { - set underline [string first "_" $menuLabel] - regsub -all -- {_} $menuLabel {} menuLabel - - if {$menuLabel eq "~"} { - ::gridplus::gpMenuOption $rootMenu {} $rootMenuIndex $menuEntries - incr rootMenuIndex - continue - } - - if {[string match @* $menuEntries]} { - set cascade ".[string range $menuEntries 1 end]" - $rootMenu add cascade -label [mc $menuLabel] -menu $cascade -underline $underline - continue - } - - set menu [string tolower $menuLabel] - - $rootMenu add cascade -label [mc $menuLabel] -menu $rootMenu.$menu -underline $underline - menu $rootMenu.$menu - $rootMenu.$menu configure -tearoff 0 - - set menuIndex 0 - - foreach menuEntryData $menuEntries { - ::gridplus::gpMenuOption $rootMenu $menu $menuIndex $menuEntryData - incr menuIndex - } - - incr rootMenuIndex - } - -} - -#=======================================================================# -# PROC : ::gridplus::gpMenuOption # -# PURPOSE: Create menu(bar) option. # -#=======================================================================# - -proc ::gridplus::gpMenuOption {rootMenu menu menuIndex menuEntryData} { - upvar 1 options options - - variable gpInfo - - set menuEntryLabel [lindex $menuEntryData 0] - set menuEntryOptions [lrange $menuEntryData 1 end] - set underline [string first "_" $menuEntryLabel] - - regsub -all -- {_} $menuEntryLabel {} menuEntryLabel - - set menuEntry [string tolower $menuEntryLabel] - - regsub -all -- { } $menuEntry {_} menuEntry - - if {$menuEntry eq "-" || $menuEntry eq "="} { - if {$menu eq ""} { - $rootMenu add separator - } else { - $rootMenu.$menu add separator - } - } else { - if {$menu eq ""} { - set command $rootMenu,$menuEntry - set menuEntryID $rootMenu@$menuIndex - set menuName {} - } else { - set command $rootMenu:$menu,$menuEntry - set menuEntryID $rootMenu.$menu@$menuIndex - set menuName .$menu - } - set cascade {} - set compound none - set menuIcon {} - set state $options(-state) - set validate 0 - - foreach item $menuEntryOptions { - switch -regexp -- $item { - ^% { - set gpInfo($menuEntryID:group) [string range $item 1 end] - } - ^<$ { - set state disabled - } - ^>$ { - set state normal - } - ^!$ { - set validate 1 - } - ^@ { - set cascade ".[string range $item 1 end]" - } - ^[.~].+ { - set command [string range $item 1 end] - } - ^: { - set menuIcon "::icon::[::icons::icons create -file [file join $options(-iconpath) $options(-iconfile)] [string range $item 1 end]]" - set compound left - } - } - } - - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $command]" - } else { - set command "$options(-prefix)[::gridplus::gpCommandFormat $command]" - } - - set state [=% $menuEntryID $state] - - if {$cascade ne ""} { - $rootMenu$menuName add cascade -label [mc $menuEntryLabel] -menu $cascade -state $state -compound $compound -image $menuIcon -underline $underline - } else { - $rootMenu$menuName add command -label [mc $menuEntryLabel] -command "::gridplus::gpCommand {$command} $options(name) $validate" -state $state -compound $compound -image $menuIcon -underline $underline - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpNotebook # -# : ::gridplus::gpNotebookSet # -# PURPOSE: Create notebook. # -#=======================================================================# - -proc ::gridplus::gpNotebook {} { - upvar 1 options options - - global {} - - variable gpTabOrder - - if {$options(-subst)} { - if {[=< substCommandNotebook [=< substCommand 0]]} { - set options(layout) [subst -nobackslashes $options(layout)] - } else { - set options(layout) [subst -nobackslashes -nocommands $options(layout)] - } - } - - ::ttk::notebook $options(name) -padding $options(-padding) - - if {$options(-command) ne ""} { - set command "$options(-command) \[$options(name) index current\] \[$options(name) tab \[$options(name) index current\] -text\];" - } else { - set command "" - } - - bind $options(name) <> "${command}::gridplus::gpNotebookSet $options(name)" - - foreach {tab item} $options(layout) { - set pane [winfo name $item] - $options(name) add [::ttk::frame $options(name).$pane -padding $options(-tabpadding)] -text [mc $tab] - pack $item -in $options(name).$pane -expand 1 -fill both - } - - ::gridplus::gpNotebookSet $options(name) - - if {$options(-wtitle) ne ""} { - wm title [winfo toplevel $options(name)] [mc $options(-wtitle)] - } -} - -proc ::gridplus::gpNotebookSet {name} { - global {} - - variable gpInfo - variable gpValidations - - if {[info exists gpInfo(validation:failed)]} { - foreach windowValidations [array names ::gridplus::gpValidations] { - foreach windowValidation $windowValidations { - foreach validationInfo $::gridplus::gpValidations($windowValidation) { - foreach {entry validation} [split $validationInfo :] {} - if {[info exists gpInfo(validation:failed)] && $gpInfo(validation:failed) eq $entry} { - if {! [::gridplus::gpValidate $entry $validation focusout - - 1]} { - ::gridplus::gpValidateFailed $entry - } - } - } - } - } - - if {[info exists gpInfo(validation:failed)]} { - ::gridplus::gpNotebookIn $gpInfo(validation:failed) - return - } - } - - variable gpTabOrder - - set pane [$name index current] - set panes [$name tabs] - - #!FIX - # regsub -all .[winfo name $name] [lindex $panes $pane] {} item - regsub .[winfo name $name] [lindex $panes $pane] {} item - - set gpTabOrder($name:000000) $item - - gpSetTabOrder $name -} - -#=======================================================================# -# PROC : ::gridplus::gpOptionAlias # -# PURPOSE: Set value for option with "alias". # -#=======================================================================# - -proc ::gridplus::gpOptionAlias {option alias} { - upvar 1 options options - - if {$options($option) ne ""} {return $options($option)} - if {$options($alias) ne ""} {return $options($alias)} - - return {} -} - -#=======================================================================# -# PROC : ::gridplus::gpOptionset # -# PURPOSE: Create optionset. # -#=======================================================================# - -proc ::gridplus::gpOptionset {} { - upvar 1 options options - - variable gpOptionSets - - set gpOptionSets($options(name)) $options(layout) - - if {[lsearch $gpOptionSets($options(name)) -style] < 0 && [=< optionsetDefaultStyle 0]} { - lappend gpOptionSets($options(name)) -style {} - } -} - -#=======================================================================# -# PROC : ::gridplus::gpPack # -# PURPOSE: Pack specified layout where resizing is required. # -#=======================================================================# - -proc ::gridplus::gpPack {} { - upvar 1 options options - - if {$options(-resize) eq ""} { - pack $options(name) - return - } - - if {! [regexp -- {(^[.][^.]+)[.]} $options(name) -> window]} { - set window "." - } - - set resizeX 0 - set resizeY 0 - - switch -- $options(-resize) { - x {set resizeX 1} - y {set resizeY 1} - xy {set resizeX 1; set resizeY 1} - } - - wm minsize $window 1 1 - - update idletasks - - pack $options(name) -expand 1 -fill both - - update idletasks - - regexp -- {^([0-9]+)x([0-9]+)} [wm geometry $window] -> width height - - set width [expr {int(($width / 100.0) * $options(-minx))}] - set height [expr {int(($height / 100.0) * $options(-miny))}] - - wm minsize $window $width $height - wm resizable $window $resizeX $resizeY -} - -#=======================================================================# -# PROC : ::gridplus::gpPane # -# PURPOSE: Create paned window. # -#=======================================================================# - -proc ::gridplus::gpPane {} { - upvar 1 options options - - variable gpInfo - variable gpTabOrder - - ::gridplus::gpLabelframe - - if {[llength [lindex [split $options(layout) "\n"] 0]] > 1} { - set orient horizontal - } else { - set orient vertical - } - - set paneCount 1 - - ::ttk::panedwindow $options(name).pane -height $options(-height) -width $options(-width) -orient $orient - - foreach row [split $options(layout) "\n"] { - set columnCount 0 - - foreach column $row { - if {[regexp -- {(^[^:+|=]+)[:+|=]} $column -> column]} { - set weight 1 - } else { - set weight 0 - } - - $options(name).pane insert end $column - - $options(name).pane pane $column -weight $weight - - set gpTabOrder([format "%s:000000%03d" $options(name) $paneCount]) $column - incr paneCount - } - } - - pack $options(name).pane -expand 1 -fill both - - gpSetTabOrder $options(name) -} - -#=======================================================================# -# PROC : ::gridplus::gpParseEmbeddedGrid # -# PURPOSE: If column contains embedded grid, parse it. # -#=======================================================================# - -proc ::gridplus::gpParseEmbeddedGrid {column} { - - if {! [regexp -- {[|][|:>&<=]} $column]} {return $column} - - set left {} - set right {} - - regsub -- {[|]:[|]} $column {|: __gpBar__ |:} column - regsub -- {[|]>[|]} $column {|> __gpBar__ |:} column - regsub -- {[|]<[|]} $column {|: __gpBar__ |>} column - regsub -- {[|]=[|]} $column {|> __gpBar__ |>} column - - if {"||" in $column} { - regexp -- {^(.*)\|\|(.*)$} $column -> left right - - if {[regexp -- {[|][:>&]} $left]} { - set grid [gpEmbeddedGridParse $left] - set side left - } else { - set label $left - } - - if {[regexp -- {[|][:>&]} $right]} { - set grid [gpEmbeddedGridParse $right] - set side right - } else { - set label $right - } - } else { - set grid [gpEmbeddedGridParse $column] - set side both - } - - switch -- $side { - left {return "$grid .:ew $label"} - right {return "$grid $label .:ew"} - both {return "$grid .:ew"} - } -} - -proc ::gridplus::gpEmbeddedGridParse {grid} { - - set columns {} - set stretch {} - set defaultWidget grid - set leftStretch 0 - set rightStretch 1 - set style {} - set widgetOptions {} - - if {[regexp -- {^(.+) [|][:]$} $grid -> left]} { - set grid $left - set leftStretch 1 - set rightStretch 0 - } - - if {[regexp -- {[|][#]([^ ]*)} $grid -> style]} { - regsub -- {[|][#]([^ ]*)} $grid {} grid - if {$style eq ""} {set style %} - } - - if {[regexp -- {[|][&]([^ ]*)} $grid -> defaultWidget]} { - regsub -- {[|][&]([^ ]*)} $grid {} grid - if {$defaultWidget eq ""} {set defaultWidget "grid"} - } - - if {[regexp -- {[|][(](.*)[)]} $grid -> widgetOptions]} { - regsub -- {[|][(](.*)[)]} $grid {} grid - regsub -- {\&} $widgetOptions {\\&} widgetOptions - if {$widgetOptions ne ""} { - set newGrid {} - foreach item $grid { - set item [list $item] - if {[string match ".*" $item]} { - set item "$widgetOptions $item" - } - set newGrid "$newGrid $item" - } - set grid $newGrid - } - } - - while {[regexp -- {^([^|]*)([|][:>])(.*)$} $grid -> left op right]} { - lappend columns $left - switch -- $op { - |: {lappend stretch 0} - |> {lappend stretch 1;set rightStretch 0} - } - - set grid $right - } - - lappend columns $grid - - regsub -- {__gpBar__} $columns {|} columns - - set stretch "$leftStretch $stretch $rightStretch" - - return "{&& {$stretch} {$defaultWidget} {$style} $columns}" -} - -#=======================================================================# -# PROC : ::gridplus::gpParseTags # -# PURPOSE: Parse tags for text widget. # -#=======================================================================# - -proc ::gridplus::gpParseTags {name tagText position} { - - regsub -all \{ $tagText {!ob:} tagText - regsub -all \} $tagText {!cb:} tagText - regsub -all {\\} $tagText {!bs:} tagText - - set whitespace " \t\r\n" - set pattern <(/?)(\[^$whitespace>]+)\[$whitespace]*(\[^>]*)> - - set substitute "\}\n::gridplus::gpInsertText $name {\\2} {\\1} {\\3} $position \{" - regsub -all $pattern $tagText $substitute tagText - - eval "::gridplus::gpInsertText $name {init} {} {} $position {$tagText}" -} - -#=======================================================================# -# PROC : ::gridplus::gpSet # -# PURPOSE: Gridplus Set values. # -#=======================================================================# - -proc ::gridplus::gpSet {} { - upvar 1 options options - - variable gpConfig - variable gpInfo - variable gpValidation - - foreach option [array names options -*] { - switch -- $option { - -century { - set gpConfig(date:century) $options(-century) - } - -dateformat { - switch -- $options(-dateformat) { - eu { - set gpConfig(date:day) 0 - set gpConfig(date:month) 1 - set gpConfig(date:year) 2 - set gpConfig(date:separator) . - } - iso { - set gpConfig(date:day) 2 - set gpConfig(date:month) 1 - set gpConfig(date:year) 0 - set gpConfig(date:separator) - - } - uk { - set gpConfig(date:day) 0 - set gpConfig(date:month) 1 - set gpConfig(date:year) 2 - set gpConfig(date:separator) / - } - us { - set gpConfig(date:day) 1 - set gpConfig(date:month) 0 - set gpConfig(date:year) 2 - set gpConfig(date:separator) / - } - default { - error "GRIDPLUS ERROR: Invalid date format ($options(-dateformat))." - return - } - } - set gpConfig(dateformat) $options(-dateformat) - } - -errormessage { - set gpConfig(errormessage) $options(-errormessage) - } - -group { - set gpInfo($options(-group)) $options(-state) - ::gridplus::gpSetGroup - } - -locale { - set gpConfig(locale) $options(-locale) - } - -prefix { - set gpConfig(prefix) $options(-prefix) - } - -proc { - set gpConfig(proc) $options(-proc) - } - -validation { - if {$options(-pattern) ne ""} { - set gpValidation($options(-validation)) $options(-pattern) - if {$options(-text) ne ""} { - set gpValidation($options(-validation):text) $options(-text) - } else { - set gpValidation($options(-validation):text) $options(-validation) - } - } - - } - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpSetFont # -# PURPOSE: Gridplus Set font attributes. # -#=======================================================================# - -proc ::gridplus::gpSetFont {attributes} { - - set font [dict create {*}[font configure TkDefaultFont]] - - if {[dict get $font -size] < 0} { - set sign "-" - } else { - set sign "" - } - - foreach attribute $attributes { - switch -regexp -- $attribute { - {^[0-9]+$} { - set font [dict replace $font -size $attribute] - } - {^[+][0-9]+$} { - set font [dict replace $font -size $sign[expr {abs([dict get $font -size]) + $attribute}]] - } - {^[-][0-9]+$} { - set font [dict replace $font -size $sign[expr {abs([dict get $font -size]) - $attribute}]] - } - {^bold$} { - set font [dict replace $font -weight bold] - } - {^underline$} { - set font [dict replace $font -underline 1] - } - {^italic$} { - set font [dict replace $font -slant italic] - } - } - } - - return "[lrange $font 2 end] [lrange $font 0 1]" -} - -#=======================================================================# -# PROC : ::gridplus::gpSetFontSize # -# PURPOSE: Gridplus Set font size for "tagged" text widget. # -#=======================================================================# - -proc ::gridplus::gpSetFontSize {defaultSize newSize} { - - switch -regexp -- $newSize { - {^[0-9]+$} { - set fontSize $newSize - } - {^[+][0-9]+$} { - set value [string range $newSize 1 end] - set fontSize [expr {$defaultSize + $value}] - } - {^[-][0-9]+$} { - set value [string range $newSize 1 end] - set fontSize [expr {$defaultSize - $value}] - } - default { - set fontSize $defaultSize - } - } - - return $fontSize -} - -#=======================================================================# -# PROC : ::gridplus::gpSetGroup # -# PURPOSE: Gridplus Set widgets state to "group" state. # -#=======================================================================# - -proc ::gridplus::gpSetGroup {} { - variable gpInfo - - foreach groupItem [array names gpInfo *:group] { - set item [string map {:group {}} $groupItem] - if {[info exists gpInfo($gpInfo($item:group))]} { - if {[regexp {^([^@]+)@(.+)$} $item -> configureItem index]} { - $configureItem entryconfigure $index -state $gpInfo($gpInfo($item:group)) - } else { - if {[string match *Entry [winfo class $item]] && $gpInfo($gpInfo($item:group)) eq "disabled"} { - $item configure -state [=< entryDisabled readonly] - } elseif {[winfo class $item] in "TSpinbox TCombobox" && $gpInfo($gpInfo($item:group)) eq "normal"} { - $item configure -state readonly - } else { - $item configure -state $gpInfo($gpInfo($item:group)) - } - } - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpSetOptionset # -# PURPOSE: Set optionset options. # -#=======================================================================# - -proc ::gridplus::gpSetOptionset {} { - upvar 1 options options - - variable gpOptionSets - - if {$options(-optionset) eq ""} { - if {$options(-style) ne "" && [info exists gpOptionSets($options(-style))] && [=< optionSetStyle 1]} { - set options(-optionset) $options(-style) - } else { - return - } - } - - if {[info exists gpOptionSets($options(-optionset))]} { - foreach {option value} $gpOptionSets($options(-optionset)) { - if {$option eq "-pad"} { - set options(-padx) $value - set options(-pady) $value - } else { - set options($option) $value - } - } - } else { - error "GRIDPLUS ERROR: Invalid optionset ($options(-optionset))." - } -} - -#=======================================================================# -# PROC : ::gridplus::gpSetTabOrder # -# PURPOSE: Gridplus Set widgets to correct "tab" order. # -#=======================================================================# - -proc ::gridplus::gpSetTabOrder {name} { - variable gpTabOrder - - foreach item [lsort [array names gpTabOrder $name:*]] { - raise $gpTabOrder($item) - ::gridplus::gpSetTabOrder $gpTabOrder($item) - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelist # -# PURPOSE: Create tablelist. # -#=======================================================================# - -proc ::gridplus::gpTablelist {} { - upvar 1 options options - - global {} - - variable gpInfo - - if {! [regexp -- {^[.]([^.]+)[.]} $options(name) -> window]} { - set window {} - } - - set gpInfo($options(name):action) $options(-action) - set gpInfo($options(name):columnsort) $options(-columnsort) - set gpInfo($options(name):iconlibrary) [file join $options(-iconpath) $options(-iconfile)] - set gpInfo($options(name):insertexpr) $options(-insertexpr) - set gpInfo($options(name):insertoptions) $options(-insertoptions) - set gpInfo($options(name):maintainsort) $options(-maintainsort) - set gpInfo($options(name):selectfirst) $options(-selectfirst) - set gpInfo($options(name):selectmode) $options(-selectmode) - set gpInfo($options(name):selectpage) $options(-selectpage) - set gpInfo($options(name):sortorder) $options(-sortorder) - set gpInfo($options(name):validate) $options(-validate) - set gpInfo($options(name):window) .$window - - if {[regsub -all -- {/[^/\} ]*} $options(-insertoptions) {} gpInfo($options(name):trueOptions)]} { - regsub -all -- {[^/\} ]*/} $options(-insertoptions) {} gpInfo($options(name):falseOptions) - } else { - set gpInfo($options(name):trueOptions) $options(-insertoptions) - set gpInfo($options(name):falseOptions) {} - } - - set state $options(-state) - - if {$options(-group) ne ""} { - set gpInfo($options(name).tablelist:group) $options(-group) - } - - set state [=% $options(name).tablelist $state] - -#-------------------------------------# -# Deal with "hide" columns in layout. # -#-------------------------------------# - - set column -1 - set columnNames {} - set count 0 - set first 0 - set hide {} - set index 0 - set sortASCIInocase {} - set sortDictionary {} - set sortInteger {} - set sortReal {} - - foreach item $options(layout) { - - if {[string is integer $item]} { - set count 0 - incr column - } - - if {$item in {asciinocase dicionary hide integer real} && $count > 1} { - switch -- $item { - asciinocase {lappend sortASCIInocase $column} - dictionary {lappend sortDictionary $column} - hide {lappend hide $column} - integer {lappend sortInteger $column} - real {lappend sortReal $column} - } - set options(layout) [lreplace $options(layout) $index $index] - incr index -1 - if {$item eq "hide" && $column == $first} { - incr first - } - } - - if {[string match =* $item]} { - lappend columnNames [list $column [string range $item 1 end]] - set options(layout) [lreplace $options(layout) $index $index] - incr index -1 - } - - incr count - incr index - } - - if {$options(-sortfirst)} { - set gpInfo($options(name):firstcolumn) 0 - } else { - set gpInfo($options(name):firstcolumn) $first - } - - set gpInfo($options(name):seeinsert) $options(-seeinsert) - - ::gridplus::gpLabelframe - - tablelist::tablelist $options(name).tablelist \ - -columns $options(layout) \ - -exportselection 0 \ - -height $options(-height) \ - -listvariable $options(-listvariable) \ - -selectmode $options(-selectmode) \ - -state $state \ - -stretch all \ - -width $options(-width) \ - -xscrollcommand [list $options(name).xbar set] \ - -yscrollcommand [list $options(name).ybar set] \ - -takefocus $options(-takefocus) \ - - if {$options(-columnsort)} { - $options(name).tablelist configure -labelcommand ::gridplus::gpTablelistSort - } - - ::ttk::scrollbar $options(name).xbar -orient horizontal -command [list $options(name).tablelist xview] - ::ttk::scrollbar $options(name).ybar -orient vertical -command [list $options(name).tablelist yview] - - foreach item $hide { - $options(name).tablelist columnconfigure $item -hide 1 - } - - foreach item $sortASCIInocase { - $options(name).tablelist columnconfigure $item -sortmode "asciinocase" - } - - foreach item $sortDictionary { - $options(name).tablelist columnconfigure $item -sortmode "dictionary" - } - - foreach item $sortInteger { - $options(name).tablelist columnconfigure $item -sortmode "integer" - } - - foreach item $sortReal { - $options(name).tablelist columnconfigure $item -sortmode "real" - } - - for {set column 0} {$column < [$options(name).tablelist columncount]} {incr column} { - set columnName [string tolower [$options(name).tablelist columncget $column -title]] - regsub -all -- {[ ]+} $columnName {_} columnName - regsub -all -- {[^a-z0-9_]} $columnName {} columnName - $options(name).tablelist columnconfigure $column -name $columnName - } - - foreach item $columnNames { - $options(name).tablelist columnconfigure [lindex $item 0] -name [lindex $item 1] - } - - if {$options(-names) ne ""} { - ::gridplus::gpTablelistSetColumns $options(name) -name $options(-names) - } - - for {set column 0} {$column < [$options(name).tablelist columncount]} {incr column} { - lappend gpInfo($options(name):columnNames) [$options(name).tablelist columncget $column -name] - } - - grid $options(name).tablelist -row 0 -column 0 -sticky news - - switch -- $options(-scroll) { - x { - grid $options(name).xbar -row 1 -column 0 -sticky ew - } - y { - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - xy { - grid $options(name).xbar -row 1 -column 0 -sticky ew - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - } - - grid rowconfigure $options(name) 0 -weight 1 - grid columnconfigure $options(name) 0 -weight 1 - - foreach item $options(-tableoptions) { - switch -- $item { - stripe { - $options(name).tablelist configure -stripebackground #e0e8f0 - } - separator { - $options(name).tablelist configure -showseparators yes - } - } - } - - foreach unknownItem [array names gpInfo *] { - set unknownOption [string map { {}} $unknownItem] - $options(name).tablelist configure $unknownOption $gpInfo($unknownItem) - } - - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $options(name)]" - } else { - if {$options(-command) eq ""} { - set command "$options(-prefix)[::gridplus::gpCommandFormat $options(name)]" - } else { - set command $options(-command) - } - } - - set gpInfo($options(name):command) $command - - switch -- $options(-action) { - double { - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window 0]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpCommand [list $command] .$window $options(-validate)]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window 0]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window 0]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window 0]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window 0]" - } - single { - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate) [list $command]]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate) [list $command]]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate) [list $command]]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window $options(-validate) [list $command]]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window $options(-validate) [list $command]]" - } - default { - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate)]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate)]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) \[$options(name).tablelist curselection\] .$window $options(-validate)]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window $options(-validate)]" - bind [$options(name).tablelist bodypath] "after 1 [list ::gridplus::gpTablelistSelect $options(name) - .$window $options(-validate)]" - } - } - - if {$options(-menu) ne ""} { - bind [$options(name).tablelist bodypath] "after 1 {::gridplus::gpTablelistMenu $options(-menu) %x %y %X %Y %W $options(name)}" - } - - bind ::$options(name) "rename ::$options(name) {}" - rename ::$options(name) ::gridplus::$options(name):frame - - proc ::$options(name) {args} { - - set thisProc [lindex [info level 0] 0] - set frameProc "::gridplus::$thisProc:frame" - - if {[lindex $args 0] in "configure cget"} { - $frameProc {*}$args - } else { - ::gridplus::gpget $thisProc [lindex $args 0] - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistColumnIndex # -# PURPOSE: Returns tablelist numeric column index for column "index". # -#=======================================================================# - -proc ::gridplus::gpTablelistColumnIndex {item index caller} { - variable gpInfo - - if {[string is integer $index]} { - return $index - } else { - if {[set columnIndex [lsearch $gpInfo($item:columnNames) $index]] == -1} { - error "GRIDPLUS ERROR: ($caller) Column name \"$index\" does not exist." - } else { - return $columnIndex - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistInsert # -# PURPOSE: Inserts/updates tablelist line. # -#=======================================================================# - -proc ::gridplus::gpTablelistInsert {item position line {gpset 0} {update 0}} { - variable gpInfo - - set column 0 - set tableLine {} - - unset -nocomplain tableIcon - - foreach tableColumn $line { - if {[regexp -- {^:([^ ]+) ?} $tableColumn -> tableIcon($column)]} { - regsub -- {^:([^ ]+) ?} $tableColumn {} tableColumn - } - lappend tableLine $tableColumn - incr column - } - - if {$update} { - $item.tablelist rowconfigure $position -text $tableLine - } else { - $item.tablelist insert $position $tableLine - } - - if {[info exists tableIcon]} { - foreach iconColumn [array names tableIcon] { - set icon $tableIcon($iconColumn) - set image "::icon::$icon" - if {$image ni [image names]} {::icons::icons create -file $gpInfo($item:iconlibrary) $icon} - $item.tablelist cellconfigure $position,$iconColumn -image $image - } - } - - if {$gpInfo($item:insertexpr) ne ""} { - gpTablelistInsertExpr $item $position $line - } - - if {$gpInfo($item:seeinsert) && ! $gpset} { - update idletasks - $item.tablelist see $position - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistInsertExpr # -# PURPOSE: Expand tablelist insert expression. # -#=======================================================================# - -proc ::gridplus::gpTablelistInsertExpr {name position line} { - upvar 1 options options - - variable gpInfo - - regsub -all -- {%([a-zA-Z0-9_]+)} $gpInfo($name:insertexpr) {[lindex $line [::gridplus::gpTablelistColumnIndex $name \1 "gpTablelistInsertExpr"]]} insertExpr - - eval "if {$insertExpr} {set result 1} else {set result 0}" - - ::gridplus::gpTablelistInsertOptions $name $position $result -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistInsertOptions # -# PURPOSE: Process tablelist insert options. # -#=======================================================================# - -proc ::gridplus::gpTablelistInsertOptions {name position result} { - upvar 1 options options - - variable gpInfo - - if {$result} { - foreach insertOption $gpInfo($name:trueOptions) { - if {[lindex $insertOption 0] eq "*"} { - regsub -- {[*]} $insertOption $position insertOption - eval "$name.tablelist rowconfigure $insertOption" - } else { - eval "$name.tablelist cellconfigure $position,$insertOption" - } - } - } else { - if {$gpInfo($name:falseOptions) ne ""} { - foreach insertOption $gpInfo($name:falseOptions) { - if {[lindex $insertOption 0] eq "*"} { - regsub -- {[*]} $insertOption $position insertOption - eval "$name.tablelist rowconfigure $insertOption" - } else { - eval "$name.tablelist cellconfigure $position,$insertOption" - } - } - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistMenu # -# PURPOSE: Right-click pop-up menu for tablelist. # -#=======================================================================# - -proc ::gridplus::gpTablelistMenu {menu x y X Y W name} { - global {} - - foreach {Widget xPosition yPosition} [tablelist::convEventFields $W $x $y] {} - set row [$name.tablelist nearest $yPosition] - - $name.tablelist selection clear 0 end - $name.tablelist selection set $row - - set ($name) [$name.tablelist get $row] - - $menu post $X $Y -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistSelect # -# PURPOSE: Sets value for tablelist selections. # -#=======================================================================# - -proc ::gridplus::gpTablelistSelect {name selection window validate {command {}}} { - upvar 1 options options - - global {} - - variable gpInfo - - if {$selection eq "-"} { - if {$gpInfo($name:selectpage) && $gpInfo($name:selectmode) eq "browse"} { - $name.tablelist selection clear 0 end - $name.tablelist selection set [$name.tablelist index active] - set selection [$name.tablelist curselection] - } else { - return - } - } - - set count [llength $selection] - set value [$name.tablelist get $selection] - - if {$gpInfo($name:selectmode) eq "multiple" || $gpInfo($name:selectmode) eq "extended"} { - if {$count == 1} { - set ($name) [list $value] - } else { - set ($name) $value - } - } else { - set ($name) $value - } - - if {$command ne ""} {{*}[list ::gridplus::gpCommand $command $window $validate]} -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistSetColumns # -# PURPOSE: Set tablelist column titles/names. # -#=======================================================================# - -proc ::gridplus::gpTablelistSetColumns {name option values} { - - set column 0 - - foreach value $values { - $name.tablelist columnconfigure $column $option $value - incr column - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTablelistSort # -# PURPOSE: Sort tablelist and save last sort. # -#=======================================================================# - -proc ::gridplus::gpTablelistSort {name column} { - - variable gpInfo - - ::tablelist::sortByColumn $name $column - - set item [regsub -- {[.]tablelist$} $name {}] - - set gpInfo($item:lastsortcolumn) [$name sortcolumn] - set gpInfo($item:lastsortorder) [$name sortorder] -} - -#=======================================================================# -# PROC : ::gridplus::gpText # -# PURPOSE: Create text. # -#=======================================================================# - -proc ::gridplus::gpText {} { - upvar 1 options options - - global {} - - variable gpInfo - - set state $options(-state) - - if {$options(-group) ne ""} { - set gpInfo($options(name).text:group) $options(-group) - } - - set state [=% $options(name).text $state] - - ::gridplus::gpLabelframe - - text $options(name).text \ - -background white \ - -height $options(-height) \ - -font TkTextFont \ - -state $state \ - -tabs {0.5c 1c 1.5c 2c 2.5c 3.0c 3.5c 4.0c 4.5c 5.0c 5.5c 6.0c 6.5c 7.0c 7.5c 8.0c} \ - -takefocus $options(-takefocus) \ - -width $options(-width) \ - -wrap $options(-wrap) \ - -xscrollcommand [list $options(name).xbar set] \ - -yscrollcommand [list $options(name).ybar set] \ - - ::ttk::scrollbar $options(name).xbar -orient horizontal -command [list $options(name).text xview] - ::ttk::scrollbar $options(name).ybar -orient vertical -command [list $options(name).text yview] - - grid $options(name).text -row 0 -column 0 -sticky news - - switch -- $options(-scroll) { - x { - grid $options(name).xbar -row 1 -column 0 -sticky ew - } - y { - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - xy { - grid $options(name).xbar -row 1 -column 0 -sticky ew - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - } - - grid rowconfigure $options(name) 0 -weight 1 - grid columnconfigure $options(name) 0 -weight 1 - - set gpInfo($options(name):seeinsert) $options(-seeinsert) - - if {$options(-tags)} { - set normalColor [lindex [split $options(-linkcolor) /] 0] - set overColor [lindex [split $options(-linkcolor) /] 1] - set normalStyle [lindex [split $options(-linkstyle) /] 0] - set overStyle [lindex [split $options(-linkstyle) /] 1] - - regsub -- {[&]} $overStyle $normalStyle, overStyle - - if {! [string match */* $options(-linkcolor)]} {set overColor $normalColor} - if {! [string match */* $options(-linkstyle)]} {set overStyle $normalStyle} - - if {$normalColor eq ""} {set normalColor "blue"} - if {$overColor eq ""} {set overColor "blue"} - - if {$normalStyle eq "underline"} { - set normalStyle "true" - } else { - set normalStyle "false" - } - if {$overStyle eq "underline"} { - set overStyle "true" - } else { - set overStyle "false" - } - - set gpInfo($options(name):bgcolor) white - set gpInfo($options(name):defaultbg) white - set gpInfo($options(name):defaultfg) black - set gpInfo($options(name):defaultfont) helvetica - set gpInfo($options(name):defaultsize) [::gridplus::gpGetFontSize [$options(name).text cget -font]] - set gpInfo($options(name):fgcolor) black - set gpInfo($options(name):font) [lindex [$options(name).text cget -font] 0] - set gpInfo($options(name):iconlibrary) [file join $options(-iconpath) $options(-iconfile)] - set gpInfo($options(name):indent) 0 - set gpInfo($options(name):link) blue - set gpInfo($options(name):linkcursor) $options(-linkcursor) - set gpInfo($options(name):normalcolor) $normalColor - set gpInfo($options(name):normalstyle) $normalStyle - set gpInfo($options(name):overcolor) $overColor - set gpInfo($options(name):overstyle) $overStyle - set gpInfo($options(name):prefix) $options(-prefix) - set gpInfo($options(name):proc) $options(-proc) - set gpInfo($options(name):size) [::gridplus::gpGetFontSize [$options(name).text cget -font]] - set gpInfo($options(name):tagid) 0 - set gpInfo($options(name):tags) 1 - - $options(name).text configure -cursor {} -state disabled - } else { - if {$options(-font) ne ""} { - $options(name).text configure -font $options(-font) - } - - set gpInfo($options(name):tags) 0 - } - - if {$options(-menu) eq ""} { - set menuName $options(name).text.edit - - menu $menuName -tearoff 0 - - if {$options(-tags) || $options(-state) eq "disabled"} { - $options(name).text.edit add command -label [mc "Copy"] -command "tk_textCopy $options(name).text" - $options(name).text.edit add separator - $options(name).text.edit add command -label [mc "Find"] -command "::gridplus::gpTextFind $options(name).text" - } else { - $options(name).text.edit add command -label [mc "Cut"] -command "tk_textCut $options(name).text;$options(name).text edit modified 1" - $options(name).text.edit add command -label [mc "Copy"] -command "tk_textCopy $options(name).text" - $options(name).text.edit add command -label [mc "Paste"] -command "tk_textPaste $options(name).text;$options(name).text edit modified 1" - $options(name).text.edit add separator - $options(name).text.edit add command -label [mc "Find"] -command "::gridplus::gpTextFind $options(name).text" - } - } else { - set menuName $options(-menu) - } - - if {$options(-command) ne ""} { - bind $options(name).text <> "::gridplus::gpTextSet $options(name) ; eval $options(-command)" - } else { - bind $options(name).text <> "::gridplus::gpTextSet $options(name)" - } - - bind $options(name).text "tk_popup $menuName %X %Y" - bind $options(name).text "[bind all ];break" - bind $options(name).text "[bind all <>]; break" - - set ($options(name)) {} - - if {$options(-autogroup) ne ""} { - set autoGroupCommand "::gridplus::gpAutoGroup $options(name) $options(-autogroup) normal" - trace add variable ($options(name)) write $autoGroupCommand - } - -} - -#=======================================================================# -# PROC : ::gridplus::gpTextSet # -# PURPOSE: Set contents of GRIDPLUS Text. # -#=======================================================================# - -proc ::gridplus::gpTextSet {item} { - global {} - - if {[$item.text edit modified]} { - set ($item) {} - - foreach {key text index} [$item.text dump -text 1.0 end] { - set ($item) "$($item)$text" - } - - $item.text edit modified 0 - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTextInsert # -# PURPOSE: Inserts line into text. # -#=======================================================================# - -proc ::gridplus::gpTextInsert {item position line} { - variable gpInfo - - set textState [$item.text cget -state] - - $item.text configure -state normal - - if {$position eq "end"} { - set insertPosition end - } else { - set insertPosition $position.0 - } - - if {$gpInfo($item:tags)} { - if {$position eq "end"} { - ::gridplus::gpParseTags $item $line $insertPosition - $item.text insert $insertPosition "\n" - } else { - $item.text insert $position.0 "\n" - ::gridplus::gpParseTags $item $line $position.end - } - $item.text tag raise sel - } else { - $item.text insert $insertPosition "$line\n" - $item.text edit modified 0 - set ($item) {} - foreach {key text index} [$item.text dump -text 1.0 end] { - set ($item) "$($item)$text" - } - } - - $item.text configure -state $textState - - if {$gpInfo($item:seeinsert)} { - update idletasks - $item.text see $insertPosition - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTextFind # -# PURPOSE: Find string in GRIDPLUS Text. # -#=======================================================================# - -proc ::gridplus::gpTextFind {item} { - variable gpInfo - - if {[winfo exists .gpTextFind]} { - ::gridplus::gpTextFind:action,cancel - } - - if {[string match *?.text $item]} { - set gpInfo() $item - } else { - set gpInfo() $item.text - } - - gridplus window .gpTextFind -topmost 1 -wcmd ::gridplus::gpTextFind:action,cancel -wtitle Find - - gridplus checkbutton .gpTextFind.match -padding 0 { - {.word "Match whole word only"} - {.case "Match case"} - } - - gridplus radiobutton .gpTextFind.direction -title Direction { - {. Up -backwards} {. Down +forwards} - } - - gridplus button .gpTextFind.action -prefix gridplus:: { - {&e "Find What: " .string 38 + >next ~gpTextFind.action,next} {"Find Next" .next < %next} - {@gpTextFind.match |> @gpTextFind.direction} {"Cancel" .cancel} - } - - pack .gpTextFind.action -} - -#=======================================================================# -# PROC : ::gridplus::gpTextFind:action,next # -# PURPOSE: Find next/previous occurance of string in GRIDPLUS Text. # -#=======================================================================# - -proc ::gridplus::gpTextFind:action,next {} { - global {} - - variable gpInfo - - if {$(.gpTextFind.direction) eq "forwards"} { - set searchIndex "insert+1char" - } else { - set searchIndex "insert" - } - - if {$(.gpTextFind.match,word)} { - set matchWord "-regexp" - set pattern "\[\[:<:\]\]$(.gpTextFind.action,string)\[\[:>:\]\]" - } else { - set matchWord "-exact" - set pattern "$(.gpTextFind.action,string)" - } - - if {$(.gpTextFind.match,case)} { - set position [$gpInfo() search -$(.gpTextFind.direction) $matchWord -- $pattern $searchIndex] - } else { - set position [$gpInfo() search -$(.gpTextFind.direction) $matchWord -nocase -- $pattern $searchIndex] - } - - if {$position ne ""} { - catch "$gpInfo() tag remove sel sel.first sel.last" - $gpInfo() tag add sel $position $position+[string length $(.gpTextFind.action,string)]chars - $gpInfo() configure -inactiveselectbackground [$gpInfo() cget -selectbackground] - $gpInfo() mark set insert $position - $gpInfo() see $position - } -} - -#=======================================================================# -# PROC : ::gridplus::gpTextFind:action,cancel # -# PURPOSE: Cancel/close Find dialog. # -#=======================================================================# - -proc ::gridplus::gpTextFind:action,cancel {} { - global {} - - variable gpInfo - - set gpInfo() {} - - gridplus clear .gpTextFind - destroy .gpTextFind -} - -#=======================================================================# -# PROC : ::gridplus::gpTree # -# PURPOSE: Create tree. # -#=======================================================================# - -proc ::gridplus::gpTree {} { - upvar 1 options options - - global {} - - variable gpInfo - - if {! [regexp -- {^[.]([^.]+)[.]} $options(name) -> window]} { - set window {} - } - - set gpInfo($options(name):action) $options(-action) - set gpInfo($options(name):fileicon) $options(-fileicon) - set gpInfo($options(name):foldericon) $options(-foldericon) - set gpInfo($options(name):iconlibrary) [file join $options(-iconpath) $options(-iconfile)] - set gpInfo($options(name):icons) $options(-icons) - set gpInfo($options(name):open) $options(-open) - set gpInfo($options(name):selectfirst) $options(-selectfirst) - set gpInfo($options(name):validate) $options(-validate) - set gpInfo($options(name):window) .$window - - ::gridplus::gpLabelframe - - ::ttk::treeview $options(name).tree \ - -cursor left_ptr \ - -height $options(-height) \ - -selectmode $options(-selectmode) \ - -show $options(-show) \ - -xscrollcommand [list $options(name).xbar set] \ - -yscrollcommand [list $options(name).ybar set] - - $options(name).tree column #0 -width $options(-width) - - ::ttk::scrollbar $options(name).xbar -orient horizontal -command [list $options(name).tree xview] - ::ttk::scrollbar $options(name).ybar -orient vertical -command [list $options(name).tree yview] - - grid $options(name).tree -row 0 -column 0 -sticky news - - switch -- $options(-scroll) { - x { - grid $options(name).xbar -row 1 -column 0 -sticky ew - } - y { - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - xy { - grid $options(name).xbar -row 1 -column 0 -sticky ew - grid $options(name).ybar -row 0 -column 1 -sticky ns - } - } - - grid rowconfigure $options(name) 0 -weight 1 - grid columnconfigure $options(name) 0 -weight 1 - - if {$options(-proc)} { - set command "gpProc [::gridplus::gpCommandFormat $options(name)]" - } else { - if {$options(-command) eq ""} { - set command "$options(-prefix)[::gridplus::gpCommandFormat $options(name)]" - } else { - set command $options(-command) - } - } - - set gpInfo($options(name):command) $command - - switch -- $options(-action) { - double { - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window 0]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window 0]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window 0]" - bind $options(name).tree "after 1 [list ::gridplus::gpCommand [list $command] .$window $options(-validate)]" - } - single { - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - } - single/space { - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate) [list $command]]" - } - default { - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate)]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate)]" - bind $options(name).tree "after 1 [list ::gridplus::gpTreeSelect $options(name) .$window $options(-validate)]" - } - } - - if {$options(-menu) ne ""} { - bind $options(name).tree "after 1 {::gridplus::gpTreeMenu $options(-menu) %x %y %X %Y %W $options(name)}" - } - - if {[lsearch [image names] ::icon::$options(-fileicon)] < 0} { - ::icons::icons create -file [file join $options(-iconpath) $options(-iconfile)] $options(-fileicon) - } - if {[lsearch [image names] ::icon::$options(-foldericon)] < 0} { - ::icons::icons create -file [file join $options(-iconpath) $options(-iconfile)] $options(-foldericon) - } - - set ($options(name)) {} -} - -#=======================================================================# -# PROC : ::gridplus::gpTreeMenu # -# PURPOSE: Right-click pop-up menu for tree. # -#=======================================================================# - -proc ::gridplus::gpTreeMenu {menu x y X Y W name} { - global {} - - $name.tree selection remove $($name) - - set item [lindex [$name.tree identify $x $y] 1] - - $name.tree selection set $item - - set ($name) [$name.tree selection] - - $menu post $X $Y -} - -#=======================================================================# -# PROC : ::gridplus::gpTreeSelect # -# PURPOSE: Sets value for tree selections. # -#=======================================================================# - -proc ::gridplus::gpTreeSelect {name window validate {command {}}} { - global {} - - set ($name) [regsub -all "\034" [$name.tree selection] { }] - - if {$command ne ""} {{*}[list ::gridplus::gpCommand $command $window $validate]} -} - -#=======================================================================# -# PROC : ::gridplus::gpTreeSet # -# PURPOSE: Set contents of GRIDPLUS Tree. # -#=======================================================================# - -proc ::gridplus::gpTreeSet {name nodes} { - variable gpInfo - - $name.tree delete [$name.tree children {}] - - foreach node $nodes { - set icon {} - set nodeText {} - set nodeType file - - foreach item $node { - switch -regexp -- $item { - ^: { - set icon [string range $item 1 end] - } - ^[+]$ { - set nodeType folder - } - ^[/] { - regsub -all { } $item "\034" nodeFullName - } - default { - set nodeText $item - } - } - } - - if {! [regexp {^(.*/)([^/]+)$} $nodeFullName -> path nodeName]} { - set path $nodeFullName - set nodeName $nodeFullName - set indent "" - } - - if {$nodeText ne ""} { - set nodeName $nodeText - } else { - regsub -all "\034" $nodeName { } nodeName - } - - set nodeName [mc $nodeName] - - if {$icon eq ""} { - set icon $gpInfo($name:${nodeType}icon) - } else { - if {[lsearch [image names] ::icon::$icon] < 0} { - ::icons::icons create -file $gpInfo($name:iconlibrary) $icon - } - } - - if {$path eq "/"} { - set parent {} - } else { - regsub -- {/$} $path {} parent - } - - if {$gpInfo($name:icons)} { - $name.tree insert $parent end -id $nodeFullName -image ::icon::$icon -open $gpInfo($name:open) -text $nodeName - } else { - $name.tree insert $parent end -id $nodeFullName -open $gpInfo($name:open) -text $nodeName - } - } - - if {$gpInfo($name:selectfirst)} { - gpselect $name [lindex [$name.tree children {}] 0] - } -} - -#=======================================================================# -# PROC : ::gridplus::gpValidate # -# PURPOSE: Validates contents of entry. # -#=======================================================================# - -proc ::gridplus::gpValidate {item validation condition prevalue fixed auto} { - global {} - - variable gpConfig - variable gpInfo - variable gpValidateError - variable gpValidation - - set focus [focus] - - if {$focus ne ""} { - set focusClass [winfo class $focus] - set focusToplevel [winfo toplevel $focus] - # Set toplevel to modal if unknown (for Tk dialogs?) - if {[info exists gpInfo($focusToplevel:modal)]} { - set focusToplevelModal $gpInfo($focusToplevel:modal) - } else { - set focusToplevelModal 1 - } - } else { - set focusClass "" - set focusToplevel "" - set focusToplevelModal 0 - } - - if {[info exists gpInfo(validation:failed)]} { - set failedItem $gpInfo(validation:failed) - set failedItemToplevel [winfo toplevel $failedItem] - set failedItemToplevelModal $gpInfo($failedItemToplevel:modal) - } else { - set failedItem "" - set failedItemToplevel "" - set failedItemToplevelModal 0 - } - - set itemToplevel [winfo toplevel $item] - set itemToplevelModal $gpInfo($itemToplevel:modal) - - if {[info exists gpInfo($focus:validationmode)]} { - set validationMode $gpInfo($focus:validationmode) - } else { - set validationMode "" - } - - switch -- $condition { - focusout { - if {$focusToplevel ne $itemToplevel && $focusToplevelModal} { - return 1 - } - if {$failedItem ne "" && $failedItem ne $item} { - if {$failedItemToplevel ne $itemToplevel && $itemToplevelModal} { - unset -nocomplain gpInfo(validation:failed) - } - - return 1 - } - } - - focusin { - if {$failedItem ne ""} { - if {$itemToplevelModal && ! $failedItemToplevelModal} { - $failedItem configure -foreground black - - if {[set window $failedItemToplevel] eq "."} { - set window {} - } - - if {[winfo exists $window.errormessage]} { - $window.errormessage configure -text {} - } - - unset -nocomplain gpInfo(validation:failed) - - ::gridplus::gpValidateErrorCancel - - 0 - - return 1 - } - - if {$failedItemToplevel ne $itemToplevel} { - focus $failedItem - return 1 - } - } - } - - key { - if {[string length $prevalue] > $fixed} { - return 0 - } - return 1 - } - } - - if {$validation eq "__gpFixed__" || $condition ne "focusout" || ! $auto} { - return 1 - } - - if {$focusClass in "Button TButton" && $validationMode ne "focus" && $prevalue ne "-"} { - return 1 - } - - if {! [regexp {^([.][^.,]+)} $item -> window]} { - set window {} - } else { - if {[winfo class $window] ne "Toplevel"} { - set window {} - } - } - - set validationOK 0 - - regexp -- {@?([^:?]+)(:([^?]*))*([?](.*))*} $validation -> validationName -> parameter -> errorText - - if {[string match @* $validation] && $($item) eq ""} { - set validationOK 1 - } else { - switch -glob -- $gpValidation($validationName) { - proc:* { - set validateProc [string range $gpValidation($validationName) 5 end] - if {[$validateProc $item $parameter]} { - set validationOK 1 - } - } - trim:* { - set ($item) [string trim $($item)] - if {[regexp [string range $gpValidation($validationName) 5 end] $($item)]} { - set validationOK 1 - } - } - default { - if {[regexp $gpValidation($validationName) $($item)]} { - set validationOK 1 - } - } - } - } - - if $validationOK { - $item configure -foreground black - - if {[winfo exists $window.errormessage]} { - $window.errormessage configure -text {} - } - - unset -nocomplain gpInfo(validation:failed) - - ::gridplus::gpValidateErrorCancel - - 0 - - return 1 - } else { - if {$focus ne ""} { - ::gridplus::gpNotebookIn $item - } - - update idletasks - - set gpInfo(validation:failed) $item - - return 0 - } -} - -#=======================================================================# -# PROC : ::gridplus::gpValidateFailed # -# PURPOSE: Sets focus to failed validation entry. # -#=======================================================================# - -proc ::gridplus::gpValidateFailed {item} { - - variable gpInfo - - if {! [winfo exists $item]} { - return - } - - set focus [focus] - - if {[string compare {} $focus] && [winfo class $focus] eq "Entry"} { - $focus selection clear - - if {[regexp {^(focus(out)?|all)} [set validate [$focus cget -validate]]]} { - $focus configure -validate none - after idle [list $focus configure -validate $validate] - } - } - - if {[info exists gpInfo(validation:failed)]} { - if {[set window [winfo toplevel $item]] eq "."} { - set window {} - } - after 1 "[list focus $item]; ::gridplus::gpValidateErrorDisplay $item" - } -} - -#=======================================================================# -# PROC : ::gridplus::gpValidateErrorDisplay # -# PURPOSE: Display validation error messages. # -#=======================================================================# - -proc ::gridplus::gpValidateErrorDisplay {item} { - variable gpValidateError - - if {! [regexp {^([.][^.,]+)} $item -> window]} { - set window {} - } else { - if {[winfo class $window] ne "Toplevel"} { - set window {} - } - } - - if {[winfo exists $window.errormessage]} { - $window.errormessage configure -text $gpValidateError($item:text) - } - - if {$gpValidateError($item:popup)} { - ::gridplus::gpValidateErrorShow $item - } - - $item configure -foreground red -} - -#=======================================================================# -# PROCS : ::gridplus::gpValidateErrorInit # -# : ::gridplus::gpValidateErrorCancel # -# : ::gridplus::gpValidateErrorShow # -# PURPOSE: Gridplus widget validation "pop-up" error message. # -#=======================================================================# - -proc ::gridplus::gpValidateErrorInit {item message {mode label}} { - variable gpValidateError - - if {! [winfo exists .gpValidateError]} { - toplevel .gpValidateError -background black -borderwidth 1 -relief flat - label .gpValidateError.message -background red -foreground white - pack .gpValidateError.message - wm overrideredirect .gpValidateError 1 - wm withdraw .gpValidateError - } - - if {$mode eq "popup"} { - set gpValidateError($item:popup) 1 - } else { - set gpValidateError($item:popup) 0 - } - - set gpValidateError($item:text) $message -} - -proc ::gridplus::gpValidateErrorCancel {testWindow eventWindow binding} { - variable gpInfo - variable gpValidateError - - if {! $binding && [info exists gpInfo(validation:failed)]} { - return 1 - } - - if {$testWindow eq $eventWindow} { - if {[winfo exists .gpValidateError]} { - wm withdraw .gpValidateError - } - } -} - -proc ::gridplus::gpValidateErrorShow {item} { - variable gpValidateError - - .gpValidateError.message configure -text $gpValidateError($item:text) - - set helpX [expr [winfo rootx $item] + 10] - set helpY [expr [winfo rooty $item] + [expr {[winfo height $item] - 1}]] - - wm geometry .gpValidateError +$helpX+$helpY - wm deiconify .gpValidateError - - raise .gpValidateError -} - -#=======================================================================# -# PROC : ::gridplus::gpValidateText # -# PURPOSE: Returns formatted validation message text. # -#=======================================================================# - -proc ::gridplus::gpValidateText {validation} { - variable gpConfig - variable gpValidation - - regexp -- {@?([^:?]+)(:([^?]*))*([?](.*))*} $validation -> validationName -> parameter -> errorText - - if {$errorText eq ""} { - set errorText [mc $gpValidation($validationName:text)] - set errorMessage [mc $gpConfig(errormessage)] - regsub {%} $errorText $parameter errorText - regsub {%} $errorMessage $errorText errorMessage - return $errorMessage - } else { - return $errorText - } -} - -#=======================================================================# -# PROC : ::gridplus::gpValidateDate # -# PURPOSE: Validates for valid date. # -#=======================================================================# - -proc ::gridplus::gpValidateDate {entry parameter} { - global {} - - foreach {month day year} [::gridplus::gpFormatDate $($entry) internal] {} - - set day [scan $day "%d"] - set month [scan $month "%d"] - set result 0 - - if {$month < 1 || $month > 12} { - return 0 - } else { - if {$day < 1 || $day > [::gridplus::gpCalMonthDays $month $year]} { - return 0 - } else { - set ($entry) [::gridplus::gpFormatDate $($entry) application] - $entry configure -validate focusout - return 1 - } - } -} - -#=======================================================================# -# PROCS : ::gridplus::gpGridIn # -# : ::gridplus::gpPackIn # -# : ::gridplus::gpNotebokIn # -# PURPOSE: If validated entry in notebook select pane containing entry. # -#=======================================================================# - -proc ::gridplus::gpGridIn {name} { - - array set info [grid info $name] - - if {[info exists info(-in)]} { - return $info(-in) - } else { - return {} - } -} - -proc ::gridplus::gpPackIn {name} { - - if {! [catch "pack info $name"]} { - array set info [pack info $name] - return $info(-in) - } else { - return {} - } -} - -proc ::gridplus::gpNotebookIn {name} { - global {} - - variable gpTabOrder - - set in $name - - while {[set in [gpGridIn $in]] ne ""} { - set lastIn $in - } - - set in $lastIn - - while {[set in [gpPackIn $in]] ne ""} { - set lastIn $in - } - - set toplevelLastIn {} - - if {[winfo class $lastIn] eq "Toplevel"} { - foreach item [array names ::gridplus::gpInfo *:in] { - if {$::gridplus::gpInfo($item) eq $lastIn} { - set in [lindex [split $item :] 0] - set toplevelLastIn $in - while {[set in [gpPackIn $in]] ne ""} { - set lastIn $in - } - } - } - } - - if {[regexp -- {(.*)[.]([^.]+$)} $lastIn -> containedIn]} { - - if {$containedIn eq "" && $toplevelLastIn ne ""} { - gpNotebookIn $toplevelLastIn - } elseif {[winfo exists $containedIn] && [winfo class $containedIn] eq "TNotebook"} { - $containedIn select $lastIn - - set pane [$containedIn index current] - set panes [$containedIn tabs] - - regsub -all .[winfo name $containedIn] [lindex $panes $pane] {} item - - set gpTabOrder($containedIn:000000) $item - - gpSetTabOrder $containedIn - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpFormatDate # -# PURPOSE: Converts date format for validation and display. # -#=======================================================================# - -proc ::gridplus::gpFormatDate {date mode} { - variable gpConfig - - if {$gpConfig(dateformat) eq "iso"} { - switch -regexp -- $date { - {^[0-9]{8}$} { - set part(0) [string range $date 0 3] - set part(1) [string range $date 4 5] - set part(2) [string range $date 6 7] - } - {^[0-9]{4}-[0-9]{2}-[0-9]{2}$} { - set part(0) [string range $date 0 3] - set part(1) [string range $date 5 6] - set part(2) [string range $date 8 9] - } - default { - set part(0) 0 - set part(1) 0 - set part(2) 0 - } - } - } else { - switch -regexp -- $date { - {^[0-9]{6}$} { - set part(0) [string range $date 0 1] - set part(1) [string range $date 2 3] - set part(2) [string range $date 4 5] - if {$part(2) <= $gpConfig(date:century)} { - set part(2) "20$part(2)" - } else { - set part(2) "19$part(2)" - } - } - {^[0-9]{8}$} { - set part(0) [string range $date 0 1] - set part(1) [string range $date 2 3] - set part(2) [string range $date 4 7] - } - {^[0-9]{2}.[0-9]{2}.[0-9]{4}$} { - set part(0) [string range $date 0 1] - set part(1) [string range $date 3 4] - set part(2) [string range $date 6 9] - } - default { - set part(0) 0 - set part(1) 0 - set part(2) 0 - } - } - } - - set separator $gpConfig(date:separator) - - if {[string equal $mode internal]} { - return "$part($gpConfig(date:month)) $part($gpConfig(date:day)) $part($gpConfig(date:year))" - } else { - return $part(0)$separator$part(1)$separator$part(2) - } -} - -#=======================================================================# -# PROC : ::gridplus::gpCalCheckDate # -# PURPOSE: Checks for valid date. # -#=======================================================================# - -proc ::gridplus::gpCalCheckDate {month day year} { - - set result 0 - - if {[scan $month %d] < 1 || [scan $month %d] > 12} { - return 0 - } else { - if {[scan $day %d] < 1 || [scan $day %d] > [::gridplus::gpCalMonthDays $month $year]} { - return 0 - } else { - return 1 - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpCalDayNames # -# PURPOSE: Returns day name header information. # -#=======================================================================# - -proc ::gridplus::gpCalDayNames {weekstart} { - variable gpConfig - - set basetime 1220223600 - set daynames {} - - for {set day [expr {$weekstart - 1}]} {$day < [expr {$weekstart + 6}]} {incr day} { - lappend daynames [string range [clock format [clock add $basetime $day day] -format %a -locale $gpConfig(locale)] 0 1] - } - - return $daynames -} - -#=======================================================================# -# PROC : ::gridplus::gpCalMonthDays # -# PURPOSE: Returns number of days for specified month/year. # -#=======================================================================# - -proc ::gridplus::gpCalMonthDays {month year} { - array set days { - 1 31 - 2 28 - 3 31 - 4 30 - 5 31 - 6 30 - 7 31 - 8 31 - 9 30 - 10 31 - 11 30 - 12 31 - } - - if {[clock format [clock add [clock scan 28/02/${year} -format "%d/%m/%Y"] 1 day] -format %d] eq "29"} { - set days(2) 29 - } - - return $days([scan $month "%d"]) -} - - -#=======================================================================# -# PROC : ::gridplus::gpDedent # -# PURPOSE: Returns "dedented" version of "value" string. # -#=======================================================================# - -proc ::gridplus::gpDedent {value} { - - set first 1 - - foreach line [split $value "\n"] { - set spaces {} - - if {[regexp -- {^ +} $line spaces]} { - if {$first} { - set indent [string length $spaces] - set first 0 - } elseif {[string length $spaces] < $indent} { - set indent [string length $spaces] - } - } - } - - regsub -lineanchor -all -- "^ {$indent}" $value {} result - - return $result -} - -#=======================================================================# -# PROC : ::gridplus::gpWindow # -# PURPOSE: Create toplevel window with "modal" option. # -#=======================================================================# - -proc ::gridplus::gpWindow {} { - upvar 1 options options - - variable gpInfo - - set options(-windowcommand) [::gridplus::gpOptionAlias -windowcommand -wcmd] - - if {[winfo exists $options(name)] && $options(-in) eq ""} { - if {! $gpInfo($options(name):toplevel)} { - return 0 - } - - if {$options(-windowcommand) ne ""} { - wm protocol $options(name) WM_DELETE_WINDOW "after 1 {$options(-windowcommand)}" - } - if {$options(-wtitle) ne ""} { - wm title [winfo toplevel $options(name)] [mc $options(-wtitle)] - } - return 0 - } - - regsub -- {%c} $options(-windowcommand) "::gridplus::gridplus clear $options(name)" - regsub -- {%d} $options(-windowcommand) "destroy $options(name)" - - set gpInfo($options(name):modal) 0 - - if {$options(-in) ne ""} { - if {[info exists gpInfo($options(-in):wcmd)]} { - eval $gpInfo($options(-in):wcmd) - } - - if {[winfo exists $options(-in).container]} { - destroy $options(-in).container - } - - frame $options(-in).container -container 1 - - set gpInfo($options(-in):container) [winfo id $options(-in).container] - - grid $options(-in).container -sticky $gpInfo($options(-in):sticky) - grid rowconfigure $options(-in) $options(-in).container -weight 1 - grid columnconfigure $options(-in) $options(-in).container -weight 1 - - toplevel $options(name) -use $gpInfo($options(-in):container) - - set gpInfo($options(name):toplevel) 0 - - if {$gpInfo([winfo toplevel $options(-in)]:modal)} { - set gpInfo($options(name):modal) 1 - } else { - set gpInfo($options(name):modal) 0 - } - - ::gridplus::gpEditMenuCreate $options(name) - - if {$options(-windowcommand) ne ""} { - set gpInfo($options(-in):wcmd) "$options(-windowcommand)" - } else { - set gpInfo($options(-in):wcmd) "::gridplus::gridplus clear $options(name);destroy $options(name)" - } - - set gpInfo($options(-in):in) $options(name) - - return 1 - } else { - set gpInfo($options(name):toplevel) 1 - - if {$options(-modal)} { - set gpInfo($options(name):modal) 1 - } - - toplevel $options(name) - wm overrideredirect $options(name) $options(-overrideredirect) - - bind $options(name) "::gridplus::gpWindowBindings $options(name) %W 1" - bind $options(name) "::gridplus::gpWindowBindings $options(name) %W 1" - bind $options(name) "::gridplus::gpWindowBindings $options(name) %W 1" - } - - wm attributes $options(name) -topmost $options(-topmost) - - bind $options(name) "::gridplus::gpWidgetHelpCancel;::gridplus::gpValidateErrorCancel $options(name) %W 1" - - ::gridplus::gpEditMenuCreate $options(name) - - wm resizable $options(name) 0 0 - - if {$options(-windowcommand) ne ""} { - wm protocol $options(name) WM_DELETE_WINDOW "after 1 {$options(-windowcommand)}" - } else { - wm protocol $options(name) WM_DELETE_WINDOW "after 1 {::gridplus::gridplus clear $options(name);destroy $options(name)}" - } - - if {$options(-wtitle) ne ""} { - wm title [winfo toplevel $options(name)] [mc $options(-wtitle)] - } - - if {$options(-modal)} { - bind modalWindow {wm deiconify %W;raise %W} - bindtags $options(name) [linsert [bindtags $options(name)] 0 modalWindow] - wm deiconify $options(name) - tkwait visibility $options(name) - grab set $options(name) - } - - return 1 -} - -#=======================================================================# -# PROC : ::gridplus::gpWindowBindings # -# PURPOSE: Process window bindings. # -#=======================================================================# - -proc ::gridplus::gpWindowBindings {testWindow eventWindow binding} { - - ::gridplus::gpWidgetHelpCancel - ::gridplus::gpValidateErrorCancel $testWindow $eventWindow $binding - ::gridplus::gpDateSelectorUnpost $testWindow -} - -#=======================================================================# -# PROC : ::gridplus::gpclear # -# PURPOSE: Clear selected text for item. # -#=======================================================================# - -proc ::gridplus::gpclear {{item {}}} { - - if {$item eq ""} { - set item [focus] - } - - if {[string match *.text $item] && [winfo class $item] eq "Text"} { - set textItem $item - } else { - set textItem $item.text - } - - if {[winfo exists $textItem]} { - event generate $textItem <> - $textItem edit modified 1 - } else { - event generate $item <> - } -} - -#=======================================================================# -# PROC : ::gridplus::gpcopy # -# PURPOSE: Perform clipboard copy for item. # -#=======================================================================# - -proc ::gridplus::gpcopy {{item {}}} { - - if {$item eq ""} { - set item [focus] - } - - if {[string match *.text $item] && [winfo class $item] eq "Text"} { - set textItem $item - } else { - set textItem $item.text - } - - if {[winfo exists $textItem]} { - tk_textCopy $textItem - } else { - clipboard clear - catch {clipboard append [selection get]} - } -} - -#=======================================================================# -# PROC : ::gridplus::gpcut # -# PURPOSE: Perform clipboard cut for item. # -#=======================================================================# - -proc ::gridplus::gpcut {{item {}}} { - - if {$item eq ""} { - set item [focus] - } - - if {[string match *.text $item] && [winfo class $item] eq "Text"} { - set textItem $item - } else { - set textItem $item.text - } - - if {[winfo exists $textItem]} { - tk_textCut $textItem - $textItem edit modified 1 - } else { - clipboard clear - catch {clipboard append [selection get]} - catch {$item delete sel.first sel.last} - } -} - -#=======================================================================# -# PROC : ::gridplus::gpdate # -# PURPOSE: Returns (calculated) date in "-dateformat". # -#=======================================================================# - -proc ::gridplus::gpdate {{action {@}} {date {}}} { - variable gpConfig - - # Run initialisation if neccessary. - if {! [info exists gpConfig]} { - gpInit - } - - set unitCode [string index $action 0] - set increment [string range $action 1 end] - - switch -- $gpConfig(dateformat) { - eu {set dateFormat "%d.%m.%Y"} - iso {set dateFormat "%Y-%m-%d"} - uk {set dateFormat "%d/%m/%Y"} - us {set dateFormat "%m/%d/%Y"} - } - - if {$date eq ""} { - set clockSeconds [clock seconds] - } else { - set clockSeconds [clock scan $date -format $dateFormat] - } - - switch -- $unitCode { - @ {return [clock format $clockSeconds -format $dateFormat]} - + {set unit "day"} - - {set unit "day";set increment "-$increment"} - > {set unit "month"} - < {set unit "month";set increment "-$increment"} - default {return $action} - } - - return [clock format [clock add $clockSeconds $increment $unit] -format $dateFormat] -} - -#=======================================================================# -# PROC : ::gridplus::gpdb # -# PURPOSE: TDBC interface. # -#=======================================================================# - -proc ::gridplus::gpdb {args} { - - foreach {option arg database window sql FOREACH code data} [lrepeat 8 {}] {} - - switch [llength $args] { - 3 { - foreach {database window sql} $args {} - } - 4 { - foreach {database window sql data} $args {} - } - 5 { - foreach {database window sql FOREACH code} $args {} - } - 6 { - foreach {database window sql FOREACH code data} $args {} - } - default { - error "GRIDPLUS ERROR: (gpdb) Invalid number of args." - } - } - - ::gridplus::gpdbRunSQL $database $window $sql "$code" $data -} - -#=======================================================================# -# PROC : ::gridplus::gpdbRunSQL # -# PURPOSE: Run SQL and set approprite result. # -#=======================================================================# - -proc ::gridplus::gpdbRunSQL {database window sql code data} { - global {} - - variable gpInfo - - set columnID 1 - set columnMap [dict create] - set dataType "map" - set format "dicts" - set prefix @ - set result {} - set rowCount 1 - set varCount 1 - - if {$code ne ""} { - set dataType "foreach" - set format "dicts" - if {[string match *@* $data]} { - set prefix $data - } - } elseif {$data ne ""} { - if {[string match .* $data]} { - set dataType "tablelist" - set format "lists" - } elseif {[string match *@* $data]} { - set dataType "gridplus" - set prefix $data - } elseif {$data eq "="} { - set dataType "list" - set format "lists" - } else { - set dataType "dict" - upvar #0 $data variable - } - } - - while {[regexp -- {@[(]([^( )@]+)[)]} $sql sqlItem itemID]} { - set columnName "gpdb____$columnID" - dict set columnMap $columnName $itemID - set sql [string map "$sqlItem {as $columnName}" $sql] - incr columnID - } - - while {[regexp -- {((%?):[(]([^( ):]+)(:([a-zA-Z0-9]+))?[)](%?))} $sql -> sqlItem wildcard1 itemID -> index wildcard2]} { - switch -glob -- $itemID { - ,* { - if {$window eq "."} { - set pattern "^\[.\]\[^,.\]+$itemID$" - } else { - set pattern "^\[.\][string range $window 1 end]\[.\]\[^,.\]+$itemID$" - } - set item [array names {} -regexp $pattern] - if {[llength $item] > 1} { - error "GRIDPLUS ERROR: (gpdb) Ambiguous item ($sqlItem)." - } - } - [.]* { - set item $itemID - } - *[@]* { - set item $itemID - } - default { - if {[string match *, $window]} { - set item $window$itemID - } else { - if {$window eq "."} { - set item .$itemID - } else { - set item $window.$itemID - } - } - } - } - - if {! [info exists ($item)]} { - error "GRIDPLUS ERROR: (gpdb) Item \"$item\" does not exist." - } - - if {$index eq ""} { - set gpdbSQLvar$varCount "$wildcard1$($item)$wildcard2" - } else { - set gpdbSQLvar$varCount "$wildcard1[lindex $($item) [::gridplus::gpTablelistColumnIndex $item $index gpdb]]$wildcard2" - } - - set sql [string map "$sqlItem :gpdbSQLvar$varCount" $sql] - - incr varCount - } - - set statement [$database prepare $sql] - - if {[catch { - $statement foreach -as $format -columnsvariable columns row { - switch -- $dataType { - foreach { - dict for {column value} $row { - gpset "$prefix$column" $value - } - eval "global {};$code" - } - tablelist { - lappend result $row - } - list { - lappend result $row - } - default { - if {$rowCount > 1} { - error "GRIDPLUS ERROR: (gpdb) More than 1 row returned for non-list result." - } else { - set result $row - } - } - } - incr rowCount - } - } sqlErrorText]} { - if {[=< sqlErrorProc] eq ""} { - error "GRIDPLUS ERROR: (gpdb) SQL error ($sqlErrorText)." - } else { - [=< sqlErrorProc] "$sqlErrorText" - } - } - - $statement close - - switch -- $dataType { - dict { - set variable [dict create {*}$result] - } - gridplus { - dict for {column value} $result { - gpset "$prefix$column" $value - } - } - map { - ::gridplus::gpdbMap $window $result $columnMap - } - tablelist { - gpset $data $result - } - list { - return $result - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpdbMap # -# PURPOSE: Map result from SQL to GRIDPLUS "variable(s)". # -#=======================================================================# - -proc ::gridplus::gpdbMap {window result columnMap} { - global {} - - dict for {column value} $result { - set mapWindow {} - set mapGrid {} - set mapItem {} - - if {[string match "gpdb____*" $column]} { - set item [dict get $columnMap $column] - } else { - regsub -all -- {[.:]} $column "_" column - set pattern $column - if {[string match *, $window]} { - set item $window$column - } else { - if {$window eq "."} { - set pattern "^\[.\]\[^,.\]+,$pattern$" - } else { - set pattern "^\[.\][string range $window 1 end]\[.\]\[^,.\]+,$pattern$" - } - set item [array names {} -regexp $pattern] - if {[llength $item] > 1} { - error "GRIDPLUS ERROR: (gpdb) Ambiguous item ($column)." - } - } - } - - if {[info exists ($item)]} { - gpset $item $value - } else { - gpset "@$column" $value - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpdefault # -# PURPOSE: Set default values for GRIDPLUS "variable(s)". # -#=======================================================================# - -proc ::gridplus::gpdefault {args} { - - variable gpInfo - - switch -- [llength $args] { - 1 { - if {[expr [llength [lindex $args 0]] % 2] != 0} { - error "GRIDPLUS ERROR: (gpdefault) Unmatched item/value." - } - foreach {item value} [lindex $args 0] { - set gpInfo(default:$item) $value - } - } - 2 { - set item [lindex $args 0] - set value [lindex $args 1] - set gpInfo(default:$item) $value - } - default { - error "GRIDPLUS ERROR: (gpdefault) Wrong number of Args." - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpdelete # -# PURPOSE: Deletes specified row/line/item from a tablelist/text/tree. # -#=======================================================================# - -proc ::gridplus::gpdelete {args} { - global {} - - variable gpInfo - - set focus 0 - set index 0 - set select 0 - - set column 0 - set match {} - - set count 1 - set current 0 - set option 1 - - set autoSelect [=< autoSelect 1] - - foreach arg $args { - switch -glob -- $arg { - | {set option 0} - -first {if {$option} {set index 1; set match 0}} - -focus {if {$option} {set select 1; set focus 1}} - -index {if {$option} {set index 1}} - -last {if {$option} {set index 1; set match "end"}} - -row {if {$option} {set index 1}} - -select {if {$option} {set select 1}} - -- {set option 0} - default { - if {$option && [string match -* $arg]} { - error "GRIDPLUS ERROR: (gpdelete) Invalid option ($arg)." - } - switch -- $count { - 1 {set name $arg; incr count} - 2 {set arg2 $arg; incr count} - 3 {set arg3 $arg; incr count} - } - } - } - } - - switch -- $count { - 2 {if {! $index} { - set current 1 - set index 1 - } - } - 3 {set match $arg2 - } - 4 {set column $arg2 - set match $arg3 - } - default { - error "GRIDPLUS ERROR: (gpdelete) Invalid number of Args." - } - } - - if {[winfo exists $name.tablelist]} { - if {$current && [$name.tablelist cget -selectmode] ni "browse single"} { - error "GRIDPLUS ERROR: (gpdelete) Current row delete only allowed when tablelist selectmode is \"browse\" or \"single\"." - } - - set currentSelection [$name.tablelist curselection] - - if {$currentSelection ne ""} { - if {$autoSelect} { - set select 1 - } - } - - if {$current} { - if {$currentSelection ne ""} { - set match $currentSelection - } else { - error "GRIDPLUS ERROR: (gpdelete) Tablelist does not have a selected row." - } - } - - $name.tablelist selection clear 0 end - set ($name) {} - - if {$index} { - set row $match - if {$row ne "end" && $row >= [$name.tablelist size]} { - set row "end" - } - } else { - set columnIndex [::gridplus::gpTablelistColumnIndex $name $column gpdelete] - set row [lsearch -exact [$name.tablelist getcolumn $columnIndex] $match] - if {$row == -1} { - error "GRIDPLUS ERROR: (gpdelete) Tablelist row with match \"$match\" not found." - } - } - - $name.tablelist delete $row - - if {$select} { - if {$focus} { - gpselect $name -focus -row $row - } else { - gpselect $name -row $row - } - } - } elseif {[winfo exists $name.text]} { - if {$match eq ""} { - error "GRIDPLUS ERROR: (gpdelete) Text line not specified." - } - - if {$match eq "first"} { - set match 1 - } - - if {$match in "end last"} { - $name.text delete "end - 1 line" "end" - } else { - $name.text delete $match.0 $match.end - $name.text delete $match.end - } - } elseif {[winfo exists $name.tree]} { - if {$current && [$name.tree cget -selectmode] ne "browse"} { - error "GRIDPLUS ERROR: (gpdelete) Current node delete only allowed when tree selectmode is \"browse\"." - } - - set currentSelection [$name.tree selection] - - if {$currentSelection ne ""} { - if {$autoSelect} { - set select 1 - } - } - - if {$current} { - if {$currentSelection ne ""} { - set match $currentSelection - } else { - error "GRIDPLUS ERROR: (gpdelete) Tree does not have a selected node." - } - } - - if {$select} { - set selectNode [$name.tree identify item 1 [expr {[lindex [$name.tree bbox $($name)] 1] + [lindex [$name.tree bbox $($name)] 3] + 1}]] - if {$selectNode eq ""} { - set selectNode [$name.tree identify item 1 [expr {[lindex [$name.tree bbox $($name)] 1] - 1}]] - } - } - - $name.tree selection remove $($name) - set ($name) {} - $name.tree delete $match - - if {$select && $selectNode ne ""} { - if {$focus} { - gpselect $name -focus $selectNode - } else { - gpselect $name $selectNode - } - } - } else { - error "GRIDPLUS ERROR: (gpdelete) Widget \"$name\" is not tablelist, text or tree." - } -} - -#=======================================================================# -# PROC : ::gridplus::gpfind # -# PURPOSE: Find next/previous occurance of string in GRIDPLUS Text. # -#=======================================================================# - -proc ::gridplus::gpfind {item pattern {direction forwards}} { - global {} - - if {$direction eq "forwards"} { - set searchIndex "insert+1char" - } else { - set searchIndex "insert" - } - - set position [$item.text search -$direction -exact -nocase -- $pattern $searchIndex] - - if {$position ne ""} { - catch "$item.text tag remove sel sel.first sel.last" - $item.text tag add sel $position $position+[string length $pattern]chars - $item.text configure -inactiveselectbackground [$item.text cget -selectbackground] - $item.text mark set insert $position - $item.text see $position - } -} - -#=======================================================================# -# PROC : ::gridplus::gpfind_dialog # -# PURPOSE: Display find dialog for specified GRIDPLUS text item. # -#=======================================================================# - -proc ::gridplus::gpfind_dialog {item} { - - ::gridplus::gpTextFind $item -} - -#=======================================================================# -# PROC : ::gridplus::gpget # -# PURPOSE: Returns tablelist column data for "columns". # -#=======================================================================# - -proc ::gridplus::gpget {item columns} { - global {} - - set result {} - - if {[string match ?*>*? $columns]} { - foreach {first last} [split $columns >] {} - set firstIndex [::gridplus::gpTablelistColumnIndex $item $first "gpget"] - set lastIndex [::gridplus::gpTablelistColumnIndex $item $last "gpget"] - set columns {} - for {set index $firstIndex} {$index <= $lastIndex} {incr index} { - lappend columns $index - } - set columns [string map {{ } ,} $columns] - } - - foreach column [split $columns ,+] { - if {$column ne ""} { - lappend result [lindex $($item) [::gridplus::gpTablelistColumnIndex $item $column "gpget"]] - } - } - - if {([string match *+* $columns] || [llength $result] == 1) && ! [string match *,* $columns]} { - set result [concat {*}$result] - } - - return $result -} - -#=======================================================================# -# PROC : ::gridplus::gpinsert # -# PURPOSE: Inserts line into tablelist/text. # -#=======================================================================# - -proc ::gridplus::gpinsert {name position line} { - global {} - - variable gpInfo - - if {[winfo exists $name.tablelist]} { - ::gridplus::gpTablelistInsert $name $position $line - } elseif {[winfo exists $name.text]} { - ::gridplus::gpTextInsert $name $position $line - } else { - error "GRIDPLUS ERROR: (gpinsert) Widget \"$name\" is not tablelist or text." - } -} - -#=======================================================================# -# PROC : ::gridplus::gpmap # -# PURPOSE: Map GRIDPLUS "variable(s)" to a list of values, array or dict# -#=======================================================================# - -proc ::gridplus::gpmap {map values {arg __direct}} { - - if {$arg ni "__direct __left __right"} { - upvar #0 $arg variable - - set position 0 - - if {[array exists variable]} { - foreach item $map { - if {[winfo exists $item] && [winfo class $item] eq "TCombobox"} { - gpset [list $item $variable([lindex $values $position])] - } else { - gpset $item $variable([lindex $values $position]) - } - incr position - } - } elseif {! [catch {dict size $variable}]} { - foreach item $map { - if {[winfo exists $item] && [winfo class $item] eq "TCombobox"} { - gpset [list $item [dict get $variable [lindex $values $position]]] - } else { - gpset $item [dict get $variable [lindex $values $position]] - } - incr position - } - } else { - error "GRIDPLUS ERROR: (gpmap) Array/Dict \"$arg\" does not exist." - } - } else { - switch -- $arg { - __direct {set start 0; set increment 1} - __left {set start 0; set increment 2} - __right {set start 1; set increment 2} - default {set start 0; set increment 1} - } - - set position $start - - foreach item $map { - if {[winfo exists $item] && [winfo class $item] eq "TCombobox"} { - gpset [list $item [lindex $values $position]] - } else { - gpset $item [lindex $values $position] - } - incr position $increment - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpnav # -# PURPOSE: Navigate to text label or calendar month. # -#=======================================================================# - -proc ::gridplus::gpnav {name target {increment {}}} { - - global {} - - if {[winfo exists $name.text]} { - $name.text yview $target - set ($name) $target - } elseif {[winfo exists $name.calendar]} { - if {$target in "current month year"} { - ::gridplus::gpCalendarNav $name $target $increment - } else { - if {[llength $target] == 1} { - foreach {month day year} [::gridplus::gpFormatDate $target internal] {} - if {! [::gridplus::gpCalCheckDate $month $day $year]} { - error "GRIDPLUS ERROR: (gpnav) \"$target\" is not a valid date." - } - } elseif {[llength $target] == 2} { - set day {} - set month [lindex $target 0] - set year [lindex $target 1] - if {[scan $month %d] < 1 || [scan $month %d] > 12} { - error "GRIDPLUS ERROR: (gpnav) \"$month\" is not a valid month." - } - if {! [string is integer $year]} { - error "GRIDPLUS ERROR: (gpnav) \"$year\" is not a valid year." - } - } - ::gridplus::gpCalendarDisplay $name $day $month $year - if {$increment ne ""} { - ::gridplus::gpCalendarNav $name month $increment - } - } - } else { - error "GRIDPLUS ERROR: (gpnav) Widget \"$name\" is not text or calendar." - } -} - -#=======================================================================# -# PROC : ::gridplus::gpoptions # -# PURPOSE: Set GRIDPLUS option database options. # -#=======================================================================# - -proc ::gridplus::gpoptions {args} { - - variable gpInfo - - switch -- [llength $args] { - 1 { - if {[expr [llength [lindex $args 0]] % 2] != 0} { - error "GRIDPLUS ERROR: (gpoption) Unmatched option/value." - } - foreach {option value} [lindex $args 0] { - option add *Gridplus.$option $value - } - } - 2 { - foreach {option value} $args {} - option add *Gridplus.$option $value - } - default { - error "GRIDPLUS ERROR: (gpoption) Wrong number of Args." - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gppaste # -# PURPOSE: Perform clipboard paste for item. # -#=======================================================================# - -proc ::gridplus::gppaste {{item {}}} { - - if {$item eq ""} { - set item [focus] - } - - if {[string match *.text $item] && [winfo class $item] eq "Text"} { - set textItem $item - } else { - set textItem $item.text - } - - if {[winfo exists $textItem]} { - tk_textPaste $textItem - $textItem edit modified 1 - } else { - if {! [catch {$item selection clear}]} { - $item insert insert [clipboard get] - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpselect # -# PURPOSE: Selects specified item in a tablelist/tree/calendar. # -#=======================================================================# - -proc ::gridplus::gpselect {args} { - global {} - - variable gpInfo - - set column 0 - set focus {} - set index 0 - set nocase {} - set restore 0 - set save 0 - set selectonly 0 - - set columnMatch 0 - set match {} - set sortOrder {} - - set count 1 - set option 1 - - foreach arg $args { - switch -glob -- $arg { - | {set columnMatch 1; set option 0} - -first {if {$option} {set index 1; set match 0}} - -focus {if {$option} {set focus "-focus"}} - -index {if {$option} {set index 1}} - -last {if {$option} {set index 1; set match "end"}} - -max {if {$option} {set sortOrder "decreasing"}} - -min {if {$option} {set sortOrder "increasing"}} - -restore {if {$option} {set restore 1}} - -row {if {$option} {set index 1}} - -save {if {$option} {set save 1}} - -selectonly {if {$option} {set selectonly 1}} - -- {set option 0} - default { - if {$option && [string match -* $arg]} { - error "GRIDPLUS ERROR: (gpselect) Invalid option ($arg)." - } - switch -- $count { - 1 {set name $arg; incr count} - 2 {set arg2 $arg; incr count} - 3 {set arg3 $arg; incr count} - } - } - } - } - - switch -- $count { - 3 {set match $arg2} - 4 {if {$columnMatch} { - set column $arg2 - set match $arg3 - } else { - set match $arg2 - set column $arg3} - } - } - - if {[winfo exists $name.tablelist]} { - if {$save} { - if {[$name.tablelist cget -selectmode] ni "browse single"} { - error "GRIDPLUS ERROR: (gpselect) Current selection save only allowed when tablelist selectmode is \"browse\" or \"single\"." - } - if {$match eq ""} { - set gpInfo($name:savedSelection) [$name.tablelist curselection] - } else { - set columnIndex [::gridplus::gpTablelistColumnIndex $name $match gpselect] - set gpInfo($name:savedSelection) [list [lindex [$name.tablelist get [$name.tablelist curselection]] $columnIndex] $columnIndex] - } - return - } - - if {$restore} { - if {[info exists gpInfo($name:savedSelection)]} { - if {[llength $gpInfo($name:savedSelection)] == 1} { - gpselect {*}$focus -index $name $gpInfo($name:savedSelection) - } else { - gpselect {*}$focus $name [lindex $gpInfo($name:savedSelection) 0] [lindex $gpInfo($name:savedSelection) 1] - } - } else { - error "GRIDPLUS ERROR: (gpselect) No selection saved for \"$name\"." - } - return - } - - if {$sortOrder ne ""} { - set columnIndex [::gridplus::gpTablelistColumnIndex $name $match gpselect] - set sortMode [$name.tablelist columncget $columnIndex -sortmode] - if {$sortMode eq "asciinocase"} { - set sortMode "ascii" - set nocase "-nocase" - } - set selectMatch [lindex [lsort {*}$nocase -$sortMode -$sortOrder -index $columnIndex [set [$name.tablelist itemlistvar]]] "0 $columnIndex"] - gpselect {*}$focus $name -- $selectMatch $columnIndex - return - } - - $name.tablelist selection clear 0 end - - if {$index} { - set row $match - if {$row ne "end" && $row >= [$name.tablelist size]} { - set row "end" - } - } else { - set columnIndex [::gridplus::gpTablelistColumnIndex $name $column gpselect] - set row [lsearch -exact [$name.tablelist getcolumn $columnIndex] $match] - if {$row == -1} { - error "GRIDPLUS ERROR: (gpselect) Tablelist line with match \"$match\" not found." - } - } - - $name.tablelist selection set $row - $name.tablelist activate $row - $name.tablelist see $row - - if {$gpInfo($name:action) eq "single"} { - ::gridplus::gpTablelistSelect $name $row $gpInfo($name:window) $gpInfo($name:validate) $gpInfo($name:command) - } else { - ::gridplus::gpTablelistSelect $name $row $gpInfo($name:window) $gpInfo($name:validate) {} - } - - if {$focus eq "-focus"} { - after idle focus [$name.tablelist bodypath] - $name.tablelist see $row - } - } elseif {[winfo exists $name.tree]} { - if {! [catch {$name.tree selection set $match}]} { - if {$gpInfo($name:action) eq "single"} { - ::gridplus::gpTreeSelect $name $gpInfo($name:window) $gpInfo($name:validate) $gpInfo($name:command) - } else { - ::gridplus::gpTreeSelect $name $gpInfo($name:window) $gpInfo($name:validate) {} - } - - if {$focus eq "-focus"} { - after idle focus $name.tree - $name.tree see $match - } - } else { - error "GRIDPLUS ERROR: (gpselect) Tree node \"$match\" not found." - } - } elseif {[winfo exists $name.calendar]} { - if {$match ne ""} { - foreach {month day year} [::gridplus::gpFormatDate $match internal] {} - if {! [::gridplus::gpCalCheckDate $month $day $year]} { - error "GRIDPLUS ERROR: (gpselect) \"$match\" is not a valid date." - } - set gpInfo($name:selecttoday) 1 - ::gridplus::gpCalendarDisplay $name $day $month $year - } else { - if {$gpInfo($name:variable) ne ""} { - set variable $gpInfo($name:variable) - } else { - set variable $name - } - - if {! $selectonly} { - set ($variable) {} - } - - if {[info exists gpInfo($name:selected)] && $gpInfo($name:selected) ne ""} { - $gpInfo($name:selected) configure -bg $gpInfo($name:bg) -fg $gpInfo($name:fg) - unset gpInfo($name:selected) - unset gpInfo($name:selectedday) - unset gpInfo($name:selectedmonth) - unset gpInfo($name:selectedyear) - } - } - } else { - error "GRIDPLUS ERROR: (gpselect) Widget \"$name\" is not calendar, tablelist or tree." - } -} - -#=======================================================================# -# PROC : ::gridplus::gpset # -# PURPOSE: Set GRIDPLUS "variable(s)". # -#=======================================================================# - -proc ::gridplus::gpset {args} { - global {} - - variable gpInfo - - update idletasks - - switch -- [llength $args] { - 1 { - if {[expr [llength [lindex $args 0]] % 2] != 0} { - error "GRIDPLUS ERROR: (gpset) Unmatched item/value." - } - foreach {item value} [lindex $args 0] { - if {[winfo exists $item.text]} { - $item.text delete 1.0 end - $item.text insert end $value - set ($item) $value - } elseif {[winfo exists $item.calendar]} { - ::gridplus::gpset -calendar $item $value - } else { - set ($item) $value - } - } - } - 2 { - set item [lindex $args 0] - set value [lindex $args 1] - if {[winfo exists $item.tablelist]} { - unset -nocomplain ($item) - $item.tablelist delete 0 end - foreach line $value { - ::gridplus::gpTablelistInsert $item end $line 1 - } - if {$gpInfo($item:columnsort)} { - if {$gpInfo($item:maintainsort) && [info exists gpInfo($item:lastsortcolumn)]} { - $item.tablelist sortbycolumn $gpInfo($item:lastsortcolumn) -$gpInfo($item:lastsortorder) - } else { - $item.tablelist sortbycolumn $gpInfo($item:firstcolumn) -$gpInfo($item:sortorder) - } - } - if {$gpInfo($item:selectfirst) && ! [info exists gpInfo($item:savedSelection)]} { - $item.tablelist selection set 0 - $item.tablelist activate 0 - set ($item) [$item.tablelist get 0] - } - } elseif {[winfo exists $item.text]} { - set textState [$item.text cget -state] - $item.text configure -state normal - if {$gpInfo($item:tags)} { - $item.text delete 1.0 end - ::gridplus::gpParseTags $item $value end - $item.text tag raise sel - } else { - $item.text delete 1.0 end - $item.text insert end $value - $item.text edit modified 0 - set ($item) $value - } - $item.text configure -state $textState - } elseif {[winfo exists $item.tree]} { - ::gridplus::gpTreeSet $item $value - } elseif {[winfo exists $item.calendar]} { - ::gridplus::gpselect $item $value - } elseif {[winfo exists $item] && [winfo class $item] eq "TCombobox" && ! [info exists gpInfo($item:datecommand)]} { - $item configure -value $value - } else { - set ($item) $value - } - } - 3 { - set option [lindex $args 0] - set item [lindex $args 1] - set value [lindex $args 2] - switch -- $option { - -| {::gridplus::gpset $item [::gridplus::gpDedent $value]} - -names {::gridplus::gpTablelistSetColumns $item -name $value} - -titles {::gridplus::gpTablelistSetColumns $item -title $value} - default {::gridplus::gpselect $item $value $option} - } - } - default { - error "GRIDPLUS ERROR: (gpset) Wrong number of Args." - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpunset # -# PURPOSE: Unset GRIDPLUS "variable(s)". # -#=======================================================================# - -proc ::gridplus::gpunset {args} { - global {} - - foreach pattern $args { - foreach item [array names {} $pattern] { - if {[info exists ($item)]} { - unset ($item) - } - if {[winfo exists $item.tablelist]} { - $item.tablelist delete 0 end - } elseif {[winfo exists $item.text]} { - $item.text delete 1.0 end - } elseif {[winfo exists $item.tree]} { - $item.tree configure -state normal - $item.tree delete 1.0 end - $item.tree configure -state disabled - } - } - } -} - -#=======================================================================# -# PROC : ::gridplus::gpupdate # -# PURPOSE: Updates specified row in a tablelist. # -#=======================================================================# - -proc ::gridplus::gpupdate {args} { - global {} - - variable gpInfo - - set focus 0 - set index 0 - set select 0 - - set column 0 - set current 0 - set match {} - set target {} - set value {} - - set count 1 - set option 1 - - foreach arg $args { - switch -glob -- $arg { - | {set option 0} - -focus {if {$option} {set focus 1}} - -index {if {$option} {set index 1}} - -row {if {$option} {set index 1}} - -select {if {$option} {set select 1}} - -- {set option 0} - default { - if {$option && [string match -* $arg]} { - error "GRIDPLUS ERROR: (gpupdate) Invalid option ($arg)." - } - switch -- $count { - 1 {set name $arg; incr count} - 2 {set arg2 $arg; incr count} - 3 {set arg3 $arg; incr count} - 4 {set arg4 $arg; incr count} - 5 {set arg5 $arg; incr count} - } - } - } - } - - set currentSelection [$name.tablelist curselection] - - switch -- $count { - 3 {set index 1 - set current 1 - set match $currentSelection - set value $arg2 - # gpupdate .mytable {row data} - } - 4 {if {$index} { - set match $arg2 - set value $arg3 - # gpupdate .mytable -row 99 {row data} - } else { - set index 1 - set current 1 - set match $currentSelection - set target $arg2 - set value $arg3 - # gpupdate .mytable | mytarget "value" - } - } - 5 {if {$index} { - set match $arg2 - set target $arg3 - set value $arg4 - # gpupdate .mytable -row 99 | mytarget "value" - } else { - set column $arg2 - set match $arg3 - set value $arg4 - # gpupdate .mytable | mycolumn "my match" | {row data} - } - } - 6 {set column $arg2 - set match $arg3 - set target $arg4 - set value $arg5 - # gpupdate .mytable | mycolumn "my match" | mytarget "value" - } - default { - error "GRIDPLUS ERROR: (gpupdate) Invalid number of Args." - } - } - - if {[winfo exists $name.tablelist]} { - if {$current && [$name.tablelist cget -selectmode] ni "browse single"} { - error "GRIDPLUS ERROR: (gpupdate) Current record update only allowed when tablelist selectmode is \"browse\" or \"single\"." - } - $name.tablelist selection clear 0 end - if {$index} { - set row $match - if {$row ne "end" && $row >= [$name.tablelist size]} { - set row "end" - } - } else { - set columnIndex [::gridplus::gpTablelistColumnIndex $name $column gpupdate] - set row [lsearch -exact [$name.tablelist getcolumn $columnIndex] $match] - if {$row == -1} { - error "GRIDPLUS ERROR: (gpupdate) Tablelist row with match \"$match\" not found." - } - } - - if {$target ne ""} { - set targetIndex [::gridplus::gpTablelistColumnIndex $name $target gpupdate] - set value [lreplace [$name.tablelist get $row] $targetIndex $targetIndex $value] - } - - ::gridplus::gpTablelistInsert $name $row $value 0 1 - - if {$select} { - gpselect $name -row $row - } elseif {$currentSelection ne ""} { - gpselect $name -row $currentSelection - } - if {$focus} { - after idle focus [$name.tablelist bodypath] - $name.tablelist see $row - } - } else { - error "GRIDPLUS ERROR: (gpupdate) Widget \"$name\" is not tablelist." - } -} - -#=======================================================================# -# PROC : ::gridplus::= # -# PURPOSE: Return specified (widget) option. # -#=======================================================================# - -proc ::gridplus::= {value key {default {}}} { - - if {[dict exists $value $key]} { - return [dict get $value $key] - } else { - return $default - } -} - -#=======================================================================# -# PROC : ::gridplus::=% # -# PURPOSE: Return state for widget group. # -#=======================================================================# - -proc ::gridplus::=% {name state {flag {}}} { - - variable gpInfo - - if {[info exists gpInfo($name:group)] && [info exists gpInfo($gpInfo($name:group))]} { - - set state $gpInfo($gpInfo($name:group)) - - if {$flag eq "!" && $state eq "disabled"} { - return "readonly" - } else { - return $state - } - } - - return $state -} - -#=======================================================================# -# PROC : ::gridplus::=: # -# PURPOSE: Create icon and return image name. # -#=======================================================================# - -proc ::gridplus::=: {icon} { - upvar 1 options options - - if {[lsearch [image names] ::icon::$icon] < 0} { - return "::icon::[::icons::icons create -file [file join $options(-iconpath) $options(-iconfile)] $icon]" - } else { - return "::icon::$icon" - } -} - -#=======================================================================# -# PROC : ::gridplus::=? # -# PURPOSE: Check if widget option has been set. # -#=======================================================================# - -proc ::gridplus::=? {value key} { - return [dict exists $value $key] -} - -#=======================================================================# -# PROC : ::gridplus::=@ # -# PURPOSE: Return default for widget. # -#=======================================================================# - -proc ::gridplus::=@ {name {default {}}} { - - variable gpInfo - - if {[info exists gpInfo(default:$name)]} { - return $gpInfo(default:$name) - } else { - return $default - } -} - -#=======================================================================# -# PROC : ::gridplus::=< # -# PURPOSE: Return specified widget option -or- default. # -#=======================================================================# - -proc ::gridplus::=< {option {default {}}} { - - set value [option get . "Gridplus.$option" -] - - if {$value eq ""} { - return $default - } else { - return $value - } -} - -#=======================================================================# -# End of Script: gridplus.tcl # -#=======================================================================# diff --git a/src/punk86.vfs/lib/gridplus2.11/pkgIndex.tcl b/src/punk86.vfs/lib/gridplus2.11/pkgIndex.tcl deleted file mode 100644 index d2e74a2d..00000000 --- a/src/punk86.vfs/lib/gridplus2.11/pkgIndex.tcl +++ /dev/null @@ -1 +0,0 @@ -package ifneeded gridplus 2.11 [list source [file join $dir gridplus.tcl]] \ No newline at end of file diff --git a/src/runtime/mapvfs.config b/src/runtime/mapvfs.config index f21d57e2..1aab40a1 100644 --- a/src/runtime/mapvfs.config +++ b/src/runtime/mapvfs.config @@ -4,7 +4,8 @@ #e.g #- myproject.vfs #- punk86.vfs -tclkit86bi.exe punk86.vfs +#tclkit86bi.exe punk86.vfs +tclkit8613.exe punk86.vfs #tclkit87a5bawt.exe punk86.vfs #tclkit86bi.exe vfs_windows/punk86win.vfs diff --git a/src/vendormodules/Thread-2.8.9.tm b/src/vendormodules/Thread-2.8.9.tm new file mode 100644 index 0000000000000000000000000000000000000000..273d8c5ae2cca8782ad74c7a18e6cd66a21815c2 GIT binary patch literal 7203 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 literal 0 HcmV?d00001 diff --git a/src/vendormodules/Thread/platform/win32_x86_64-2.8.9.tm b/src/vendormodules/Thread/platform/win32_x86_64-2.8.9.tm new file mode 100644 index 0000000000000000000000000000000000000000..798ef6240e996681947b99359b798556de44a54f GIT binary patch literal 77919 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 literal 0 HcmV?d00001 diff --git a/src/vendormodules/dictutils-0.2.1.tm b/src/vendormodules/dictutils-0.2.1.tm index cd6b4e58..12ca495b 100644 --- a/src/vendormodules/dictutils-0.2.1.tm +++ b/src/vendormodules/dictutils-0.2.1.tm @@ -1,145 +1,145 @@ -# dictutils.tcl -- - # - # Various dictionary utilities. - # - # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). - # - # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). - # - - #2023 0.2.1 - changed "package require Tcl 8.6" to "package require Tcl 8.6-" - - package require Tcl 8.6- - package provide dictutils 0.2.1 - - namespace eval dictutils { - namespace export equal apply capture witharray nlappend - namespace ensemble create - - # dictutils witharray dictVar arrayVar script -- - # - # Unpacks the elements of the dictionary in dictVar into the array - # variable arrayVar and then evaluates the script. If the script - # completes with an ok, return or continue status, then the result is copied - # back into the dictionary variable, otherwise it is discarded. A - # [break] can be used to explicitly abort the transaction. - # - proc witharray {dictVar arrayVar script} { - upvar 1 $dictVar dict $arrayVar array - array set array $dict - try { uplevel 1 $script - } on break {} { # Discard the result - } on continue result - on ok result { - set dict [array get array] ;# commit changes - return $result - } on return {result opts} { - set dict [array get array] ;# commit changes - dict incr opts -level ;# remove this proc from level - return -options $opts $result - } - # All other cases will discard the changes and propagage - } - - # dictutils equal equalp d1 d2 -- - # - # Compare two dictionaries for equality. Two dictionaries are equal - # if they (a) have the same keys, (b) the corresponding values for - # each key in the two dictionaries are equal when compared using the - # equality predicate, equalp (passed as an argument). The equality - # predicate is invoked with the key and the two values from each - # dictionary as arguments. - # - proc equal {equalp d1 d2} { - if {[dict size $d1] != [dict size $d2]} { return 0 } - dict for {k v} $d1 { - if {![dict exists $d2 $k]} { return 0 } - if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } - } - return 1 - } - - # apply dictVar lambdaExpr ?arg1 arg2 ...? -- - # - # A combination of *dict with* and *apply*, this procedure creates a - # new procedure scope populated with the values in the dictionary - # variable. It then applies the lambdaTerm (anonymous procedure) in - # this new scope. If the procedure completes normally, then any - # changes made to variables in the dictionary are reflected back to - # the dictionary variable, otherwise they are ignored. This provides - # a transaction-style semantics whereby atomic updates to a - # dictionary can be performed. This procedure can also be useful for - # implementing a variety of control constructs, such as mutable - # closures. - # - proc apply {dictVar lambdaExpr args} { - upvar 1 $dictVar dict - set env $dict ;# copy - lassign $lambdaExpr params body ns - if {$ns eq ""} { set ns "::" } - set body [format { - upvar 1 env __env__ - dict with __env__ %s - } [list $body]] - set lambdaExpr [list $params $body $ns] - set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] - if {$rc == 0} { - # Copy back any updates - set dict $env - } - return -options $opts $ret - } - - # capture ?level? ?exclude? ?include? -- - # - # Captures a snapshot of the current (scalar) variable bindings at - # $level on the stack into a dictionary environment. This dictionary - # can later be used with *dictutils apply* to partially restore the - # scope, creating a first approximation of closures. The *level* - # argument should be of the forms accepted by *uplevel* and - # designates which level to capture. It defaults to 1 as in uplevel. - # The *exclude* argument specifies an optional list of literal - # variable names to avoid when performing the capture. No variables - # matching any item in this list will be captured. The *include* - # argument can be used to specify a list of glob patterns of - # variables to capture. Only variables matching one of these - # patterns are captured. The default is a single pattern "*", for - # capturing all visible variables (as determined by *info vars*). - # - proc capture {{level 1} {exclude {}} {include {*}}} { - if {[string is integer $level]} { incr level } - set env [dict create] - foreach pattern $include { - foreach name [uplevel $level [list info vars $pattern]] { - if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } - upvar $level $name value - catch { dict set env $name $value } ;# no arrays - } - } - return $env - } - - # nlappend dictVar keyList ?value ...? - # - # Append zero or more elements to the list value stored in the given - # dictionary at the path of keys specified in $keyList. If $keyList - # specifies a non-existent path of keys, nlappend will behave as if - # the path mapped to an empty list. - # - proc nlappend {dictvar keylist args} { - upvar 1 $dictvar dict - if {[info exists dict] && [dict exists $dict {*}$keylist]} { - set list [dict get $dict {*}$keylist] - } - lappend list {*}$args - dict set dict {*}$keylist $list - } - - # invoke cmd args... -- - # - # Helper procedure to invoke a callback command with arguments at - # the global scope. The helper ensures that proper quotation is - # used. The command is expected to be a list, e.g. {string equal}. - # - proc invoke {cmd args} { uplevel #0 $cmd $args } - - } +# dictutils.tcl -- + # + # Various dictionary utilities. + # + # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). + # + # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). + # + + #2023 0.2.1 - changed "package require Tcl 8.6" to "package require Tcl 8.6-" + + package require Tcl 8.6- + package provide dictutils 0.2.1 + + namespace eval dictutils { + namespace export equal apply capture witharray nlappend + namespace ensemble create + + # dictutils witharray dictVar arrayVar script -- + # + # Unpacks the elements of the dictionary in dictVar into the array + # variable arrayVar and then evaluates the script. If the script + # completes with an ok, return or continue status, then the result is copied + # back into the dictionary variable, otherwise it is discarded. A + # [break] can be used to explicitly abort the transaction. + # + proc witharray {dictVar arrayVar script} { + upvar 1 $dictVar dict $arrayVar array + array set array $dict + try { uplevel 1 $script + } on break {} { # Discard the result + } on continue result - on ok result { + set dict [array get array] ;# commit changes + return $result + } on return {result opts} { + set dict [array get array] ;# commit changes + dict incr opts -level ;# remove this proc from level + return -options $opts $result + } + # All other cases will discard the changes and propagage + } + + # dictutils equal equalp d1 d2 -- + # + # Compare two dictionaries for equality. Two dictionaries are equal + # if they (a) have the same keys, (b) the corresponding values for + # each key in the two dictionaries are equal when compared using the + # equality predicate, equalp (passed as an argument). The equality + # predicate is invoked with the key and the two values from each + # dictionary as arguments. + # + proc equal {equalp d1 d2} { + if {[dict size $d1] != [dict size $d2]} { return 0 } + dict for {k v} $d1 { + if {![dict exists $d2 $k]} { return 0 } + if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } + } + return 1 + } + + # apply dictVar lambdaExpr ?arg1 arg2 ...? -- + # + # A combination of *dict with* and *apply*, this procedure creates a + # new procedure scope populated with the values in the dictionary + # variable. It then applies the lambdaTerm (anonymous procedure) in + # this new scope. If the procedure completes normally, then any + # changes made to variables in the dictionary are reflected back to + # the dictionary variable, otherwise they are ignored. This provides + # a transaction-style semantics whereby atomic updates to a + # dictionary can be performed. This procedure can also be useful for + # implementing a variety of control constructs, such as mutable + # closures. + # + proc apply {dictVar lambdaExpr args} { + upvar 1 $dictVar dict + set env $dict ;# copy + lassign $lambdaExpr params body ns + if {$ns eq ""} { set ns "::" } + set body [format { + upvar 1 env __env__ + dict with __env__ %s + } [list $body]] + set lambdaExpr [list $params $body $ns] + set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] + if {$rc == 0} { + # Copy back any updates + set dict $env + } + return -options $opts $ret + } + + # capture ?level? ?exclude? ?include? -- + # + # Captures a snapshot of the current (scalar) variable bindings at + # $level on the stack into a dictionary environment. This dictionary + # can later be used with *dictutils apply* to partially restore the + # scope, creating a first approximation of closures. The *level* + # argument should be of the forms accepted by *uplevel* and + # designates which level to capture. It defaults to 1 as in uplevel. + # The *exclude* argument specifies an optional list of literal + # variable names to avoid when performing the capture. No variables + # matching any item in this list will be captured. The *include* + # argument can be used to specify a list of glob patterns of + # variables to capture. Only variables matching one of these + # patterns are captured. The default is a single pattern "*", for + # capturing all visible variables (as determined by *info vars*). + # + proc capture {{level 1} {exclude {}} {include {*}}} { + if {[string is integer $level]} { incr level } + set env [dict create] + foreach pattern $include { + foreach name [uplevel $level [list info vars $pattern]] { + if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } + upvar $level $name value + catch { dict set env $name $value } ;# no arrays + } + } + return $env + } + + # nlappend dictVar keyList ?value ...? + # + # Append zero or more elements to the list value stored in the given + # dictionary at the path of keys specified in $keyList. If $keyList + # specifies a non-existent path of keys, nlappend will behave as if + # the path mapped to an empty list. + # + proc nlappend {dictvar keylist args} { + upvar 1 $dictvar dict + if {[info exists dict] && [dict exists $dict {*}$keylist]} { + set list [dict get $dict {*}$keylist] + } + lappend list {*}$args + dict set dict {*}$keylist $list + } + + # invoke cmd args... -- + # + # Helper procedure to invoke a callback command with arguments at + # the global scope. The helper ensures that proper quotation is + # used. The command is expected to be a list, e.g. {string equal}. + # + proc invoke {cmd args} { uplevel #0 $cmd $args } + + } diff --git a/src/vendormodules/dictutils-0.2.tm b/src/vendormodules/dictutils-0.2.tm deleted file mode 100644 index 154042e0..00000000 --- a/src/vendormodules/dictutils-0.2.tm +++ /dev/null @@ -1,143 +0,0 @@ -# dictutils.tcl -- - # - # Various dictionary utilities. - # - # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk). - # - # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style). - # - - package require Tcl 8.6- - package provide dictutils 0.2 - - namespace eval dictutils { - namespace export equal apply capture witharray nlappend - namespace ensemble create - - # dictutils witharray dictVar arrayVar script -- - # - # Unpacks the elements of the dictionary in dictVar into the array - # variable arrayVar and then evaluates the script. If the script - # completes with an ok, return or continue status, then the result is copied - # back into the dictionary variable, otherwise it is discarded. A - # [break] can be used to explicitly abort the transaction. - # - proc witharray {dictVar arrayVar script} { - upvar 1 $dictVar dict $arrayVar array - array set array $dict - try { uplevel 1 $script - } on break {} { # Discard the result - } on continue result - on ok result { - set dict [array get array] ;# commit changes - return $result - } on return {result opts} { - set dict [array get array] ;# commit changes - dict incr opts -level ;# remove this proc from level - return -options $opts $result - } - # All other cases will discard the changes and propagage - } - - # dictutils equal equalp d1 d2 -- - # - # Compare two dictionaries for equality. Two dictionaries are equal - # if they (a) have the same keys, (b) the corresponding values for - # each key in the two dictionaries are equal when compared using the - # equality predicate, equalp (passed as an argument). The equality - # predicate is invoked with the key and the two values from each - # dictionary as arguments. - # - proc equal {equalp d1 d2} { - if {[dict size $d1] != [dict size $d2]} { return 0 } - dict for {k v} $d1 { - if {![dict exists $d2 $k]} { return 0 } - if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 } - } - return 1 - } - - # apply dictVar lambdaExpr ?arg1 arg2 ...? -- - # - # A combination of *dict with* and *apply*, this procedure creates a - # new procedure scope populated with the values in the dictionary - # variable. It then applies the lambdaTerm (anonymous procedure) in - # this new scope. If the procedure completes normally, then any - # changes made to variables in the dictionary are reflected back to - # the dictionary variable, otherwise they are ignored. This provides - # a transaction-style semantics whereby atomic updates to a - # dictionary can be performed. This procedure can also be useful for - # implementing a variety of control constructs, such as mutable - # closures. - # - proc apply {dictVar lambdaExpr args} { - upvar 1 $dictVar dict - set env $dict ;# copy - lassign $lambdaExpr params body ns - if {$ns eq ""} { set ns "::" } - set body [format { - upvar 1 env __env__ - dict with __env__ %s - } [list $body]] - set lambdaExpr [list $params $body $ns] - set rc [catch { ::apply $lambdaExpr {*}$args } ret opts] - if {$rc == 0} { - # Copy back any updates - set dict $env - } - return -options $opts $ret - } - - # capture ?level? ?exclude? ?include? -- - # - # Captures a snapshot of the current (scalar) variable bindings at - # $level on the stack into a dictionary environment. This dictionary - # can later be used with *dictutils apply* to partially restore the - # scope, creating a first approximation of closures. The *level* - # argument should be of the forms accepted by *uplevel* and - # designates which level to capture. It defaults to 1 as in uplevel. - # The *exclude* argument specifies an optional list of literal - # variable names to avoid when performing the capture. No variables - # matching any item in this list will be captured. The *include* - # argument can be used to specify a list of glob patterns of - # variables to capture. Only variables matching one of these - # patterns are captured. The default is a single pattern "*", for - # capturing all visible variables (as determined by *info vars*). - # - proc capture {{level 1} {exclude {}} {include {*}}} { - if {[string is integer $level]} { incr level } - set env [dict create] - foreach pattern $include { - foreach name [uplevel $level [list info vars $pattern]] { - if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue } - upvar $level $name value - catch { dict set env $name $value } ;# no arrays - } - } - return $env - } - - # nlappend dictVar keyList ?value ...? - # - # Append zero or more elements to the list value stored in the given - # dictionary at the path of keys specified in $keyList. If $keyList - # specifies a non-existent path of keys, nlappend will behave as if - # the path mapped to an empty list. - # - proc nlappend {dictvar keylist args} { - upvar 1 $dictvar dict - if {[info exists dict] && [dict exists $dict {*}$keylist]} { - set list [dict get $dict {*}$keylist] - } - lappend list {*}$args - dict set dict {*}$keylist $list - } - - # invoke cmd args... -- - # - # Helper procedure to invoke a callback command with arguments at - # the global scope. The helper ensures that proper quotation is - # used. The command is expected to be a list, e.g. {string equal}. - # - proc invoke {cmd args} { uplevel #0 $cmd $args } - - } diff --git a/src/vendormodules/gridplus-2.11.tm b/src/vendormodules/gridplus-2.11.tm new file mode 100644 index 0000000000000000000000000000000000000000..c3d7957ee89588c704c069666ab88b00db6dc9e6 GIT binary patch literal 50052 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 literal 0 HcmV?d00001 diff --git a/src/vendormodules/include_modules.config b/src/vendormodules/include_modules.config new file mode 100644 index 00000000..258273ca --- /dev/null +++ b/src/vendormodules/include_modules.config @@ -0,0 +1,17 @@ + +set local_modules [list\ + c:/repo/jn/tclmodules/overtype/modules overtype\ + c:/repo/jn/tclmodules/modpod/modules modpod\ + c:/repo/jn/tclmodules/packageTest/modules packageTest\ + c:/repo/jn/tclmodules/gridplus/modules gridplus\ + c:/repo/jn/tclmodules/tablelist/modules tablelist\ + c:/repo/jn/tclmodules/tablelist/modules tablelist_tile\ + c:/repo/jn/tclmodules/Thread/modules Thread\ + c:/repo/jn/tclmodules/Thread/modules Thread::platform::win32_x86_64\ +] + +set fossil_modules [dict create\ +] + +set git_modules [dict create\ +] \ No newline at end of file diff --git a/src/vendormodules/md5-2.0.8.tm b/src/vendormodules/md5-2.0.8.tm index f021c0ac..51f35dce 100644 --- a/src/vendormodules/md5-2.0.8.tm +++ b/src/vendormodules/md5-2.0.8.tm @@ -16,7 +16,7 @@ # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # ------------------------------------------------------------------------- -package require Tcl 8.2; # tcl minimum version +package require Tcl 8.2-; # tcl minimum version namespace eval ::md5 { variable accel diff --git a/src/vendormodules/modpod-0.1.0.tm b/src/vendormodules/modpod-0.1.0.tm new file mode 100644 index 00000000..84c4e754 --- /dev/null +++ b/src/vendormodules/modpod-0.1.0.tm @@ -0,0 +1,700 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2024 +# +# @@ Meta Begin +# Application modpod 0.1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin modpod_module_modpod 0 0.1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require modpod] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of modpod +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by modpod +#[list_begin itemized] + +package require Tcl 8.6- +package require struct::set ;#review +package require punk::lib +package require punk::args +#*** !doctools +#[item] [package {Tcl 8.6-}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::class { + #*** !doctools + #[subsection {Namespace modpod::class}] + #[para] class definitions + if {[info commands [namespace current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + } +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod { + namespace export {[a-z]*}; # Convention: export all lowercase + + variable connected + if {![info exists connected(to)]} { + set connected(to) list + } + variable modpodscript + set modpodscript [info script] + if {[string tolower [file extension $modpodscript]] eq ".tcl"} { + set connected(self) [file dirname $modpodscript] + } else { + #expecting a .tm + set connected(self) $modpodscript + } + variable loadables [info sharedlibextension] + variable sourceables {.tcl .tk} ;# .tm ? + + #*** !doctools + #[subsection {Namespace modpod}] + #[para] Core API functions for modpod + #[list_begin definitions] + + + + #proc sample1 {p1 args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [opt {?option value...?}]] + # #[para]Description of sample1 + # return "ok" + #} + + proc connect {args} { + puts stderr "modpod::connect--->>$args" + set argd [punk::args::get_dict { + -type -default "" + *values -min 1 -max 1 + path -type string -minlen 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" + } $args] + catch { + punk::lib::showdict $argd ;#heavy dependencies + } + set opt_path [dict get $argd values path] + variable connected + set original_connectpath $opt_path + set modpodpath [modpod::system::normalize $opt_path] ;# + + if {$modpodpath in $connected(to)} { + return [dict create ok ALREADY_CONNECTED] + } + lappend connected(to) $modpodpath + + set connected(connectpath,$opt_path) $original_connectpath + set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info_script]]}] + + set connected(location,$modpodpath) [file dirname $modpodpath] + set connected(startdata,$modpodpath) -1 + set connected(type,$modpodpath) [dict get $argd-opts -type] + set connected(fh,$modpodpath) "" + + if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { + set connected(type,$modpodpath) "unwrapped" + lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] + + } else { + #connect to .tm but may still be unwrapped version available + lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) + set this_pkg_tm_folder [file dirname $modpodpath] + if {$connected(type,$modpodpath) ne "unwrapped"} { + #Not directly connected to unwrapped version - but may still be redirected there + set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] + if {[file exists $unwrappedFolder]} { + #folder with exact version-match must exist for redirect to 'unwrapped' + set con(type,$modpodpath) "modpod-redirecting" + } + } + + } + set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" + set connected(tmfile,$modpodpath) + set tail_segments [list] + set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] + break + } + } + if {[llength $tail_segments]} { + set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require + } else { + set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] + } + + switch -exact -- $connected(type,$modpodpath) { + "modpod-redirecting" { + #redirect to the unwrapped version + set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] + + } + "unwrapped" { + if {[info commands ::thread::id] ne ""} { + set from [pid],[thread::id] + } else { + set from [pid] + } + #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" + return [list ok ""] + } + default { + #autodetect .tm - zip/tar ? + #todo - use vfs ? + + #connect to tarball - start at 1st header + set connected(startdata,$modpodpath) 0 + set fh [open $modpodpath r] + set connected(fh,$modpodpath) $fh + fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} + + if {$connected(startdata,$modpodpath) >= 0} { + #verify we have a valid tar header + if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { + seek $fh $connected(startdata,$modpodpath) start + return [list ok $fh] + } else { + #error "cannot verify tar header" + } + } + lpop connected(to) end + set connected(startdata,$modpodpath) -1 + unset connected(fh,$modpodpath) + catch {close $fh} + return [dict create err {Does not appear to be a valid modpod}] + } + } + } + proc disconnect {{modpod ""}} { + variable connected + if {![llength $connected(to)]} { + return 0 + } + if {$modpod eq ""} { + puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" + set modpod [lindex $connected(to) end] + } + + if {[set posn [lsearch $connected(to) $modpod]] == -1} { + puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" + return 0 + } + if {[string length $connected(fh,$modpod)]} { + close $connected(fh,$modpod) + } + array unset connected *,$modpod + set connected(to) [lreplace $connected(to) $posn $posn] + return 1 + } + proc get {args} { + set argd [punk::args::get_dict { + -from -default "" -help "path to pod" + *values -min 1 -max 1 + filename + } $args] + set frompod [dict get $argd opts -from] + set filename [dict get $argd values filename] + + variable connected + set modpod [::tarjar::system::connect_if_not $frompod] + set fh $connected(fh,$modpod) + if {$connected(type,$modpod) eq "unwrapped"} { + #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder + if {[string range $filename 0 0 eq "/"]} { + #absolute path (?) + set path [file join $connected(location,$modpod) .. [string trim $filename /]] + } else { + #relative path - use #modpod-xxx as base + set path [file join $connected(location,$modpod) $filename] + } + set fd [open $path r] + #utf-8? + #fconfigure $fd -encoding iso8859-1 -translation binary + return [list ok [lindex [list [read $fd] [close $fd]] 0]] + } else { + #read from vfs + puts stderr "get $filename from wrapped pod '$frompod' not implemented" + } + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval modpod::lib { + namespace export {[a-z]*}; # Convention: export all lowercase + namespace path [namespace parent] + #*** !doctools + #[subsection {Namespace modpod::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + proc is_valid_tm_version {versionpart} { + #Needs to be suitable for use with Tcl's 'package vcompare' + if {![catch [list package vcompare $versionparts $versionparts]]} { + return 1 + } else { + return 0 + } + } + proc make_zip_modpod {zipfile outfile} { + set mount_stub { + #zip file with Tcl loader prepended. + #generated using modpod::make_zip_modpod + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #determine module namespace so we can mount appropriately + proc intersect {A B} { + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + if {[llength $B] > [llength $A]} { + set res $A + set A $B + set B $res + } + set res {} + foreach x $A {set ($x) {}} + foreach x $B { + if {[info exists ($x)]} { + lappend res $x + } + } + return $res + } + set lcase_tmfile_segments [string tolower [file split $moddir]] + set lcase_modulepaths [string tolower [tcl::tm::list]] + foreach lc_mpath $lcase_modulepaths { + set mpath_segments [file split $lc_mpath] + if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { + set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use propertly cased tail + break + } + } + if {[llength $tail_segments]} { + set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require + set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver + } else { + set fullpackage $moduletail + set mount_at #modpod/#mounted-modpod-$mod_and_ver + } + + if {[info commands tcl::zipfs::mount] ne ""} { + #argument order changed to be consistent with vfs::zip::Mount etc + #early versions: zipfs::Mount mountpoint zipname + #since 2023-09: zipfs::Mount zipname mountpoint + #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on non-existance) + set mountpoints [dict keys [tcl::zipfs::mount]] + if {"//zipfs:/$mount_at" ni $mountpoints} { + #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it + if {[catch { + #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) + puts "tcl::zipfs::mount $modfile $mount_at" + tcl::zipfs::mount $modfile $mount_at + } errM]} { + #try old api + puts stderr ">>> tcl::zipfs::mount //zipfs://$mount_at $modfile" + tcl::zipfs::mount //zipfs:/$mount_at $modfile + } + if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + puts stderr "zipfs mounts: [zipfs mount]" + #tcl::zipfs::unmount //zipfs:/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form + source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + #fallback to slower vfs::zip + #NB. We don't create the intermediate dirs - but the mount still works + if {![file exists $moddir/$mount_at]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver" + append msg \n "If vfs::zip is unavailable - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $ + } + set fd [vfs::zip::Mount $modfile $moddir/$mount_at] + if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/$mount_at + error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" + } + } + source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm + } + } + #zipped data follows + } + #todo - test if zipfile has #modpod-loadcript.tcl before even creating + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub + + } + proc make_zip_modpod1 {zipfile outfile} { + set mount_stub { + #zip file with Tcl loader prepended. + #generated using modpod::make_zip_modpod + if {[catch {file normalize [info script]} modfile]} { + error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" + } + if {$modfile eq "" || ![file exists $modfile]} { + error "modpod zip stub error. Unable to determine module path" + } + set moddir [file dirname $modfile] + set mod_and_ver [file rootname [file tail $modfile]] + lassign [split $mod_and_ver -] moduletail version + if {[file exists $moddir/#modpod-$mod_and_ver]} { + source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm + } else { + if {![file exists $moddir/#mounted-modpod-$mod_and_ver]} { + if {[catch {package require vfs::zip} errM]} { + set msg "Unable to load vfs::zip package to mount module $mod_and_ver" + append msg \n "If vfs::zip is unavailable - the module can still be loaded by manually unzipping the file $modfile in place." + append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $ + } + set fd [vfs::zip::Mount $modfile $moddir/#mounted-modpod-$mod_and_ver] + if {![file exists $moddir/#mounted-modpod-$mod_and_ver/#modpod-$mod_and_ver/$mod_and_ver.tm]} { + vfs::zip::Unmount $fd $moddir/#mounted-modpod-$mod_and_ver + error "Unable to find #modpod-$mod_and_ver/$mod_and_ver.tm in $modfile" + } + } + source $moddir/#mounted-modpod-$mod_and_ver/#modpod-$mod_and_ver/$mod_and_ver.tm + } + #zipped data follows + } + #todo - test if zipfile has #modpod-loadcript.tcl before even creating + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub + + } + proc make_zip_source_mountable {zipfile outfile} { + set mount_stub { + package require vfs::zip + vfs::zip::Mount [info script] [info script] + } + append mount_stub \x1A + modpod::system::make_mountable_zip $zipfile $outfile $mount_stub + } + + #*** !doctools + #[list_end] [comment {--- end definitions namespace modpod::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +namespace eval modpod::system { + #*** !doctools + #[subsection {Namespace modpod::system}] + #[para] Internal functions that are not part of the API + + #deflate,store only supported + proc make_mountable_zip {zipfile outfile mount_stub} { + set in [open $zipfile r] + fconfigure $in -encoding iso8859-1 -translation binary + set out [open $outfile w+] + fconfigure $out -encoding iso8859-1 -translation binary + puts -nonewline $out $mount_stub + set offset [tell $out] + lappend report "sfx stub size: $offset" + fcopy $in $out + + close $in + set size [tell $out] + #Now seek in $out to find the end of directory signature: + #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text + if {$size < 65559} { + set seek 0 + } else { + set seek [expr {$size - 65559}] + } + seek $out $seek + set data [read $out] + set start_of_end [string last "\x50\x4b\x05\x06" $data] + #set start_of_end [expr {$start_of_end + $seek}] + incr start_of_end $seek + + lappend report "START-OF-END: $start_of_end ([expr {$start_of_end - $size}]) [string length $data]" + + seek $out $start_of_end + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + lappend report "End of central directory: [array get eocd]" + seek $out [expr {$start_of_end+16}] + + #adjust offset of start of central directory by the length of our sfx stub + puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $offset}]] + flush $out + + seek $out $start_of_end + set end_of_ctrl_dir [read $out] + binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ + eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) + + # 0x06054b50 - end of central dir signature + puts stderr "$end_of_ctrl_dir" + puts stderr "comment_len: $eocd(comment_len)" + puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" + lappend report "New dir offset: $eocd(diroffset)" + lappend report "Adjusting $eocd(totalnum) zip file items." + catch { + punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies + } + + seek $out $eocd(diroffset) + for {set i 0} {$i <$eocd(totalnum)} {incr i} { + set current_file [tell $out] + set fileheader [read $out 46] + puts -------------- + puts [ansistring VIEW -lf 1 $fileheader] + puts -------------- + #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + + binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + set ::last_header $fileheader + + puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" + puts "ver: $x(version)" + puts "method: $x(method)" + + #33639248 dec = 0x02014b50 - central file header signature + if { $x(sig) != 33639248 } { + error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" + } + + foreach size $x(lengths) var {filename extrafield comment} { + if { $size > 0 } { + set x($var) [read $out $size] + } else { + set x($var) "" + } + } + set next_file [tell $out] + lappend report "file $i: $x(offset) $x(sizes) $x(filename)" + + seek $out [expr {$current_file+42}] + puts -nonewline $out [binary format i [expr {$x(offset)+$offset}]] + + #verify: + flush $out + seek $out $current_file + set fileheader [read $out 46] + lappend report "old $x(offset) + $offset" + binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ + x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) + lappend report "new $x(offset)" + + seek $out $next_file + } + close $out + #pdict/showdict reuire punk & textlib - ie lots of dependencies + #don't fall over just because of that + catch { + punk::lib::showdict -roottype list -chan stderr $report + } + #puts [join $report \n] + return + } + + proc connect_if_not {{podpath ""}} { + upvar ::modpod::connected connected + set podpath [::modpod::system::normalize $podpath] + set docon 0 + if {![llength $connected(to)]} { + if {![string length $podpath]} { + error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" + } else { + set docon 1 + } + } else { + if {![string length $podpath]} { + set podpath [lindex $connected(to) end] + puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" + } else { + if {$podpath ni $connected(to)} { + set docon 1 + } + } + } + if {$docon} { + if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { + error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" + } else { + return $podpath + } + } + #we were already connected + return $podpath + } + + proc myversion {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" + } + set fname [file tail [file rootname [file normalize $script]]] + set scriptdir [file dirname $script] + + if {![string match "#modpod-*" $fname]} { + lassign [lrange [split $fname -] end-1 end] _pkgname version + } else { + lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version + if {![string length $version]} { + #try again on the name of the containing folder + lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version + #todo - proper walk up the directory tree + if {![string length $version]} { + #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) + lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version + } + } + } + + #tarjar::Log debug "'myversion' determined version for [info script]: $version" + return $version + } + + proc myname {} { + upvar ::modpod::connected connected + set script [info script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" + } + return $connected(fullpackage,$script) + } + proc myfullname {} { + upvar ::modpod::connected connected + set script [info script] + #set script [::tarjar::normalize $script] + set script [file normalize $script] + if {![string length $script]} { + error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" + } + return $::tarjar::connected(fullpackage,$script) + } + proc normalize {path} { + #newer versions of Tcl don't do tilde sub + + #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) + # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. + set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. + set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after + set path [file normalize $path] + #set path [string tolower $path] ;#must do this after file normalize + return [string map [list $matilda ~] $path] ;#get our tildes back. +} +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide modpod [namespace eval modpod { + variable pkg modpod + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/overtype-1.6.3.tm b/src/vendormodules/overtype-1.6.5.tm similarity index 87% rename from src/bootsupport/modules/overtype-1.6.3.tm rename to src/vendormodules/overtype-1.6.5.tm index ef12e956..143794fb 100644 --- a/src/bootsupport/modules/overtype-1.6.3.tm +++ b/src/vendormodules/overtype-1.6.5.tm @@ -7,7 +7,7 @@ # (C) Julian Noble 2003-2023 # # @@ Meta Begin -# Application overtype 1.6.3 +# Application overtype 1.6.5 # Meta platform tcl # Meta license BSD # @@ Meta End @@ -17,7 +17,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.3] +#[manpage_begin overtype_module_overtype 0 1.6.5] #[copyright "2024"] #[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] #[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] @@ -65,7 +65,15 @@ package require punk::assertion #*** !doctools #[list_end] - +#PERFORMANCE notes +#overtype is very performance sensitive - used in ansi output all over the place ie needs to be optimised +#NOTE use of tcl::dict::for tcl::string::range etc instead of ensemble versions. This is for the many tcl 8.6/8.7 interps which don't compile ensemble commands when in safe interps +#similar for tcl::namespace::eval - but this is at least on some versions of Tcl - faster even in a normal interp. Review to see if that holds for Tcl 9. +#for string map: when there are exactly 2 elements - it is faster to use a literal which has a special case optimisation in the c code +#ie use tcl::string::map {\n ""} ... instead of tcl::string::map [list \n ""] ... +#note that we can use unicode (e.g \uFF31) and other escapes such as \t within these curly braces - we don't have to use double quotes +#generally using 'list' is preferred for the map as less error prone. +#can also use: tcl::string::map "token $var" .. but be careful regarding quoting and whitespace in var. This should be used sparingly if at all. # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ @@ -78,10 +86,10 @@ package require punk::assertion # #todo - ellipsis truncation indicator for center,right -#v1.4 2023-07 - naive ansi color handling - todo - fix string range +#v1.4 2023-07 - naive ansi color handling - todo - fix tcl::string::range # - need to extract and replace ansi codes? -namespace eval overtype { +tcl::namespace::eval overtype { namespace import ::punk::assertion::assert punk::assertion::active true @@ -90,7 +98,7 @@ namespace eval overtype { namespace export * variable default_ellipsis_horizontal "..." ;#fallback variable default_ellipsis_vertical "..." - namespace eval priv { + tcl::namespace::eval priv { proc _init {} { upvar ::overtype::default_ellipsis_horizontal e_h upvar ::overtype::default_ellipsis_vertical e_v @@ -112,18 +120,18 @@ proc overtype::about {} { return "Simple text formatting. Author JMN. BSD-License" } -namespace eval overtype { - variable grapheme_widths [dict create] +tcl::namespace::eval overtype { + variable grapheme_widths [tcl::dict::create] variable escape_terminals #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + tcl::dict::set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #tcl::dict::set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + tcl::dict::set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals #self-contained 2 byte ansi escape sequences - review more? variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ + set ansi_2byte_codes_dict [tcl::dict::create\ "reset_terminal" "\u001bc"\ "save_cursor_posn" "\u001b7"\ "restore_cursor_posn" "\u001b8"\ @@ -138,66 +146,12 @@ namespace eval overtype { } -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - proc overtype::string_columns {text} { if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] + #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::ansistrip. Alternatively try punk::ansi::printing_length" + set text [punk::ansi::ansistrip $text] } return [punk::char::ansifreestring_width $text] } @@ -206,7 +160,7 @@ proc overtype::string_columns {text} { #These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock #overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay. #(i.e not even necessariy having it's top left within the underlay) -namespace eval overtype::priv { +tcl::namespace::eval overtype::priv { } #could return larger than colwidth @@ -232,7 +186,7 @@ proc _get_row_append_column {row} { } } -namespace eval overtype { +tcl::namespace::eval overtype { #*** !doctools #[subsection {Namespace overtype}] #[para] Core API functions for overtype @@ -240,7 +194,7 @@ namespace eval overtype { - #string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r + #tcl::string::range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r #render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string. #The underlay and overlay can be multiline blocks of text of varying line lengths. #The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text. @@ -257,13 +211,14 @@ namespace eval overtype { variable default_ellipsis_horizontal if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} + error {usage: ?-width ? ?-startcolumn ? ?-transparent [0|1|]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} } lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ + set opts [tcl::dict::create\ -bias ignored\ -width \uFFEF\ -height \uFFEF\ + -startcolumn 1\ -wrap 0\ -ellipsis 0\ -ellipsistext $default_ellipsis_horizontal\ @@ -278,31 +233,33 @@ namespace eval overtype { ] #-ellipsis args not used if -wrap is true set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { + foreach {k v} $argsflags { switch -- $k { - -looplimit - -width - -height - -bias - -wrap - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -appendlines - -transparent - -exposed1 - -exposed2 - -experimental {} + -looplimit - -width - -height - -startcolumn - -bias - -wrap - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -appendlines - -transparent - -exposed1 - -exposed2 - -experimental { + tcl::dict::set opts $k $v + } default { - set known_opts [dict keys $defaults] - error "overtype::renderspace unknown option '$k'. Known options: $known_opts" + error "overtype::renderspace unknown option '$k'. Known options: [tcl::dict::keys $opts]" } } } - set opts [dict merge $defaults $argsflags] + #set opts [tcl::dict::merge $defaults $argsflags] # -- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] + set opt_overflow [tcl::dict::get $opts -overflow] ##### # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. - set opt_wrap [dict get $opts -wrap] ;#wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) + set opt_wrap [tcl::dict::get $opts -wrap] ;#wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) ##### #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. - set opt_width [dict get $opts -width] - set opt_height [dict get $opts -height] - set opt_appendlines [dict get $opts -appendlines] - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo + set opt_width [tcl::dict::get $opts -width] + set opt_height [tcl::dict::get $opts -height] + set opt_startcolumn [tcl::dict::get $opts -startcolumn] + set opt_appendlines [tcl::dict::get $opts -appendlines] + set opt_transparent [tcl::dict::get $opts -transparent] + set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] + set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] + set opt_exposed1 [tcl::dict::get $opts -exposed1] ;#widechar_exposed_left - todo + set opt_exposed2 [tcl::dict::get $opts -exposed2] ;#widechar_exposed_right - todo # -- --- --- --- --- --- # ---------------------------- @@ -312,7 +269,7 @@ namespace eval overtype { set test_mode 1 set info_mode 0 set edit_mode 0 - set opt_experimental [dict get $opts -experimental] + set opt_experimental [tcl::dict::get $opts -experimental] foreach o $opt_experimental { switch -- $o { test_mode { @@ -343,9 +300,8 @@ namespace eval overtype { set reverse_mode 0 - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] + set underblock [tcl::string::map {\r\n \n} $underblock] + set overblock [tcl::string::map {\r\n \n} $overblock] #set underlines [split $underblock \n] @@ -393,11 +349,11 @@ namespace eval overtype { #a hack until we work out how to avoid infinite loops... # - set looplimit [dict get $opts -looplimit] + set looplimit [tcl::dict::get $opts -looplimit] if {$looplimit eq "\uFFEF"} { #looping for each char is worst case (all newlines?) - anything over that is an indication of something broken? #do we need any margin above the length? (telnet mapscii.me test) - set looplimit [expr {[string length $overblock] + 10}] + set looplimit [expr {[tcl::string::length $overblock] + 10}] } if {!$test_mode} { @@ -424,7 +380,7 @@ namespace eval overtype { } set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single? set lastpt [lindex $inputchunks end] - lset inputchunks end [string cat $lastpt [lindex $sequence_split 0]] + lset inputchunks end [tcl::string::cat $lastpt [lindex $sequence_split 0]] lappend inputchunks {*}[lrange $sequence_split 1 end] incr i } @@ -438,7 +394,7 @@ namespace eval overtype { lappend lflines $ln } if {[llength $lflines]} { - lset lflines end [string range [lindex $lflines end] 0 end-1] + lset lflines end [tcl::string::range [lindex $lflines end] 0 end-1] } set inputchunks $lflines[unset lflines] @@ -451,11 +407,11 @@ namespace eval overtype { #lassign [blocksize $overblock] _w overblock_width _h overblock_height - set replay_codes_underlay [dict create 1 ""] + set replay_codes_underlay [tcl::dict::create 1 ""] #lappend replay_codes_overlay "" set replay_codes_overlay "" set unapplied "" - set cursor_saved_position [dict create] + set cursor_saved_position [tcl::dict::create] set cursor_saved_attributes "" @@ -467,10 +423,10 @@ namespace eval overtype { if {$data_mode} { set col [_get_row_append_column $row] } else { - set col 1 + set col $opt_startcolumn } - set instruction_stats [dict create] + set instruction_stats [tcl::dict::create] set loop 0 #while {$overidx < [llength $inputchunks]} { } @@ -478,7 +434,7 @@ namespace eval overtype { while {[llength $inputchunks]} { #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx "" set overtext [lpop inputchunks 0] - if {![string length $overtext]} { + if {![tcl::string::length $overtext]} { incr loop continue } @@ -489,10 +445,10 @@ namespace eval overtype { #renderline pads each underaly line to width with spaces and should track where end of data is - #set overtext [string cat [lindex $replay_codes_overlay $overidx] $overtext] - set overtext [string cat $replay_codes_overlay $overtext] - if {[dict exists $replay_codes_underlay $row]} { - set undertext [string cat [dict get $replay_codes_underlay $row] $undertext] + #set overtext [tcl::string::cat [lindex $replay_codes_overlay $overidx] $overtext] + set overtext [tcl::string::cat $replay_codes_overlay $overtext] + if {[tcl::dict::exists $replay_codes_underlay $row]} { + set undertext [tcl::string::cat [tcl::dict::get $replay_codes_underlay $row] $undertext] } #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary - #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l @@ -512,25 +468,25 @@ namespace eval overtype { $undertext\ $overtext\ ] - set instruction [dict get $rinfo instruction] - set insert_mode [dict get $rinfo insert_mode] - set autowrap_mode [dict get $rinfo autowrap_mode] ;# - #set reverse_mode [dict get $rinfo reverse_mode];#how to support in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set overflow_right_column [dict get $rinfo overflow_right_column] - set unapplied [dict get $rinfo unapplied] - set unapplied_list [dict get $rinfo unapplied_list] - set post_render_col [dict get $rinfo cursor_column] - set post_render_row [dict get $rinfo cursor_row] - set c_saved_pos [dict get $rinfo cursor_saved_position] - set c_saved_attributes [dict get $rinfo cursor_saved_attributes] - set visualwidth [dict get $rinfo visualwidth] - set insert_lines_above [dict get $rinfo insert_lines_above] - set insert_lines_below [dict get $rinfo insert_lines_below] - dict set replay_codes_underlay [expr {$renderedrow+1}] [dict get $rinfo replay_codes_underlay] - #lset replay_codes_overlay [expr $overidx+1] [dict get $rinfo replay_codes_overlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] + set instruction [tcl::dict::get $rinfo instruction] + set insert_mode [tcl::dict::get $rinfo insert_mode] + set autowrap_mode [tcl::dict::get $rinfo autowrap_mode] ;# + #set reverse_mode [tcl::dict::get $rinfo reverse_mode];#how to support in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? + set rendered [tcl::dict::get $rinfo result] + set overflow_right [tcl::dict::get $rinfo overflow_right] + set overflow_right_column [tcl::dict::get $rinfo overflow_right_column] + set unapplied [tcl::dict::get $rinfo unapplied] + set unapplied_list [tcl::dict::get $rinfo unapplied_list] + set post_render_col [tcl::dict::get $rinfo cursor_column] + set post_render_row [tcl::dict::get $rinfo cursor_row] + set c_saved_pos [tcl::dict::get $rinfo cursor_saved_position] + set c_saved_attributes [tcl::dict::get $rinfo cursor_saved_attributes] + set visualwidth [tcl::dict::get $rinfo visualwidth] + set insert_lines_above [tcl::dict::get $rinfo insert_lines_above] + set insert_lines_below [tcl::dict::get $rinfo insert_lines_below] + tcl::dict::set replay_codes_underlay [expr {$renderedrow+1}] [tcl::dict::get $rinfo replay_codes_underlay] + #lset replay_codes_overlay [expr $overidx+1] [tcl::dict::get $rinfo replay_codes_overlay] + set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] @@ -542,7 +498,7 @@ namespace eval overtype { } #-- - if {[dict size $c_saved_pos] >= 1} { + if {[tcl::dict::size $c_saved_pos] >= 1} { set cursor_saved_position $c_saved_pos set cursor_saved_attributes $c_saved_attributes } @@ -557,7 +513,7 @@ namespace eval overtype { #todo - handle potential insertion mode as above for cursor restore? #keeping separate branches for debugging - review and merge as appropriate when stable - dict incr instruction_stats $instruction + tcl::dict::incr instruction_stats $instruction switch -- $instruction { {} { if {$test_mode == 0} { @@ -657,15 +613,15 @@ namespace eval overtype { #testfile belinda.ans uses this #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" - if {[dict exists $cursor_saved_position row]} { - set row [dict get $cursor_saved_position row] - set col [dict get $cursor_saved_position column] + if {[tcl::dict::exists $cursor_saved_position row]} { + set row [tcl::dict::get $cursor_saved_position row] + set col [tcl::dict::get $cursor_saved_position column] #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]" #set nextprefix $cursor_saved_attributes #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes - set replay_codes_overlay [dict get $rinfo replay_codes_overlay]$cursor_saved_attributes + set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]$cursor_saved_attributes #set replay_codes_overlay $cursor_saved_attributes - set cursor_saved_position [dict create] + set cursor_saved_position [tcl::dict::create] set cursor_saved_attributes "" } else { #TODO @@ -684,10 +640,10 @@ namespace eval overtype { puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]" - set sub_info [overtype::renderline -info 1 -width $colwidth -insert_mode $insert_mode -autowrap_mode $autowrap_mode -overflow [dict get $opts -overflow] "" $overflow_right] - set foldline [dict get $sub_info result] - set insert_mode [dict get $sub_info insert_mode] ;#probably not needed.. - set autowrap_mode [dict get $sub_info autowrap_mode] ;#nor this.. + set sub_info [overtype::renderline -info 1 -width $colwidth -insert_mode $insert_mode -autowrap_mode $autowrap_mode -overflow [tcl::dict::get $opts -overflow] "" $overflow_right] + set foldline [tcl::dict::get $sub_info result] + set insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed.. + set autowrap_mode [tcl::dict::get $sub_info autowrap_mode] ;#nor this.. linsert outputlines $renderedrow $foldline #review - row & col set by restore - but not if there was no save.. } @@ -726,7 +682,7 @@ namespace eval overtype { if {$row > [llength $outputlines]} { lappend outputlines "" } - set col 1 + set col $opt_startcolumn # ---------------------- } lf_mid { @@ -754,7 +710,7 @@ namespace eval overtype { set row $renderedrow - set col 1 + set col $opt_startcolumn incr row #only add newline if we're at the bottom if {$row > [llength $outputlines]} { @@ -768,7 +724,7 @@ namespace eval overtype { set unapplied "" set row $post_render_row #set col $post_render_col - set col 1 + set col $opt_startcolumn if {$row > [llength $outputlines]} { lappend outputlines {*}[lrepeat 1 ""] } @@ -776,7 +732,7 @@ namespace eval overtype { append rendered $overflow_right set overflow_right "" set row $post_render_row - set col 1 + set col $opt_startcolumn if {$row > [llength $outputlines]} { lappend outputlines {*}[lrepeat 1 ""] } @@ -804,7 +760,7 @@ namespace eval overtype { if {$row > [llength $outputlines]} { lappend outputlines {*}[lrepeat 1 ""] } - set col 1 + set col $opt_startcolumn } newlines_above { @@ -835,7 +791,7 @@ namespace eval overtype { set row $renderedrow set outputlines [linsert $outputlines [expr {$renderedrow }] {*}[lrepeat $insert_lines_below ""]] ;#note - linsert can add to end too incr row $insert_lines_below - set col 1 + set col $opt_startcolumn } else { #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]] #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs] @@ -858,7 +814,7 @@ namespace eval overtype { lappend outputlines {*}[lrepeat $insert_lines_below ""] } incr row $insert_lines_below - set col 1 + set col $opt_startcolumn @@ -901,7 +857,7 @@ namespace eval overtype { lappend outputlines "" } } - set c 1 + set c $opt_startcolumn } else { incr c } @@ -952,12 +908,12 @@ namespace eval overtype { set row $post_render_row ;#renderline will not advance row when reporting overflow char if {$autowrap_mode} { incr row - set col 1 ;#whether wrap or not - next data is at column 1 ?? + set col $opt_startcolumn ;#whether wrap or not - next data is at column 1 ?? } else { #this works for test_mode (which should become the default) - but could give a bad result otherwise - review - add tests fix. set col $post_render_col #set unapplied "" ;#this seems wrong? - #set unapplied [string range $unapplied 1 end] + #set unapplied [tcl::string::range $unapplied 1 end] #The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs #There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate #We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow' @@ -998,7 +954,7 @@ namespace eval overtype { } set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] } else { - set col 1 + set col $opt_startcolumn incr row } } else { @@ -1034,7 +990,7 @@ namespace eval overtype { if {!$opt_overflow && !$autowrap_mode} { #not allowed to overflow column or wrap therefore we get overflow data to truncate - if {[dict get $opts -ellipsis]} { + if {[tcl::dict::get $opts -ellipsis]} { set show_ellipsis 1 if {!$opt_ellipsiswhitespace} { #we don't want ellipsis if only whitespace was lost @@ -1045,11 +1001,11 @@ namespace eval overtype { if {$unapplied ne ""} { append lostdata $unapplied } - if {[string trim $lostdata] eq ""} { + if {[tcl::string::trim $lostdata] eq ""} { set show_ellipsis 0 } - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim [punk::ansi::stripansi $lostdata]] eq ""} { + #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] + if {[tcl::string::trim [punk::ansi::ansistrip $lostdata]] eq ""} { set show_ellipsis 0 } } @@ -1113,9 +1069,9 @@ namespace eval overtype { append debugmsg "test_mode:$test_mode\n" append debugmsg "data_mode:$data_mode\n" append debugmsg "opt_appendlines:$opt_appendlines\n" - append debugmsg "prev_row :[dict get $LASTCALL -cursor_row]\n" - append debugmsg "prev_col :[dict get $LASTCALL -cursor_column]\n" - dict for {k v} $rinfo { + append debugmsg "prev_row :[tcl::dict::get $LASTCALL -cursor_row]\n" + append debugmsg "prev_col :[tcl::dict::get $LASTCALL -cursor_column]\n" + tcl::dict::for {k v} $rinfo { append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n } append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n @@ -1148,7 +1104,7 @@ namespace eval overtype { foreach {underblock overblock} [lrange $args end-1 end] break #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ + set opts [tcl::dict::create\ -bias left\ -ellipsis 0\ -ellipsistext $default_ellipsis_horizontal\ @@ -1159,29 +1115,30 @@ namespace eval overtype { -exposed2 \uFFFD\ ] set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { + foreach {k v} $argsflags { switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} + -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 { + tcl::dict::set opts $k $v + } default { - set known_opts [dict keys $defaults] + set known_opts [tcl::dict::keys $opts] error "overtype::centre unknown option '$k'. Known options: $known_opts" } } } - set opts [dict merge $defaults $argsflags] + #set opts [tcl::dict::merge $defaults $argsflags] # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] + set opt_transparent [tcl::dict::get $opts -transparent] + set opt_ellipsis [tcl::dict::get $opts -ellipsis] + set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] + set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] + set opt_exposed1 [tcl::dict::get $opts -exposed1] + set opt_exposed2 [tcl::dict::get $opts -exposed2] # -- --- --- --- --- --- - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] + set underblock [tcl::string::map {\r\n \n} $underblock] + set overblock [tcl::string::map {\r\n \n} $overblock] set underlines [split $underblock \n] #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] @@ -1197,7 +1154,7 @@ namespace eval overtype { set left_exposed [expr {$under_exposed_max / 2}] } else { set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { + if {[tcl::string::tolower [tcl::dict::get $opts -bias]] eq "left"} { set left_exposed $beforehalf } else { #bias to the right @@ -1223,30 +1180,30 @@ namespace eval overtype { set udiff [expr {$colwidth - $ulen}] set undertext "$undertext[string repeat { } $udiff]" } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] + set undertext [tcl::string::cat $replay_codes_underlay $undertext] + set overtext [tcl::string::cat $replay_codes_overlay $overtext] set overflowlength [expr {$overtext_datalen - $colwidth}] #review - right-to-left langs should elide on left! - extra option required if {$overflowlength > 0} { #overlay line wider or equal - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] + set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [tcl::dict::get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] + set rendered [tcl::dict::get $rinfo result] + set overflow_right [tcl::dict::get $rinfo overflow_right] + set unapplied [tcl::dict::get $rinfo unapplied] #todo - get replay_codes from overflow_right instead of wherever it was truncated? #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] + if {![tcl::dict::get $opts -overflow]} { + #lappend outputlines [tcl::string::range $overtext 0 [expr {$colwidth - 1}]] + #set overtext [tcl::string::range $overtext 0 $colwidth-1 ] if {$opt_ellipsis} { set show_ellipsis 1 if {!$opt_ellipsiswhitespace} { #we don't want ellipsis if only whitespace was lost - #don't use string range on ANSI data - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] + #don't use tcl::string::range on ANSI data + #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] set lostdata "" if {$overflow_right ne ""} { append lostdata $overflow_right @@ -1254,7 +1211,7 @@ namespace eval overtype { if {$unapplied ne ""} { append lostdata $unapplied } - if {[string trim $lostdata] eq ""} { + if {[tcl::string::trim $lostdata] eq ""} { set show_ellipsis 0 } } @@ -1269,10 +1226,10 @@ namespace eval overtype { #background block is wider than or equal to data for this line #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [dict get $rinfo result] + lappend outputlines [tcl::dict::get $rinfo result] } - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] + set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] + set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] } return [join $outputlines \n] } @@ -1290,7 +1247,7 @@ namespace eval overtype { } foreach {underblock overblock} [lrange $args end-1 end] break - set defaults [dict create\ + set opts [tcl::dict::create\ -bias ignored\ -ellipsis 0\ -ellipsistext $default_ellipsis_horizontal\ @@ -1302,30 +1259,31 @@ namespace eval overtype { -align "left"\ ] set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { + tcl::dict::for {k v} $argsflags { switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {} + -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align { + tcl::dict::set opts $k $v + } default { - set known_opts [dict keys $defaults] + set known_opts [tcl::dict::keys $opts] error "overtype::centre unknown option '$k'. Known options: $known_opts" } } } - set opts [dict merge $defaults $argsflags] + #set opts [tcl::dict::merge $defaults $argsflags] # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_align [dict get $opts -align] + set opt_transparent [tcl::dict::get $opts -transparent] + set opt_ellipsis [tcl::dict::get $opts -ellipsis] + set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] + set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] + set opt_overflow [tcl::dict::get $opts -overflow] + set opt_exposed1 [tcl::dict::get $opts -exposed1] + set opt_exposed2 [tcl::dict::get $opts -exposed2] + set opt_align [tcl::dict::get $opts -align] # -- --- --- --- --- --- - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] + set underblock [tcl::string::map {\r\n \n} $underblock] + set overblock [tcl::string::map {\r\n \n} $overblock] set underlines [split $underblock \n] #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] @@ -1375,27 +1333,27 @@ namespace eval overtype { set startoffset 0 ;#negative? } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] + set undertext [tcl::string::cat $replay_codes_underlay $undertext] + set overtext [tcl::string::cat $replay_codes_overlay $overtext] set overflowlength [expr {$overtext_datalen - $colwidth}] if {$overflowlength > 0} { #raw overtext wider than undertext column set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] + set replay_codes [tcl::dict::get $rinfo replay_codes] + set rendered [tcl::dict::get $rinfo result] if {!$opt_overflow} { if {$opt_ellipsis} { set show_ellipsis 1 if {!$opt_ellipsiswhitespace} { #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { + set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] + if {[tcl::string::trim $lostdata] eq ""} { set show_ellipsis 0 } } if {$show_ellipsis} { - set ellipsis [string cat $replay_codes $opt_ellipsistext] + set ellipsis [tcl::string::cat $replay_codes $opt_ellipsistext] #todo - overflow on left if allign = right?? set rendered [overtype::right $rendered $ellipsis] } @@ -1407,11 +1365,11 @@ namespace eval overtype { #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [dict get $rinfo result] + lappend outputlines [tcl::dict::get $rinfo result] } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] + set replay_codes [tcl::dict::get $rinfo replay_codes] + set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] + set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] } return [join $outputlines \n] @@ -1428,9 +1386,10 @@ namespace eval overtype { if {[llength $args] < 2} { error {usage: ?-blockalign left|centre|right? ?-textalign left|centre|right? ?-overflow [1|0]? ?-transparent 0|? undertext overtext} } - foreach {underblock overblock} [lrange $args end-1 end] break + #foreach {underblock overblock} [lrange $args end-1 end] break + lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ + set opts [tcl::dict::create\ -ellipsis 0\ -ellipsistext $default_ellipsis_horizontal\ -ellipsiswhitespace 0\ @@ -1445,34 +1404,33 @@ namespace eval overtype { -blockvertical "top"\ ] set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { + tcl::dict::for {k v} $argsflags { switch -- $k { - -blockalignbias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -textalign - -blockalign - -blockvertical {} + -blockalignbias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -textalign - -blockalign - -blockvertical { + tcl::dict::set opts $k $v + } default { - set known_opts [dict keys $defaults] - error "overtype::block unknown option '$k'. Known options: $known_opts" + error "overtype::block unknown option '$k'. Known options: [tcl::dict::keys $opts]" } } } - set opts [dict merge $defaults $argsflags] # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_textalign [dict get $opts -textalign] - set opt_blockalign [dict get $opts -blockalign] + set opt_transparent [tcl::dict::get $opts -transparent] + set opt_ellipsis [tcl::dict::get $opts -ellipsis] + set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] + set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] + set opt_overflow [tcl::dict::get $opts -overflow] + set opt_exposed1 [tcl::dict::get $opts -exposed1] + set opt_exposed2 [tcl::dict::get $opts -exposed2] + set opt_textalign [tcl::dict::get $opts -textalign] + set opt_blockalign [tcl::dict::get $opts -blockalign] if {$opt_blockalign eq "center"} { set opt_blockalign "centre" } # -- --- --- --- --- --- - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] + set underblock [tcl::string::map {\r\n \n} $underblock] + set overblock [tcl::string::map {\r\n \n} $overblock] set underlines [split $underblock \n] #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] @@ -1497,7 +1455,7 @@ namespace eval overtype { set left_exposed [expr {$under_exposed_max / 2}] } else { set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -blockalignbias]] eq "left"} { + if {[tcl::string::tolower [tcl::dict::get $opts -blockalignbias]] eq "left"} { set left_exposed $beforehalf } else { #bias to the right @@ -1552,24 +1510,24 @@ namespace eval overtype { set startoffset 0 ;#negative? } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] + set undertext [tcl::string::cat $replay_codes_underlay $undertext] + set overtext [tcl::string::cat $replay_codes_overlay $overtext] set overflowlength [expr {$overtext_datalen - $colwidth}] if {$overflowlength > 0} { #raw overtext wider than undertext column set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] + set replay_codes [tcl::dict::get $rinfo replay_codes] + set rendered [tcl::dict::get $rinfo result] + set overflow_right [tcl::dict::get $rinfo overflow_right] + set unapplied [tcl::dict::get $rinfo unapplied] if {!$opt_overflow} { if {$opt_ellipsis} { set show_ellipsis 1 if {!$opt_ellipsiswhitespace} { #we don't want ellipsis if only whitespace was lost - #don't use string range on ANSI data - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] + #don't use tcl::string::range on ANSI data + #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] set lostdata "" if {$overflow_right ne ""} { append lostdata $overflow_right @@ -1577,7 +1535,7 @@ namespace eval overtype { if {$unapplied ne ""} { append lostdata $unapplied } - if {[string trim $lostdata] eq ""} { + if {[tcl::string::trim $lostdata] eq ""} { set show_ellipsis 0 } } @@ -1590,13 +1548,13 @@ namespace eval overtype { # set show_ellipsis 1 # if {!$opt_ellipsiswhitespace} { # #we don't want ellipsis if only whitespace was lost - # set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - # if {[string trim $lostdata] eq ""} { + # set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] + # if {[tcl::string::trim $lostdata] eq ""} { # set show_ellipsis 0 # } # } # if {$show_ellipsis} { - # set ellipsis [string cat $replay_codes $opt_ellipsistext] + # set ellipsis [tcl::string::cat $replay_codes $opt_ellipsistext] # #todo - overflow on left if allign = right?? # set rendered [overtype::right $rendered $ellipsis] # } @@ -1608,13 +1566,13 @@ namespace eval overtype { #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] - lappend outputlines [dict get $rinfo result] + set overflow_right [tcl::dict::get $rinfo overflow_right] + set unapplied [tcl::dict::get $rinfo unapplied] + lappend outputlines [tcl::dict::get $rinfo result] } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] + set replay_codes [tcl::dict::get $rinfo replay_codes] + set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] + set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] } return [join $outputlines \n] @@ -1661,7 +1619,8 @@ namespace eval overtype { # error "overtype::renderline not allowed to contain newlines" #} - set defaults [dict create\ + #generally faster to create a new dict in the proc than to use a namespace variable to create dict once and link to variable (2024 8.6/8.7) + set opts [tcl::dict::create\ -etabs 0\ -width \uFFEF\ -overflow 0\ @@ -1688,41 +1647,41 @@ namespace eval overtype { #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { + tcl::dict::for {k v} $argsflags { switch -- $k { - -experimental - -cp437 - -width - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -autowrap_mode - -reverse_mode - -info - -exposed1 - -exposed2 - -cursor_restore_attributes {} + -experimental - -cp437 - -width - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -autowrap_mode - -reverse_mode - -info - -exposed1 - -exposed2 - -cursor_restore_attributes { + tcl::dict::set opts $k $v + } default { - set known_opts [dict keys $defaults] - error "overtype::renderline unknown option '$k'. Known options: $known_opts" + error "overtype::renderline unknown option '$k'. Known options: [tcl::dict::keys $opts]" } } } - set opts [dict merge $defaults $argsflags] # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_width [dict get $opts -width] - set opt_etabs [dict get $opts -etabs] - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [dict get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [dict get $opts -cursor_row] + set opt_width [tcl::dict::get $opts -width] + set opt_etabs [tcl::dict::get $opts -etabs] + set opt_overflow [tcl::dict::get $opts -overflow] + set opt_colstart [tcl::dict::get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay + set opt_colcursor [tcl::dict::get $opts -cursor_column];#start cursor column relative to overlay + set opt_row_context [tcl::dict::get $opts -cursor_row] if {[string length $opt_row_context]} { - if {![string is integer -strict $opt_row_context] || $opt_row_context <1 } { + if {![tcl::string::is integer -strict $opt_row_context] || $opt_row_context <1 } { error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" } } # -- --- --- --- --- --- --- --- --- --- --- --- #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode) - set opt_insert_mode [dict get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) + set opt_insert_mode [tcl::dict::get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) #default is for overtype # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_autowrap_mode [dict get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line - set opt_reverse_mode [dict get $opts -reverse_mode] ;#DECSNM + set opt_autowrap_mode [tcl::dict::get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line + set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] ;#DECSNM # -- --- --- --- --- --- --- --- --- --- --- --- - set temp_cursor_saved [dict get $opts -cursor_restore_attributes] + set temp_cursor_saved [tcl::dict::get $opts -cursor_restore_attributes] set test_mode 0 - set cp437_glyphs [dict get $opts -cp437] - foreach e [dict get $opts -experimental] { + set cp437_glyphs [tcl::dict::get $opts -cp437] + foreach e [tcl::dict::get $opts -experimental] { switch -- $e { test_mode { set test_mode 1 @@ -1731,16 +1690,16 @@ namespace eval overtype { } } set test_mode 1 ;#try to elminate - set cp437_map [dict create] + set cp437_map [tcl::dict::create] if {$cp437_glyphs} { set cp437_map [set ::punk::ansi::cp437_map] #for cp437 images we need to map these *after* splitting ansi #some old files might use newline for its glyph.. but we can't easily support that. #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs? - dict unset cp437_map \n + tcl::dict::unset cp437_map \n } - set opt_transparent [dict get $opts -transparent] + set opt_transparent [tcl::dict::get $opts -transparent] if {$opt_transparent eq "0"} { set do_transparency 0 } else { @@ -1750,10 +1709,10 @@ namespace eval overtype { } } # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] + set opt_returnextra [tcl::dict::get $opts -info] # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] + set opt_exposed1 [tcl::dict::get $opts -exposed1] + set opt_exposed2 [tcl::dict::get $opts -exposed2] # -- --- --- --- --- --- --- --- --- --- --- --- if {$opt_row_context eq ""} { @@ -1768,6 +1727,8 @@ namespace eval overtype { if {[info exists punk::console::tabwidth]} { #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync + #todo - we also need to handle the new threaded repl where console config is in a different thread. + # - also - concept of sub-regions being mini-consoles with their own settings - 'id' for console, or use in/out channels as id? set tw $::punk::console::tabwidth } else { set tw 8 @@ -1796,7 +1757,12 @@ namespace eval overtype { # -- --- --- --- --- --- --- --- if {$under ne ""} { - set undermap [punk::ansi::ta::split_codes_single $under] + if {[punk::ansi::ta::detect $under]} { + set undermap [punk::ansi::ta::split_codes_single $under] + } else { + #single plaintext part + set undermap [list $under] + } } else { set undermap [list] } @@ -1814,11 +1780,12 @@ namespace eval overtype { #pt = plain text #append pt_underchars $pt if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] + set pt [tcl::string::map $cp437_map $pt] } foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. + #an ugly but easy hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. + #todo - test decimal value instead, compare performance switch -- $grapheme { " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - @@ -1868,23 +1835,24 @@ namespace eval overtype { #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc if {$code ne ""} { - set c1c2 [string range $code 0 1] - set leadernorm [string range [string map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars + 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];# leadernorm is 1st 2 chars mapped to normalised indicator - or is original 2 chars switch -- $leadernorm { 7CSI - 8CSI { #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse #REVIEW - what else could end in m but be mistaken as a normal SGR code here? set maybemouse "" - if {[string index $c1c2 0] eq "\x1b"} { - set maybemouse [string index $code 2] + if {[tcl::string::index $c1c2 0] eq "\x1b"} { + set maybemouse [tcl::string::index $code 2] } - if {$maybemouse ne "<" && [string index $code end] eq "m"} { + if {$maybemouse ne "<" && [tcl::string::index $code end] eq "m"} { if {[punk::ansi::codetype::is_sgr_reset $code]} { set u_codestack [list "\x1b\[m"] } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { @@ -1898,7 +1866,7 @@ namespace eval overtype { } } 7GFX { - switch -- [string index $code 2] { + switch -- [tcl::string::index $code 2] { "0" { set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess } @@ -2008,7 +1976,17 @@ namespace eval overtype { #this will be processed as transparent - and handle doublewidth underlay characters appropriately set startpad_overlay [string repeat " " [expr {$opt_colstart -1}]] append startpad_overlay $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] + if {$startpad_overlay ne ""} { + if {[punk::ansi::ta::detect $startpad_overlay]} { + set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] + } else { + #single plaintext part + set overmap [list $startpad_overlay] + } + } else { + set overmap [list] + } + #set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] #### #??? @@ -2035,7 +2013,7 @@ namespace eval overtype { #todo - wrap in test for empty pt (we used split_codes_single - and it may be common for sgr sequences to be unmerged and so have empty pts between) if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] + set pt [tcl::string::map $cp437_map $pt] } append pt_overchars $pt #will get empty pt between adjacent codes @@ -2099,8 +2077,8 @@ namespace eval overtype { #set replay_codes_overlay [join $o_codestack ""] set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - #if {[dict exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [dict get $overstacks $max_overlay_grapheme_index] ""] + #if {[tcl::dict::exists $overstacks $max_overlay_grapheme_index]} { + # set replay_codes_overlay [join [tcl::dict::get $overstacks $max_overlay_grapheme_index] ""] #} else { # set replay_codes_overlay "" #} @@ -2161,6 +2139,17 @@ namespace eval overtype { set insert_mode $opt_insert_mode ;#default 1 set autowrap_mode $opt_autowrap_mode ;#default 1 + #set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM + #set re_col_move {\x1b\[([0-9]*)(C|D|G)$} + #set re_row_move {\x1b\[([0-9]*)(A|B)$} + #set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? + #set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} + #set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! + #set re_cursor_restore {\x1b\[u$} + #set re_cursor_save_dec {\x1b7$} + #set re_cursor_restore_dec {\x1b8$} + #set re_other_single {\x1b(D|M|E)$} + #set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins #puts "-->$overlay_grapheme_control_list<--" #puts "-->overflow_idx: $overflow_idx" @@ -2188,7 +2177,7 @@ namespace eval overtype { #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE - set chtest [string map [list \n \x85 \b \r \v \x7f ] $ch] + set chtest [tcl::string::map [list \n \x85 \b \r \v \x7f ] $ch] #puts --->chtest:$chtest #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached switch -- $chtest { @@ -2276,7 +2265,7 @@ namespace eval overtype { #review split 2w overflow? #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line #better to consider the overlay char as unable to be applied to the line - #render empty string to column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied + #render empty column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here. priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode #change the overflow_idx @@ -2315,7 +2304,7 @@ namespace eval overtype { #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) if {$idx > [llength $outcols]-1} { lappend outcols " " - #dict set understacks $idx [list] ;#review - use idx-1 codestack? + #tcl::dict::set understacks $idx [list] ;#review - use idx-1 codestack? lset understacks $idx [list] incr idx incr cursor_column @@ -2343,7 +2332,7 @@ namespace eval overtype { if {[grapheme_width_cached $ch] == 1} { if {!$insert_mode} { #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay + set next_pt_overchar [tcl::string::index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay if {$next_pt_overchar eq ""} { #special-case trailing transparent - no next_pt_overchar incr idx @@ -2354,7 +2343,7 @@ namespace eval overtype { incr cursor_column } else { #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode + #priv::render_addchar $idx $opt_exposed1 [tcl::dict::get $overstacks $idx_over] [tcl::dict::get $overstacks_gx $idx_over] $insert_mode priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode incr idx incr cursor_column @@ -2497,35 +2486,23 @@ namespace eval overtype { } other { - set code $item + #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. + #we should probably therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore + set code [tcl::string::map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $item] #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' - set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM - set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - set re_row_move {\x1b\[([0-9]*)(A|B)$} - set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? - set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! - set re_cursor_restore {\x1b\[u$} - set re_cursor_save_dec {\x1b7$} - set re_cursor_restore_dec {\x1b8$} - set re_other_single {\x1b(D|M|E)$} - set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins set matchinfo [list] #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping #review - cost/benefit of function calls within these switch-arms instead of inline code? - #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. - #we should probably therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore - set code [string map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $code] - - - set c1 [string index $code 0] - set c1c2c3 [string range $code 0 2] + set c1 [tcl::string::index $code 0] + set c1c2c3 [tcl::string::range $code 0 2] #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} - set leadernorm [string range [string map [list\ + #tcl 8.7 - faster to use inline list than to store it in a local var outside of loop. + #(surprising - but presumably ) + set leadernorm [tcl::string::range [tcl::string::map [list\ \x1b\[< 1006\ \x1b\[ 7CSI\ \x9b 8CSI\ @@ -2539,16 +2516,16 @@ namespace eval overtype { 1006 { #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release - set codenorm [string cat $leadernorm [string range $code 3 end]] + set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 3 end]] } 7CSI - 7OSC { - set codenorm [string cat $leadernorm [string range $code 2 end]] + set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]] } 7ESC { - set codenorm [string cat $leadernorm [string range $code 1 end]] + set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] } 8CSI - 8OSC { - set codenorm [string cat $leadernorm [string range $code 1 end]] + set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] } default { #we haven't made a mapping for this @@ -2561,7 +2538,7 @@ namespace eval overtype { 1006 { #TODO # - switch -- [string index $codenorm end] { + switch -- [tcl::string::index $codenorm end] { M { puts stderr "mousedown $codenorm" } @@ -2572,9 +2549,9 @@ namespace eval overtype { } {7CSI} - {8CSI} { - set param [string range $codenorm 4 end-1] - #puts stdout "--> CSI [string index $leadernorm 0] bit param:$param" - switch -- [string index $codenorm end] { + set param [tcl::string::range $codenorm 4 end-1] + #puts stdout "--> CSI [tcl::string::index $leadernorm 0] bit param:$param" + switch -- [tcl::string::index $codenorm end] { D { #Col move #puts stdout "<-back" @@ -2679,8 +2656,8 @@ namespace eval overtype { #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" incr idx $moveend incr cursor_column $moveend - #if {[dict exists $understacks $idx]} { - # set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext + #if {[tcl::dict::exists $understacks $idx]} { + # set stackinfo [tcl::dict::get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext #} else { # set stackinfo [list] #} @@ -2690,7 +2667,7 @@ namespace eval overtype { set stackinfo [list] } if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [dict get $understacks_gx $idx] + #set gxstackinfo [tcl::dict::get $understacks_gx $idx] set gxstackinfo [lindex $understacks_gx $idx] } else { set gxstackinfo [list] @@ -3000,8 +2977,8 @@ namespace eval overtype { #$re_mode if first after CSI is "?" #some docs mention ESC=h|l - not seen on windows terminals.. review #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html - if {[string index $codenorm 4] eq "?"} { - set num [string range $codenorm 5 end-1] ;#param between ? and h|l + if {[tcl::string::index $codenorm 4] eq "?"} { + set num [tcl::string::range $codenorm 5 end-1] ;#param between ? and h|l #lassign $matchinfo _match num type switch -- $num { 5 { @@ -3062,7 +3039,7 @@ namespace eval overtype { } 7ESC { #$re_other_single - switch -- [string index $codenorm end] { + switch -- [tcl::string::index $codenorm end] { D { #\x84 #index (IND) @@ -3158,7 +3135,7 @@ namespace eval overtype { set gxleader "" if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] + #set g0 [tcl::dict::get $understacks_gx $i] set g0 [lindex $understacks_gx $i] if {$g0 ne $prev_g0} { if {$g0 eq [list "gx0_on"]} { @@ -3174,7 +3151,7 @@ namespace eval overtype { set sgrleader "" if {$i < [llength $understacks]} { - #set cstack [dict get $understacks $i] + #set cstack [tcl::dict::get $understacks $i] set cstack [lindex $understacks $i] if {$cstack ne $prevstack} { if {[llength $prevstack] && ![llength $cstack]} { @@ -3228,7 +3205,7 @@ namespace eval overtype { append outstring $gxleader append outstring $sgrleader if {$idx+1 < $cursor_column} { - append outstring [string map [list "\u0000" " "] $ch] + append outstring [tcl::string::map {\u0000 " "} $ch] } else { append outstring $ch } @@ -3237,16 +3214,16 @@ namespace eval overtype { } #flower.ans good test for null handling - reverse line building if {![ansistring length $overflow_right]} { - set outstring [string trimright $outstring "\u0000"] + set outstring [tcl::string::trimright $outstring "\u0000"] } - set outstring [string map [list "\u0000" " "] $outstring] - set overflow_right [string trimright $overflow_right "\u0000"] - set overflow_right [string map [list "\u0000" " "] $overflow_right] + set outstring [tcl::string::map {\u0000 " "} $outstring] + set overflow_right [tcl::string::trimright $overflow_right "\u0000"] + set overflow_right [tcl::string::map {\u0000 " "} $overflow_right] set replay_codes "" if {[llength $understacks] > 0} { if {$overflow_idx == -1} { - #set tail_idx [dict size $understacks] + #set tail_idx [tcl::dict::size $understacks] set tail_idx [llength $understacks] } else { set tail_idx [llength $undercols] @@ -3289,7 +3266,7 @@ namespace eval overtype { } else { set overflow_right_column [expr {$overflow_idx+1}] } - set result [dict create\ + set result [tcl::dict::create\ result $outstring\ visualwidth [punk::ansi::printing_length $outstring]\ instruction $instruction\ @@ -3325,14 +3302,14 @@ namespace eval overtype { set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ. } } - dict set result result [ansistring $viewop -lf 1 -vt 1 [dict get $result result]] - dict set result overflow_right [ansistring VIEW -lf 1 -vt 1 [dict get $result overflow_right]] - dict set result unapplied [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied]] - dict set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied_list]] - dict set result replay_codes [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes]] - dict set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_underlay]] - dict set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_overlay]] - dict set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [dict get $result cursor_saved_attributes]] + tcl::dict::set result result [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result result]] + tcl::dict::set result overflow_right [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result overflow_right]] + tcl::dict::set result unapplied [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied]] + tcl::dict::set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied_list]] + tcl::dict::set result replay_codes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes]] + tcl::dict::set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_underlay]] + tcl::dict::set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_overlay]] + tcl::dict::set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result cursor_saved_attributes]] return $result } } else { @@ -3345,7 +3322,7 @@ namespace eval overtype { #[list_end] [comment {--- end definitions namespace overtype ---}] } -namespace eval overtype::piper { +tcl::namespace::eval overtype::piper { proc overcentre {args} { if {[llength $args] < 2} { error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} @@ -3369,19 +3346,19 @@ namespace eval overtype::piper { proc overtype::transparentline {args} { foreach {under over} [lrange $args end-1 end] break set argsflags [lrange $args 0 end-2] - set defaults [dict create\ + set defaults [tcl::dict::create\ -transparent 1\ -exposed 1 " "\ -exposed 2 " "\ ] - set newargs [dict merge $defaults $argsflags] + set newargs [tcl::dict::merge $defaults $argsflags] tailcall overtype::renderline {*}$newargs $under $over } #renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. # We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. #We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. # -namespace eval overtype::piper { +tcl::namespace::eval overtype::piper { proc renderline {args} { if {[llength $args] < 2} { error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} @@ -3398,11 +3375,11 @@ interp alias "" piper_renderline "" overtype::piper::renderline #(a cache of ansifreestring_width calls - as these are quite regex heavy) proc overtype::grapheme_width_cached {ch} { variable grapheme_widths - if {[dict exists $grapheme_widths $ch]} { - return [dict get $grapheme_widths $ch] + if {[tcl::dict::exists $grapheme_widths $ch]} { + return [tcl::dict::get $grapheme_widths $ch] } set width [punk::char::ansifreestring_width $ch] - dict set grapheme_widths $ch $width + tcl::dict::set grapheme_widths $ch $width return $width } @@ -3420,9 +3397,9 @@ proc overtype::test_renderline {} { #block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both proc overtype::blocksize {textblock} { if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings + return [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings } - if {[string first \t $textblock] >= 0} { + if {[tcl::string::first \t $textblock] >= 0} { if {[info exists punk::console::tabwidth]} { set tw $::punk::console::tabwidth } else { @@ -3430,12 +3407,12 @@ proc overtype::blocksize {textblock} { } set textblock [textutil::tabify::untabify2 $textblock $tw] } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests + #ansistrip on entire block in one go rather than line by line - result should be the same - review - make tests if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] + set textblock [punk::ansi::ansistrip $textblock] } - if {[string first \n $textblock] >= 0} { - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list + if {[tcl::string::last \n $textblock] >= 0} { + set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map {\n {}} $textblock]]}] ;#faster than splitting into single-char list set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] } else { set num_le 0 @@ -3444,22 +3421,22 @@ proc overtype::blocksize {textblock} { #our concept of block-height is likely to be different to other line-counting mechanisms set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height + return [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height } -namespace eval overtype::priv { - variable cache_is_sgr [dict create] +tcl::namespace::eval overtype::priv { + variable cache_is_sgr [tcl::dict::create] #we are likely to be asking the same question of the same ansi codes repeatedly #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS #todo - test if still worthwhile after a large cache is built up. (limit cache size?) proc is_sgr {code} { variable cache_is_sgr - if {[dict exists $cache_is_sgr $code]} { - return [dict get $cache_is_sgr $code] + if {[tcl::dict::exists $cache_is_sgr $code]} { + return [tcl::dict::get $cache_is_sgr $code] } set answer [punk::ansi::codetype::is_sgr $code] - dict set cache_is_sgr $code $answer + tcl::dict::set cache_is_sgr $code $answer return $answer } proc render_unapplied {overlay_grapheme_control_list gci} { @@ -3564,7 +3541,7 @@ namespace eval overtype::priv { upvar understacks_gx gxstacks #ECH clears character attributes from erased character positions #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller should do that mapping and only supply 1 or greater. - if {![string is integer -strict $count] || $count < 1} { + if {![tcl::string::is integer -strict $count] || $count < 1} { error "render_erasechar count must be integer >= 1" } set start $i @@ -3639,15 +3616,15 @@ namespace eval overtype::priv { # -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { +tcl::namespace::eval overtype { interp alias {} ::overtype::center {} ::overtype::centre } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Ready -package provide overtype [namespace eval overtype { +package provide overtype [tcl::namespace::eval overtype { variable version - set version 1.6.3 + set version 1.6.5 }] return diff --git a/src/vendormodules/packageTest-0.1.0.tm b/src/vendormodules/packageTest-0.1.0.tm new file mode 100644 index 0000000000000000000000000000000000000000..befc864a37b6b5d2d3fd0025f89833f11c7ed5a4 GIT binary patch literal 8506 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 literal 0 HcmV?d00001 diff --git a/src/vendormodules/tablelist-6.22.tm b/src/vendormodules/tablelist-6.22.tm new file mode 100644 index 0000000000000000000000000000000000000000..50b012e8b766d7f5e46ef39bd0f9b40fbe04d5b3 GIT binary patch literal 286547 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

nY|Se773 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 z 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 NoD2-np}0RRE~?JocT literal 0 HcmV?d00001 diff --git a/src/vendormodules/tablelist_tile-6.22.tm b/src/vendormodules/tablelist_tile-6.22.tm new file mode 100644 index 00000000..455d33fb --- /dev/null +++ b/src/vendormodules/tablelist_tile-6.22.tm @@ -0,0 +1 @@ +source [file dirname [info script]]/tablelist-6.22.tm diff --git a/src/vendormodules/textutil/wcswidth-35.2.tm b/src/vendormodules/textutil/wcswidth-35.2.tm index a8afafeb..d153744a 100644 --- a/src/vendormodules/textutil/wcswidth-35.2.tm +++ b/src/vendormodules/textutil/wcswidth-35.2.tm @@ -8,7 +8,7 @@ # Author: Sean Woods # Author: Andreas Kupries ### -package require Tcl 8.5 +package require Tcl 8.5- package provide textutil::wcswidth 35.2 namespace eval ::textutil {} diff --git a/src/vendormodules_tcl8/include_modules.config b/src/vendormodules_tcl8/include_modules.config new file mode 100644 index 00000000..f080c8b1 --- /dev/null +++ b/src/vendormodules_tcl8/include_modules.config @@ -0,0 +1,11 @@ + +set local_modules [list\ + c:/repo/jn/tclmodules/Thread/modules_tcl8 Thread\ + c:/repo/jn/tclmodules/Thread/modules_tcl8 Thread::platform::win32_x86_64_tcl8\ +] + +set fossil_modules [dict create\ +] + +set git_modules [dict create\ +] \ No newline at end of file diff --git a/src/vendormodules_tcl9/Thread-3.0b3.tm b/src/vendormodules_tcl9/Thread-3.0b3.tm new file mode 100644 index 0000000000000000000000000000000000000000..cfc398d60025753124b122695fb7e27844f76baa GIT binary patch literal 7175 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 literal 0 HcmV?d00001 diff --git a/src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm b/src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm new file mode 100644 index 0000000000000000000000000000000000000000..9729945ffefa25946c85975bb1bb120830cf396b GIT binary patch literal 209406 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%0DI5-io;qq002rG{(W7pmsE195~^ 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