Browse Source

update bootsupport,project_layouts,vfs

master
Julian Noble 2 months ago
parent
commit
50de46ff65
  1. 10
      src/bootsupport/modules/punk-0.1.tm
  2. 1
      src/bootsupport/modules/punk/aliascore-0.1.0.tm
  3. 2
      src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm
  4. 4
      src/bootsupport/modules/punk/char-0.1.0.tm
  5. 2
      src/bootsupport/modules/punk/config-0.1.tm
  6. 18
      src/bootsupport/modules/punk/du-0.1.0.tm
  7. 373
      src/bootsupport/modules/punk/lib-0.1.6.tm
  8. 39
      src/bootsupport/modules/punk/mix/base-0.1.tm
  9. 32
      src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  10. 16
      src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm
  11. 30
      src/bootsupport/modules/punk/mix/util-0.1.0.tm
  12. 5
      src/bootsupport/modules/punk/ns-0.1.0.tm
  13. 23
      src/bootsupport/modules/punk/packagepreference-0.1.0.tm
  14. 133
      src/bootsupport/modules/punk/repl-0.1.2.tm
  15. 34
      src/bootsupport/modules/punk/repo-0.1.1.tm
  16. 20
      src/bootsupport/modules/punkcheck-0.1.0.tm
  17. 444
      src/bootsupport/modules/shellfilter-0.2.2.tm
  18. 177
      src/bootsupport/modules/shellthread-1.6.2.tm
  19. 59
      src/bootsupport/modules/textblock-0.1.3.tm
  20. BIN
      src/bootsupport/modules/zipper-0.14.tm
  21. 10
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm
  22. 1
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm
  23. 2
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm
  24. 4
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/char-0.1.0.tm
  25. 2
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm
  26. 18
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  27. 373
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  28. 39
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm
  29. 32
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  30. 16
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm
  31. 30
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm
  32. 5
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm
  33. 23
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm
  34. 133
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm
  35. 34
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm
  36. 20
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm
  37. 444
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm
  38. 177
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm
  39. 59
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm
  40. BIN
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zipper-0.14.tm
  41. 10
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm
  42. 1
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm
  43. 2
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm
  44. 4
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/char-0.1.0.tm
  45. 2
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm
  46. 18
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  47. 373
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  48. 39
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm
  49. 32
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  50. 16
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm
  51. 30
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm
  52. 5
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm
  53. 23
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm
  54. 133
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm
  55. 34
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm
  56. 20
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm
  57. 444
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm
  58. 177
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm
  59. 59
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm
  60. BIN
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zipper-0.14.tm
  61. 34
      src/project_layouts/vendor/punk/project-0.1/tclint.toml
  62. 39
      src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.tm
  63. 34
      src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm
  64. 3395
      src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm
  65. 3347
      src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm
  66. 829
      src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm
  67. 873
      src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm
  68. 1293
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl
  69. 3604
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html
  70. 4408
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html
  71. 8
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl
  72. 1951
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl
  73. BIN
      src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl24.dll

10
src/bootsupport/modules/punk-0.1.tm

@ -26,13 +26,11 @@ namespace eval punk {
#we call tcl::tm::list to trigger the initial set of tm paths before
#we can override it, otherwise our changes will be lost
#REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
return "\
apply {{ap tmlist} {
set ::auto_path \$ap
list apply {{ap tmlist} {
set ::auto_path $ap
tcl::tm::list
set ::tcl::tm::paths \$tmlist
}} {$::auto_path} {[tcl::tm::list]}
"
set ::tcl::tm::paths $tmlist
}} $::auto_path [tcl::tm::list]
}

1
src/bootsupport/modules/punk/aliascore-0.1.0.tm

@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore {
set aliases [tcl::dict::create {*}{
val ::punk::pipe::val
tstr ::punk::args::lib::tstr
FOR ::punk::lib::FOR
list_as_lines ::punk::lib::list_as_lines
lines_as_list ::punk::lib::lines_as_list
linelist ::punk::lib::linelist

2
src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm

@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates {
#we will ignore any .tm files that don't have versions that tcl understands - but warn
#this reduces the cases we have to test later
set fname [file tail $tf]
lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver
lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver
if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} {
puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)"
if {[string match *-* $mname]} {

4
src/bootsupport/modules/punk/char-0.1.0.tm

@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char {
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
#if {![regexp {[\u100-\U10FFFF]} $text]} {
# return [tcl::string::length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
if {![regexp "\[\u100-\U10FFFF\]" $text]} {
return [tcl::string::length $text]
#punk::char::wcswidth has to split and examine dec value of each code
#By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW

2
src/bootsupport/modules/punk/config-0.1.tm

@ -563,7 +563,7 @@ tcl::namespace::eval punk::config {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::config::show
@cmd -name punk::config::get -help\
@cmd -name punk::config::show -help\
"Display configuration values from a config.
Accepts globs eg XDG*"
@leaders -min 1 -max 1

18
src/bootsupport/modules/punk/du-0.1.0.tm

@ -1947,15 +1947,15 @@ namespace eval punk::du {
#e.g we can populate compsizes for files (compressed size)
proc du_dirlisting_zipfs {folderpath args} {
puts stderr "zipfs: $folderpath"
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

373
src/bootsupport/modules/punk/lib-0.1.6.tm

@ -2011,6 +2011,7 @@ namespace eval punk::lib {
#for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution.
#---------------------------------
proc tclscript_info {script {nscontext ""}} {
package require parser
#if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing.
if {[punk::ansi::ta::detect $script]} {
#we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts.
@ -3414,85 +3415,10 @@ namespace eval punk::lib {
return [list $stdout $stderr 0]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] "
}
set argspec [string map [list %sep% $sep] {
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "%sep%"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
#puts stderr "$argspec"
set argd [punk::args::parse $args withdef $argspec]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
@ -3532,6 +3458,77 @@ namespace eval punk::lib {
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "${$DYN_SEP}"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
set argd [punk::args::parse $args withid ::punk::lib::pdict]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@dynamic
@ -4935,6 +4932,23 @@ namespace eval punk::lib {
the range will come out the same, so the result needs to be treated as a 1-based
set of indices when performing further operations.
"
-return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels {
indices
" return a list of all indices in the order specified by the indexset,
with duplicates if specified by the indexset.
So for example
indexset_resolve 6 3..0,2,4,end
would return 3 2 1 0 2 4 5"
pairs
" return a list of index pairs representing the start and end of each range,
which may be increasing or decreasing, or just a single index
(where start and end are the same).
So for example
indexset_resolve -return pairs 6 3..0,2,4,end
would return {3 0} {2 2} {4 5}
indexset_resolve -return pairs 7 3..0,2,4,end
would return {3 0} {2 2} {4 4} {6 6}"
}
@values -min 2 -max 3
numitems -type integer
indexset -type indexset -help "comma delimited specification for indices to return"
@ -4948,32 +4962,66 @@ namespace eval punk::lib {
# for the unhappy path - the punk::args::parse is fine to generate the usage/error information.
# --------------------------------------------------
if {[llength $args] < 2} {
#too few args - use parser to generate error message
punk::args::resolve $args withid ::punk::lib::indexset_resolve
}
set indexset [lindex $args end]
set numitems [lindex $args end-1]
set indexset [lindex $args end]
if {![string is integer -strict $numitems] || ![is_indexset $indexset]} {
#use parser on unhappy path only
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
#assert we have 2 or more args
set optlist [lrange $args 0 end-2]
if {[llength $optlist] % 2 != 0} {
#options should come in pairs
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype "indices" ;#default
set base 0 ;#default
if {[llength $args] > 2} {
#if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
if {[llength $args] != 4} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set optname [lindex $args 0]
set optval [lindex $args 1]
set fulloptname [tcl::prefix::match -error "" -base $optname]
if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
dict for {opt val} $optlist {
set fulloptname [tcl::prefix::match -error "" {-base -return} $opt]
switch -exact -- $fulloptname {
-return {
set fullval [tcl::prefix::match -error "" {indices pairs} $val]
if {$fullval ni {indices pairs}} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype $fullval
}
-base {
if {![string is integer -strict $val]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set base $val
}
default {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
}
set base $optval
}
#set base 0 ;#default
#if {[llength $args] > 2} {
# #if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
# if {[llength $args] != 4} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set optname [lindex $args 0]
# set optval [lindex $args 1]
# set fulloptname [tcl::prefix::match -error "" -base $optname]
# if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set base $optval
#}
# --------------------------------------------------
@ -5160,8 +5208,84 @@ namespace eval punk::lib {
}
}
}
if {$returntype eq "pairs"} {
return [indices_to_pairs $index_list]
}
return $index_list
}
proc indices_to_pairs {indices} {
#convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing.
#if the direction of the run changes - we end the previous run and start a new one
if {[llength $indices] == 0} {
return [list]
}
set pairs [list]
set start [lindex $indices 0]
set prev $start
set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing
for {set i 1} {$i < [llength $indices]} {incr i} {
set idx [lindex $indices $i]
if {![string is integer -strict $idx]} {
error "non-integer index '$idx' in indices list"
}
if {$idx == $prev + 1} {
#increase since prev
if {$direction == 0} {
set direction 1
} elseif {$direction == -1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
} else {
#still increasing
}
set prev $idx
} elseif {$idx == $prev - 1} {
#decrease since prev
if {$direction == 0} {
set direction -1
} elseif {$direction == 1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
}
set prev $idx
} else {
#run ended - add pair to list
lappend pairs [list $start $prev]
set start $idx
set prev $idx
set direction 0
}
}
# add final run
lappend pairs [list $start $prev]
return $pairs
}
#proc indices_to_pairs {indices} {
# #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices
# set pairs [list]
# set start [lindex $indices 0]
# set prev $start
# for {set i 1} {$i < [llength $indices]} {} {
# set idx [lindex $indices $i]
# if {$idx == $prev + 1} {
# #still in a run
# set prev $idx
# } else {
# #run ended - add pair to list
# lappend pairs [list $start $prev]
# set start $idx
# set prev $idx
# }
# }
# # add final run
# lappend pairs [list $start $prev]
# return $pairs
#}
# showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side
#This doesn't need the list itself - just the length suffices.
punk::args::define {
@ -7835,6 +7959,77 @@ namespace eval punk::lib {
}
}
#sugar
#exclusive end - more intuitive for some cases and more consistent with other languages
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::lib::FOR
@cmd -name punk::lib::FOR\
-summary\
"BASIC style integer 'for loop'"\
-help\
"Sugar syntax for a common looping pattern.
The loop variable takes on values from start to end (exclusive) in increments of step.
If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end.
This is a common looping pattern that isn't directly supported by Tcl's built in control structures,
and this syntax is more concise for convenient interactive usage. For example:
FOR i 0 10 {puts $i}
will print the numbers 0 to 9.
This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops,
so may not be suitable for performance critical inner loops.
See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop
"
@values -min 3 -max 4
varname -type string -help "loop variable name"
start -type integer -help "initial value for loop variable"
end -type integer -help "end value for loop variable (exclusive)"
step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)"
script -type script -help "script to execute for each loop iteration"
}]
}
proc FOR { var args } {
switch -- [llength $args] {
3 {
# FOR x start end {}
lassign $args start end script
set step [ expr {$start > $end ? - 1 : 1} ]
}
4 {
# FOR x start end step {}
lassign $args start end step script
}
default {
error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script"
}
}
if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} {
error "FOR: start,end and step values must be integers"
}
upvar $var loopVar
set loopVar [expr {$start - $step}]
#to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition
if {$start < $end} {
if {$step <= 0} {
error "FOR: step value must be positive when start < end"
}
while {[incr loopVar $step] < $end} {
uplevel $script
}
} else {
if {$step >= 0} {
error "FOR: step value must be negative when start > end"
}
while {[incr loopVar $step] > $end} {
uplevel $script
}
}
}
#review - there are various type of uuid - we should use something consistent across platforms
#twapi is used on windows because it's about 5 times faster - but is this more important than consistency?
#twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway

39
src/bootsupport/modules/punk/mix/base-0.1.tm

@ -677,7 +677,7 @@ namespace eval punk::mix::base {
if {$opt_use_tar != 0} {
set target [file tail $path]
set tmplocation [punk::mix::util::tmpdir]
set archivename $tmplocation/[punk::mix::util::tmpfile].tar
set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file.
cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues)
@ -687,14 +687,38 @@ namespace eval punk::mix::base {
set tsstart [clock millis]
if {[set tarpath [auto_execok tar]] ne ""} {
#using an external binary is *significantly* faster than tar::create - but comes with some risks
#review - need to check behaviour/flag variances across platforms
set versioninfo [exec {*}$tarpath --version]
#look for "bsdtar" vs "GNU"
#GNU tar is more common on linux - but also available on windows via gnuutils or msys
#<system32>/tar.exe on windows is likely to be bsdtar.
#GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag
#review - need to further check behaviour/flag variances across platforms
#don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum
#also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?)
exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " tar -cf done ($ms ms)"
} else {
if {[string match "*GNU*" $versioninfo]} {
set flags "--force-local"
} else {
#presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths
set flags ""
}
if {[catch {
#{*}$tarpath needed in case spaces in tarpath
exec {*}$tarpath -cf {*}$flags $archivename $target
} errMsg]} {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg"
} else {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' done ($ms ms)"
}
}
if {![file exists $archivename]} {
#fallback to tar library approach if external tar failed to create the archive.
set tsstart [clock millis] ;#don't include auto_exec search time for tar::create
tar::create $archivename $target
set tsend [clock millis]
@ -703,6 +727,7 @@ namespace eval punk::mix::base {
puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing"
}
if {$ftype eq "file"} {
set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)"
} else {

32
src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -333,11 +333,11 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-errorprefix projectname
}]
if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"}
if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"}
set known_opts [dict keys $defaults]
foreach k [dict keys $args] {
if {$k ni $known_opts} {
error "validate_modulename error: unknown option $k. known options: $known_opts"
error "validate_projectname error: unknown option $k. known options: $known_opts"
}
}
set opts [dict merge $defaults $args]
@ -381,34 +381,6 @@ namespace eval punk::mix::cli {
return $name
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
#
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
proc get_status {{workingdir ""} args} {
set result ""

16
src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm

@ -9,7 +9,7 @@
# @@ Meta Begin
# Application punk::mix::commandset::module 0.1.0
# Meta platform tcl
# Meta license BSD
# Meta license BSD
# @@ Meta End
@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module {
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_version_supplied [dict get $opts -version]
set opt_version $opt_version_supplied
if {![util::is_valid_tm_version $opt_version]} {
if {![punk::mix::util::is_valid_tm_version $opt_version]} {
error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version"
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module {
set mversion_supplied "" ;#version supplied directly in module argument
if {[string first - $module]> 0} {
#if it has a dash then version is required to be valid
lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion
if {![util::is_valid_tm_version $mversion]} {
lassign [punk::mix::util::split_modulename_version $module] modulename mversion
if {![punk::mix::util::is_valid_tm_version $mversion]} {
error "deck module.new error - unable to determine modulename-version from supplied value '$module'"
}
set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name
@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module {
} else {
set module [string range [string length $vendor.] end]
}
lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion
lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion
lappend key_version_list [list $m $mversion]
}
if {[llength $matches]} {
@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module {
} else {
error "module.new error: Unable to interpret filename components of template file '$templatefile'"
}
lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version
lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version
#t_version may be empty string if template is unversioned e.g template_whatever.tm
set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd
@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module {
} else {
#
if {[util::is_valid_tm_version $t_version]} {
if {[punk::mix::util::is_valid_tm_version $t_version]} {
if {$mversion_supplied eq ""} {
set build_version $t_version
} else {
@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module {
set name_version_pairs [list]
lappend name_version_pairs [list $moduletail $infile_version]
foreach existing $existing_versions {
lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
if {[string match #modpod-* $namepart]} {
set namepart [string range $namepart 8 end]
}

30
src/bootsupport/modules/punk/mix/util-0.1.0.tm

@ -330,6 +330,35 @@ namespace eval punk::mix::util {
return 0
}
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
#Note that semver only has a small overlap with tcl tm versions.
#todo - work out what overlap and whether it's even useful
#see also TIP #439: Semantic Versioning (tcl 9??)
@ -337,6 +366,7 @@ namespace eval punk::mix::util {
set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$}
}
#todo - semver conversion/validation for other systems?
proc magic_tm_version {} {
set magicbase 999999 ;#deliberately large so given load-preference when testing!
#we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version

5
src/bootsupport/modules/punk/ns-0.1.0.tm

@ -4186,7 +4186,7 @@ y" {return quirkykeyscript}
#this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces)
#we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace.
nseval $targetns [list apply [list {procname argname} {
set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]]
set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]]
if {$has_default} {
set answer [dict create exists 1 default $::punk::ns::corp_defvar]
} else {
@ -5025,7 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
#puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -7191,6 +7191,7 @@ y" {return quirkykeyscript}
nstest eval {package require punk::ns}
set ns ""
if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} {
#review
set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}]
set ns [nstest eval $script]
} else {

23
src/bootsupport/modules/punk/packagepreference-0.1.0.tm

@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference {
#[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names
#[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall
if {![catch {commandstack::get_stack} cstack]} {
if {[dict exists $cstack ::package]} {
set pstack [dict get $cstack ::package]
foreach record $pstack {
if {[dict get $record rename] eq "punk::packagepreference"} {
#already installed - silently ignore.
return 0
}
}
}
}
#todo - review/update commandstack package
#modern module/lib names should preferably be lower case
#see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9)
@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference {
if {[llength $pkgloadedinfo]} {
if {[llength $available_versions] > 1} {
puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
#puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid"
}
lassign $pkgloadedinfo loaded_path name
set lc_loadedpath [string tolower $loaded_path]
@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference {
puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg"
}
} else {
puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid"
}
}
#---------------------------------------------------------------

133
src/bootsupport/modules/punk/repl-0.1.2.tm

@ -432,7 +432,7 @@ proc repl::start {inchan args} {
variable codethread
#review
if {$codethread eq ""} {
error "start - no codethread. call init first. (options -safe 0|1)"
error "start - no codethread. call init first. (options -type 0|1)"
}
variable commandstr
@ -478,7 +478,7 @@ proc repl::start {inchan args} {
#set ::punk::repl::codethread::running 1
#the interp in which commands such as d/ run
#we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values
#we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values
#punk::repl::codethread::running is required whether safe or not.
interp eval code {
namespace eval ::punk::repl::codethread {}
@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} {
}
#add to sliding buffer of last x chars emmitted to screen by repl
# add to sliding buffer of last x chars emmitted to screen by repl
#(we could maintain only one char - more kept merely for debug assistance)
#will not detect emissions from exec with stdout redirected and presumably some extensions etc
proc repl::screen_last_char_add {c what {why ""}} {
@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#if {$chunk eq "\x1b\[C"} {
#}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_off
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
$editbuf add_chunk $chunk
@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
lappend input_chunks_waiting($inputchan) $waiting
}
}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_on
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
if {$editbuf_linenum_submitted == 0} {
#(there is no line 0 - lines start at 1)
@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap1 POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error1 in repl_handler: $repl_error"
rputs stderr "error1 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "erroropts: $erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
variable codethread_cond
variable codethread_mutex
lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#JJJJ JMN test
#REVIEW
#lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#thread::transfer $codethread stderr
#chan configure stdout -buffering none
@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error in repl_handler: $repl_error"
rputs stderr "error2 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "$erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2986,10 +2996,10 @@ namespace eval repl {
variable codethread_cond
variable codethread_mutex
set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
foreach {k v} $args {
switch -- $k {
-force - -safe - -safelog - -paths - -callback_interp {
-force - -type - -safelog - -paths - -callback_interp {
dict set opts $k $v
}
default {
@ -2998,7 +3008,7 @@ namespace eval repl {
}
}
set opt_force [dict get $opts -force]
set opt_safe [dict get $opts -safe]
set opt_repltype [dict get $opts -type]
set opt_safelog [dict get $opts -safelog]
if {$opt_safelog eq "0"} {
set opt_safelog ""
@ -3113,46 +3123,50 @@ namespace eval repl {
package require punk::args
#package require Thread
if {[catch {package require thread} errM]} {
puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
#if {[catch {package require thread} errM]} {
# puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
if {[catch {package require Thread} errM2]} {
puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2"
puts stdout ">>repl::init auto_path : $::auto_path"
puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]"
}
}
#}
namespace eval ::punk::repl::codethread {}
# -------------------------------------------------------------------------------------------------------------------------------------------------------
#-----
#review - icomm as a possible way to talk to thread outside of the code interp.
#thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow
#thread::send to caller defined interp targets (reference?)
#snit required for icomm
if {[catch {package require snit} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
}
if {[catch {package require punk::icomm} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
}
#if {[catch {package require snit} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
#}
#if {[catch {package require punk::icomm} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
#}
#-----
namespace eval ::punk::repl::codethread {}
#todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
catch {package require tcl::chan::fifo2}
if {[catch {
#first use can raise error being a version number e.g 0.1.0 - why?
lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
} errMsg]} {
puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
} else {
#experimental?
#puts stdout "transferring chan $replside to thread %replthread%"
#flush stdout
#if {[catch {
# #after 0 [list thread::transfer %replthread% $replside]
#} errMsg]} {
# #puts stdout "---thread::transfer error: $errMsg"
#}
}
#----------------------------------------------------------------------------------------------------------------------
# todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
# catch {package require tcl::chan::fifo2}
# if {[catch {
# #first use can raise error being a version number e.g 0.1.0 - why?
# lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
# } errMsg]} {
# puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
# } else {
# #experimental?
# #puts stdout "transferring chan $replside to thread %replthread%"
# #flush stdout
# # if {[catch {
# # #after 0 [list thread::transfer %replthread% $replside]
# # } errMsg]} {
# # #puts stdout "---thread::transfer error: $errMsg"
# # }
# }
#----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------------------------------------------------------------------
package require punk::console
package require punk::repl::codethread
@ -3333,7 +3347,7 @@ namespace eval repl {
set ts_start [clock seconds]
set replresult [interp eval code {
package require punk::repl
repl::init -safe punk
repl::init -type punk
repl::start stdin
}]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3343,7 +3357,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe safe {*}$args]
interp eval code [list repl::init -type safe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3353,7 +3367,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
set codethread [interp eval code [list repl::init -safe safebase {*}$args]]
set codethread [interp eval code [list repl::init -type safebase {*}$args]]
puts stdout "safebase codethread:$codethread"
set replresult [interp eval code [list repl::start stdin]]
@ -3364,7 +3378,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe punksafe {*}$args]
interp eval code [list repl::init -type punksafe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3376,14 +3390,14 @@ namespace eval repl {
#flush stdout
set args %args%
set safe [dict get $args -safe]
set repltype [dict get $args -type]
set safelog [dict get $args -safelog]
set paths [list]
if {[dict exists $args -paths]} {
set paths [dict get $args -paths]
}
switch -- $safe {
switch -- $repltype {
safe {
interp create -safe -- code
code eval [list namespace eval ::punk::libunknown {}]
@ -3444,7 +3458,7 @@ namespace eval repl {
#todo a specific punk::libunknown 'package unknown' handler for safe interps
#pull in code via calls to source cached code?
switch -- $safe {
switch -- $repltype {
safe {
if {[llength $paths]} {
package require punk::island
@ -3797,8 +3811,27 @@ namespace eval repl {
}
punk - 0 {
#------------------------------------------------------------------------
#Test - experimental 2026
if {"stdout" in [chan names]} {
interp share {} stdout code
} else {
interp share {} [shellfilter::stack::item_tophandle stdout] code
}
if {"stderr" in [chan names]} {
interp share {} stderr code
} else {
interp share {} [shellfilter::stack::item_tophandle stderr] code
}
foreach nm {shellspyout shellspyerr} {
set thandle [shellfilter::stack::item_tophandle $nm]
if {$thandle ne ""} {
interp share {} $thandle code
}
}
#------------------------------------------------------------------------
interp eval code {
#safe !=1 and safe !=2, tmlist: %tmlist%
#repltype !=1 and repltype !=2, tmlist: %tmlist%
set ::argv0 %argv0%
set ::argv %argv%
set ::argc %argc%
@ -3978,8 +4011,8 @@ namespace eval repl {
thread::id
}
set init_script [string map $scriptmap $init_script]
#REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args%
#we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that?
#REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args%
#we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that?
#thread::send $codethread $init_script
if {![catch {
@ -3994,7 +4027,7 @@ namespace eval repl {
error $errMsg
}
}
#init - don't auto init - require init with possible options e.g -safe
#init - don't auto init - require init with possible options e.g -type
}
package provide punk::repl [namespace eval punk::repl {
variable version

34
src/bootsupport/modules/punk/repo-0.1.1.tm

@ -83,34 +83,38 @@ namespace eval punk::repo {
proc get_fossil_usage {} {
set allcmds [runout -n fossil help -a]
set allcmds [punk::ansi::ansistrip $allcmds]
set mainhelp [runout -n fossil help]
set mainhelp [punk::ansi::ansistrip $mainhelp]
set maincommands [list]
#only start parsing for TOPICS after a line such as "Other comman values for TOPIC:"
set parsing_topics 0
foreach ln [split $mainhelp \n] {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
lappend maincommands {*}$ln
}
lappend maincommands {*}$ln
}
}
#fossil output was ordered in columns, but we loaded list in row-wise, messing up the order
set maincommands [lsort $maincommands]
set allcmds [lsort $allcmds]
set othercmds [punk::lib::ldiff $allcmds $maincommands]
set fossil_setting_names [lsort [runout -n fossil help -s]]
set setting_info [runout -n fossil help -s]
set setting_info [punk::ansi::ansistrip $setting_info]
set fossil_setting_names [lsort $setting_info]
set result "@leaders -min 0\n"
@ -186,6 +190,8 @@ namespace eval punk::repo {
foreach ln $basic_opt_lines {
set ln [string trim $ln]
#fossil sometimes emits cursor control sequences e.g CSI 3 q
set ln [punk::ansi::ansistrip $ln]
if {$ln eq ""} {
continue
}
@ -250,6 +256,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage add]}
@form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}}
} ""]
@ -264,6 +271,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage diff]}
@form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}}
} ""]

20
src/bootsupport/modules/punkcheck-0.1.0.tm

@ -121,14 +121,6 @@ namespace eval punkcheck {
}
method as_record {} {
#set fields [list\
# -targets $o_targets\
# -keep_installrecords $o_keep_installrecords\
# -keep_skipped $o_keep_skipped\
# -keep_inprogress $o_keep_inprogress\
# body $o_records\
#]
dict create {*}{
} tag FILEINFO {*}{
} -targets $o_targets {*}{
@ -216,18 +208,6 @@ namespace eval punkcheck {
} else {
set tsiso_end ""
}
#set fields [list\
# -tsiso_begin $tsiso_begin\
# -ts_begin $o_ts_begin\
# -tsiso_end $tsiso_end\
# -ts_end $o_ts_end\
# -id $o_id\
# -source $o_rel_sourceroot\
# -targets $o_rel_targetroot\
# -types $o_types\
# -config $o_configdict\
#]
#set record [dict create tag EVENT {*}$fields]
dict create {*}{
} tag EVENT {*}{

444
src/bootsupport/modules/shellfilter-0.2.2.tm

@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log {
}
proc ::shellfilter::log::close {tag} {
#shellthread::manager::close_worker $tag
#unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag.
shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed
}
@ -106,6 +108,13 @@ namespace eval shellfilter::pipe {
package require shellthread
#we are only using the fifo in a single direction to pipe to another thread
# - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each
#----------------------------------------------------------------------------------------
# Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations:
# (not necessarily a comprehensive list)
# - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms
# tcl::chan::fifo2 is a pure Tcl implementation.
# - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2.
#----------------------------------------------------------------------------------------
if {![catch {package require Memchan}]} {
lassign [fifo2] wchan rchan
} else {
@ -289,7 +298,8 @@ namespace eval shellfilter::chan {
set o_enc [tcl::dict::get $tf -encoding]
set o_encbuf ""
set settingsdict [tcl::dict::get $tf -settings]
set varname [tcl::dict::get $settingsdict -varname]
set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them?
#these are not upvared - must be fully qualified variable names.
set o_datavars $varname
if {[tcl::dict::exists $tf -junction]} {
set o_is_junction [tcl::dict::get $tf -junction]
@ -298,18 +308,18 @@ namespace eval shellfilter::chan {
}
}
method initialize {ch mode} {
return [list initialize finalize write flush clear]
if {"read" in $mode} {
#this should raise an error in 'chan push'
error "shellfilter::chan::tee_to_var transform does not support read mode"
}
return [list initialize finalize write flush]
}
method finalize {ch} {
my destroy
}
method clear {ch} {
return
}
method watch {ch events} {
# must be present but we ignore it because we do not
# post any events
}
#method clear {ch} {
# return
#}
#method read {ch count} {
# return ?
#}
@ -320,9 +330,14 @@ namespace eval shellfilter::chan {
#puts stdout "<flush>"
#review - just clear o_encbuf and emit nothing?
#we wouldn't have a value there if it was convertable from the channel encoding?
set clear $o_encbuf
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {ch bytes} {
#test with set x [string repeat " \U1f6c8" 2043]
@ -387,10 +402,12 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read drain write flush clear finalize]
#return [list initialize read drain write flush clear finalize]
return [list initialize write flush clear finalize]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#Note that an error in the finalize can stop 'chan pop' from running properly.
#::shellfilter::log::close $o_logsource
my destroy
}
method watch {transform_handle events} {
@ -398,26 +415,53 @@ namespace eval shellfilter::chan {
# post any events
}
method clear {transform_handle} {
set o_encbuf ""
return
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set logdata [tcl::encoding::convertfrom $o_enc $bytes]
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $logdata
return $bytes
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set logdata [tcl::encoding::convertfrom $o_enc $bytes]
# #::shellfilter::log::write $o_logsource $logdata
# puts -nonewline $o_localchan $logdata
# return $bytes
#}
#method flush {transform_handle} {
# #return ""
# set clear $o_encbuf[set o_encbuf ""]
# if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
# #if we can't convert the buffer contents to a string - put it back and try again with more data later
# #REVIEW?
# set o_encbuf $clear
# puts -nonewline $o_localchan ""
# return ""
# }
# #jjj
# puts -nonewline $o_localchan $stringdata
# flush $o_localchan
# return $clear
#}
method flush {transform_handle} {
#return ""
set clear $o_encbuf
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {transform_handle bytes} {
#set logdata [tcl::encoding::convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
if {$inputbytes eq ""} {
#review - do we even get empty writes?
puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write."
return ""
}
set o_encbuf ""
set tail_offset 0
while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} {
@ -426,18 +470,23 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
#nothing convertable in the buffer - put it back and try again with more data later
set stringdata ""
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $stringdata
#return $bytes
return [::tcl::string::range $inputbytes 0 end-$tail_offset]
return $converted ;#same data as $stringdata but in bytes form.
}
#a tee is not a redirection - because data still flows along the main path
method meta_is_redirection {} {
@ -501,6 +550,7 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
@ -508,10 +558,15 @@ namespace eval shellfilter::chan {
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
#set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
::shellfilter::log::write $o_logsource $stringdata
return $bytes
#return $bytes
return $converted ;#same data as $stringdata but in bytes form.
}
method meta_is_redirection {} {
return $o_is_junction
@ -519,6 +574,10 @@ namespace eval shellfilter::chan {
}
#see TIP 230 Tcl Channel Transformation Reflection API
#the logonly transform does not emit any data downwards towards the base channel - it only writes to the log.
# - ie we are redirecting all data to the log.
oo::class create logonly {
variable o_tid
variable o_logsource
@ -537,19 +596,35 @@ namespace eval shellfilter::chan {
set o_tid [::shellfilter::log::open $o_logsource $settingsdict]
}
method initialize {transform_handle mode} {
return [list initialize finalize write]
#mode is a list containing any of the strings 'read' or 'write'.
if {"read" in $mode} {
#Note that raising an error prevents creation of the transformation.
#The thrown error will appear as a error thrown by 'chan push'.
error "logonly transform does not support read mode"
}
#return all methods supported by the handler
#(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter)
return [list initialize finalize write flush]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#review - tip 230 states that any return value or error raised by finalize is ignored
#we wrap in catch to ensure the 'my destroy' is always called.
catch {::shellfilter::log::close $o_logsource}
my destroy
}
method watch {transform_handle events} {
# must be present but we ignore it because we do not
# post any events
#clear?
method flush {transform_handle} {
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log?
#REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string.
#This may be useful for debugging issues, but it may also result in garbage data in the log.
::shellfilter::log::write $o_logsource $o_encbuf
set o_encbuf ""
}
return
}
#method read {transform_handle count} {
# return ?
#}
method write {transform_handle bytes} {
#set logdata [encoding convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
@ -600,26 +675,49 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read write clear flush drain finalize]
#return [list initialize read write clear flush drain finalize]
#REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work.
#this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel.
#----------------------
#don't do this
#----------------------
#if {"read" in $mode} {
# #Note that raising an error prevents creation of the transformation.
# #The thrown error will appear as a error thrown by 'chan push'.
# error "shellfilter::chan::ansistrip transform does not support read mode"
#}
#----------------------
return [list initialize read write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method clear {transform_handle} {
return
}
method watch {transform_handle events} {
}
method drain {transform_handle} {
return ""
}
#method clear {transform_handle} {
# return
#}
#method drain {transform_handle} {
# return ""
#}
method read {transform_handle bytes} {
set instring [encoding convertfrom $o_enc $bytes]
set outstring [punk::ansi::ansistrip $instring]
return [encoding convertto $o_enc $outstring]
}
#method flush {transform_handle} {
# return ""
#}
method flush {transform_handle} {
return ""
#return ""
set clear $o_encbuf[set o_encbuf ""]
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - put it back and try again with more data later
#REVIEW?
set o_encbuf $clear
return ""
}
#review
return $stringdata
}
#method write {transform_handle bytes} {
# #broken due to occasional unexpected byte sequence
@ -989,29 +1087,44 @@ namespace eval shellfilter::chan {
}
method initialize {transform_handle mode} {
#clear undesirable in terminal output channels (review)
return [list initialize write flush read drain finalize]
#return [list initialize write flush read drain finalize]
if {$mode eq "read"} {
error "shellfilter::chan::ansiwrap channel transform does not support read mode"
}
return [list initialize write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method watch {transform_handle events} {
}
method clear {transform_handle} {
#In the context of stderr/stdout - we probably don't want clear to run.
#Terminals might call it in the middle of a split ansi code - resulting in broken output.
#Leave clear of it the init call
#Leave clear out of the initialize call for now
puts stdout "<clear>"
set emit [tcl::encoding::convertto $o_enc $o_buffered]
set o_buffered ""
return $emit
}
#method flush {transform_handle} {
# #puts stdout "<flush>"
# set inputbytes $o_buffered$o_encbuf
# set emit [tcl::encoding::convertto $o_enc $inputbytes]
# set o_buffered ""
# set o_encbuf ""
# return $emit
#}
method flush {transform_handle} {
#puts stdout "<flush>"
set inputbytes $o_buffered$o_encbuf
set emit [tcl::encoding::convertto $o_enc $inputbytes]
#return ""
set clear $o_buffered$o_encbuf
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes?
# - probably not.
#REVIEW?
return ""
}
set o_buffered ""
set o_encbuf ""
return $emit
return $stringdata
}
method write {transform_handle bytes} {
#set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally
@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan {
#set outstring ">>>$instring"
return [tcl::encoding::convertto $o_enc $outstring]
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set instring [tcl::encoding::convertfrom $o_enc $bytes]
set outstring "$o_do_colour$instring$o_do_normal"
return [tcl::encoding::convertto $o_enc $outstring]
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set instring [tcl::encoding::convertfrom $o_enc $bytes]
# set outstring "$o_do_colour$instring$o_do_normal"
# return [tcl::encoding::convertto $o_enc $outstring]
#}
method meta_is_redirection {} {
return $o_is_junction
}
@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack {
}
proc status {{pipename *} args} {
variable pipelines
package require textblock
set pipecount [dict size $pipelines]
set tabletitle "$pipecount pipelines active"
set t [textblock::class::table new $tabletitle]
$t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts.
$t add_column -headers [list channel-ident]
$t add_column -headers [list device-info localchan]
$t configure_column 1 -header_colspans {3}
@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack {
$t add_column -headers [list stack-info]
foreach k [dict keys $pipelines $pipename] {
set lc [dict get $pipelines $k device localchan]
if {[catch {chan configure $lc -encoding} lc_enc]} {
set lc_enc "<err>"
}
set rc [dict get $pipelines $k device remotechan]
if {[dict exists $k device workertid]} {
set tid [dict get $pipelines $k device workertid]
@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack {
set stackinfo ""
} else {
set tbl_inner [textblock::class::table new]
$tbl_inner configure -frametype ascii
$tbl_inner configure -show_edge 0
$tbl_inner add_column -headers id
$tbl_inner add_column -headers transform
$tbl_inner add_column -headers handle
$tbl_inner add_column -headers settings
$tbl_inner add_column -headers aside
foreach rec $stack {
set handle [punk::lib::dict_getdef $rec -handle ""]
set id [punk::lib::dict_getdef $rec -id ""]
set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]]
set handle [punk::lib::dict_getdef $rec -handle ""]
if {$handle ne ""} {
if {[catch {chan configure $handle -encoding} handle_enc]} {
set handle_enc "<err>"
}
} else {
set handle_enc ""
}
set settings [punk::lib::dict_getdef $rec -settings ""]
$tbl_inner add_row [list $id $transform $handle $settings]
set aside [punk::lib::dict_getdef $rec -aside ""]
if {$aside ne ""} {
set aside [punk::lib::showdict $aside]
}
$tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside]
}
set stackinfo [$tbl_inner print]
$tbl_inner destroy
}
$t add_row [list $k $lc $rc $tid $stackinfo]
$t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo]
}
set result [$t print]
$t destroy
@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack {
proc unwind {pipename} {
variable pipelines
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
foreach tf [lreverse $stack] {
chan pop $localchan
if {[catch {chan eof $localchan} _eof]} {
#We don't actually care about eof state - but we use this to test if the channel exists.
#('chan names' doesn't reliably show all channels in some cases)
#do nothing.
} else {
#if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel
# but here we should only be calling it when there are transforms on the channel as indicated by the stack variable.
chan pop $localchan
}
}
dict set pipelines $pipename [list]
}
#todo
proc delete {pipename {wait 0}} {
#::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"]
variable pipelines
set pipeinfo [dict get $pipelines $pipename]
set deviceinfo [dict get $pipeinfo device]
@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack {
thread::release $tid
}
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why?
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"'
#- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed.
catch {chan close $localchan}
#if {[catch {chan close $localchan} errMsg]} {
# ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg"
#}
}
#review - proc name clarity is questionable. remove_stackitem?
proc remove {pipename remove_id} {
#::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"]
variable pipelines
if {![dict exists $pipelines $pipename]} {
puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
#puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
return
}
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set previous_blockingstate [chan configure $localchan -blocking]
if {$previous_blockingstate} {
chan configure $localchan -blocking 0
}
set posn 0
set idposn -1
set asideposn -1
@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack {
dict set container -aside {}
lset stack $asideposn $container
dict set pipelines $pipename stack $stack
#::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'"
} else {
if {$idposn < 0} {
::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
#::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
#puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
return 0
}
set removed_item [lindex $stack $idposn]
@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack {
#pop all chans before adding anything back in!
foreach p $poplist {
#review
#update idletasks
#puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]"
#::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---"
#after 0 [list chan pop $localchan]
chan pop $localchan
#::shellfilter::log::write shellfilter-remove "POPPED"
#update idletasks
}
#after 5
#::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
#puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
set obj [$t new $restore]
set h [chan push $localchan $obj]
dict set restore -handle $h
dict set restore -obj $obj
lappend stack $restore
#puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
}
#after 5
#put popped back except for the first one, which we want to remove
foreach p [lrange $poplist 1 end] {
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set obj [$t new $p]
set h [chan push $localchan $obj]
dict set p -handle $h
dict set p -obj $obj
lappend stack $p
#update idletasks
#puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
}
#after 5
dict set pipelines $pipename stack $stack
}
#puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE"
#::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE"
if {$previous_blockingstate} {
chan configure $localchan -blocking 1
}
#JMNJMN 2025 review!
#show_pipeline $pipename -note "after_remove $remove_id"
return 1
@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack {
namespace eval shellfilter {
variable sources [list]
variable stacks [dict create]
#-------------------------------------------------------------------------------------------------
#tcllib logger infrastructure.
#-------------------------------------------------------------------------------------------------
namespace eval ::shellfilter::loggerprocs {
#container for procs/aliases to be pointed to by tcllib logger using log::logproc
proc Dolog {lvl txt} {
#logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted
set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'"
puts stderr $msg
}
proc Runlog {lvl script} {
uplevel 1 $script
}
}
if {![catch {
package require logger
}]} {
logger::initNamespace ::shellfilter
foreach lvl [logger::levels] {
interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl
log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl
}
logger::setlevel warn
#namespace path ::shellfilter::log
} else {
#e.g tcllib not available, safe interp?
#fake out the logger calls
namespace eval ::shellfilter::log {
foreach lvl {debug info notice warn error critical alert emergency} {
proc $lvl {args} {}
}
}
}
#-------------------------------------------------------------------------------------------------
proc ::shellfilter::redir_channel_to_log {chan args} {
variable sources
@ -2396,13 +2613,9 @@ namespace eval shellfilter {
#must be a list. If it was a shell commandline string. convert it elsewhere first.
variable sources
set runtag "shellfilter-run"
#set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
set tid [::shellfilter::log::open $runtag [list -syslog ""]]
if {[catch {llength $commandlist} listlen]} {
set listlen "<not-a-tcl-list>"
}
::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"
#flush stdout
#flush stderr
@ -2416,6 +2629,7 @@ namespace eval shellfilter {
-errchan stderr
-inchan stdin
-tclscript 0
-syslog ""
}]
set opts [dict merge $defaults $args]
@ -2432,6 +2646,13 @@ namespace eval shellfilter {
set teehandle_err ${teehandle}err
set teehandle_in ${teehandle}in
set syslog [dict get $opts -syslog]
dict unset opts -syslog
set runtag "shellfilter-run"
set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
#set tid [::shellfilter::log::open $runtag [list -syslog $syslog]]
log::info {::shellfilter::log::write $runtag " opts: $opts"}
log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"}
#puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err"
@ -2441,19 +2662,32 @@ namespace eval shellfilter {
lappend sources $source
}
}
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
#set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device]
#set inpipechan [dict get $indeviceinfo localchan]
#---------------------
# #TEST
# chan configure $outpipechan -blocking 0
# chan configure $errpipechan -blocking 0
log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"}
log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"}
#---------------------
log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"}
#NOTE:These transforms are not necessarily at the top of each stack!
#The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit.
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
}
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2493,6 +2727,7 @@ namespace eval shellfilter {
set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr]
}
}
log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"}
} else {
if {[catch {
#script result
@ -2500,29 +2735,45 @@ namespace eval shellfilter {
} errMsg]} {
set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"]
}
log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"}
}
#puts "shellfilter::run finished call"
#-------------------------
#warning - without flush stdout - we can get hang, but only on some terminals
# - mechanism for this problem not understood!
#todo - test/document.
flush stdout
flush stderr
#update idletasks
#-------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
shellfilter::stack::remove stdout $id_out
shellfilter::stack::remove stderr $id_err
#shellfilter::stack::remove stderr $id_in
#puts stderr "shellfilter::run complete..."
#----------------------------------------------------------------------------------------------
# wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug.
# Logger allows the contents to be evaluated only if logging is switched on.
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#chan configure stderr -buffering line
#flush stdout
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
::shellfilter::log::write $runtag " return '$exitinfo'"
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
::shellfilter::log::close $runtag
return $exitinfo
}
@ -2552,6 +2803,7 @@ namespace eval shellfilter {
}
if {$close} {
lappend tidied_sources $s
#unsubscribe from source tag s.
shellfilter::log::close $s
lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s]
}

177
src/bootsupport/modules/shellthread-1.6.2.tm

@ -119,29 +119,58 @@ namespace eval shellthread::worker {
set waitvar ::shellthread::worker::wait($inpipe,[clock micros])
#tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan
chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
vwait $waitvar
}
proc pipe_read {chan source waitfor readbuffering writebuffering} {
#chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
if {$readbuffering eq "line"} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
}
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
} else {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
}
vwait $waitvar
}
#proc pipe_read {chan source waitfor readbuffering writebuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
# } else {
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# }
# } else {
# set chunk [chan read $chan]
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $chan
# }
#}
proc start_pipe_write {source writechan args} {
variable outpipe
@ -181,8 +210,8 @@ namespace eval shellthread::worker {
chan configure $writechan -blocking 0
set waitvar ::shellthread::worker::wait($outpipe,[clock micros])
chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
if {$readbuffering eq "line"} {
if {$readbuffering eq "line"} {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
@ -191,19 +220,55 @@ namespace eval shellthread::worker {
puts -nonewline $writechan $chunk
}
}
} else {
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
flush $writechan ;#2026-05-19 - ensure all data is sent before closing
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}} $readchan $writechan $source $waitvar]
} else {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunk [chan read $chan]
puts -nonewline $writechan $chunk
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}
}} $readchan $writechan $source $waitvar $readbuffering]
}} $readchan $writechan $source $waitvar]
}
# chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# puts $writechan $chunk
# } else {
# puts -nonewline $writechan $chunk
# }
# }
# } else {
# set chunk [chan read $chan]
# puts -nonewline $writechan $chunk
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $writechan
# if {$chan ne "stdin"} {
# chan close $chan
# }
# }
# }} $readchan $writechan $source $waitvar $readbuffering]
vwait $waitvar
}
@ -479,6 +544,10 @@ namespace eval shellthread::manager {
set sourcetag [lindex $sourcetaglist 0] ;#todo - use all
set defaults [dict create {*}{
-raw 0
-file {}
-syslog {}
-direction out
-workertype message
}]
set settingsdict [dict merge $defaults $settingsdict]
@ -501,6 +570,10 @@ namespace eval shellthread::manager {
return [dict get $winfo tid]
} elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} {
#review - magic dict seems brittle - shouldn't hard code here.???
#existing worker has default settings - so we'll assume it's a placeholder and update it with our settings
#review - where/when do we override the default settings?
dict lappend winfo list_client_tids $tidclient
dict set workers $sourcetag $winfo ;#writeback
return [dict get $winfo tid]
@ -575,11 +648,11 @@ namespace eval shellthread::manager {
package require Thread
package require shellthread
if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} {
unset ::settingsinfo
set ::shellthread_init "ok"
unset ::settingsinfo
set ::shellthread_init "ok"
} else {
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
}
}]
@ -622,15 +695,23 @@ namespace eval shellthread::manager {
proc write_log {source msg args} {
variable workers
set ts_micros_sent [clock micros]
set defaults [list -async 1 -level info]
set opts [dict merge $defaults $args]
if {[dict exists $workers $source]} {
if {[dict exists $workers $source tid]} {
set tidworker [dict get $workers $source tid]
if {$tidworker eq "noop"} {
return
}
} else {
set tidworker ""
}
set ts_micros_sent [clock micros]
set defaults [list {*}{
-async 1
-level info
}]
set opts [dict merge $defaults $args]
if {$tidworker ne ""} {
if {![thread::exists $tidworker]} {
# -syslog -file ?
set tidworker [new_worker $source]
@ -674,10 +755,7 @@ namespace eval shellthread::manager {
if {[dict exists $workers $source]} {
set list_client_tids [dict get $workers $source list_client_tids]
if {[set posn [lsearch $list_client_tids $mytid]] >= 0} {
#set list_client_tids [lreplace $list_client_tids $posn $posn]
#set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn]
ledit list_client_tids $posn $posn
dict set workers $source list_client_tids $list_client_tids
}
if {![llength $list_client_tids]} {
@ -685,7 +763,6 @@ namespace eval shellthread::manager {
}
}
}
#we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all.
set subscriberless_workers [list]
@ -696,8 +773,8 @@ namespace eval shellthread::manager {
set subscriber_count 0
set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed
foreach taginfo $worker_tags {
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
}
if {$subscriber_count == 0} {
lappend subscriberless_workers $workertid
@ -760,7 +837,7 @@ namespace eval shellthread::manager {
set ::shellthread::waitfor waiting
#after $timeout [list set ::shellthread::waitfor]
#2025-07 timed-out untested review
set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]]
set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}]
set waiting_for [list]
set ended [list]
@ -769,7 +846,9 @@ namespace eval shellthread::manager {
if {[thread::exists $tid]} {
lappend waiting_for $tid
#thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads)
thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor
set tid_client [thread::id]
#shellthread::worker::terminate will return thread id of terminating thread (or empty string)
thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor
}
}
if {[llength $waiting_for]} {
@ -779,13 +858,13 @@ namespace eval shellthread::manager {
set timedout 1
break
} else {
after cancel $cancelid
after cancel $timeout_timer
lappend ended $::shellthread::waitfor
}
}
}
set free_threads [list]
return [dict create existed $waiting_for ended $ended timedout $timedout]
return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]]
}
#TODO - important.

59
src/bootsupport/modules/textblock-0.1.3.tm

@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock {
tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}]
}
if {[catch {package require parser}]} {
#tclparser c extension not available - use tcl string functions to count line-endings
#try not to load parser (and associated punk::args::moduledoc::parser) immediately.
if {[package provide parser] ne ""} {
#parser already loaded.
proc height {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
#Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal)
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
} else {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
} else {
#parser not loaded - but might be loadable.
#install a 'height' function that will load parser
proc _height_tcl {textblock} {
#This is the height as it will/would-be rendered - not the number of input lines purely in terms of le
#empty string still has height 1 (at least for left-right/right-left languages)
@ -5119,8 +5135,7 @@ tcl::namespace::eval textblock {
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
}
} else {
proc height {textblock} {
proc _height_c {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
#oneshot height function - renames itself on first call to the appropriate implementation.
proc height {textblock} {
if {[catch {package require parser}]} {
#parser not available - use tcl implementation
rename ::textblock::height ""
rename ::textblock::_height_tcl ::textblock::height
} else {
#parser available - use c implementation
rename ::textblock::height ""
rename ::textblock::_height_c ::textblock::height
}
tailcall ::textblock::height $textblock
}
}
#MAINTENANCE - same as overtype::blocksize?
proc size {textblock} {
if {$textblock eq ""} {
@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock {
-etabs -default 0\
-help "expanding tabs - experimental/unimplemented."
#review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0 -choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0\
-choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements.
passing an empty string will result in no box, but title/subtitle will still appear if supplied.
${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}"

BIN
src/bootsupport/modules/zipper-0.14.tm

Binary file not shown.

10
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm

@ -26,13 +26,11 @@ namespace eval punk {
#we call tcl::tm::list to trigger the initial set of tm paths before
#we can override it, otherwise our changes will be lost
#REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
return "\
apply {{ap tmlist} {
set ::auto_path \$ap
list apply {{ap tmlist} {
set ::auto_path $ap
tcl::tm::list
set ::tcl::tm::paths \$tmlist
}} {$::auto_path} {[tcl::tm::list]}
"
set ::tcl::tm::paths $tmlist
}} $::auto_path [tcl::tm::list]
}

1
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm

@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore {
set aliases [tcl::dict::create {*}{
val ::punk::pipe::val
tstr ::punk::args::lib::tstr
FOR ::punk::lib::FOR
list_as_lines ::punk::lib::list_as_lines
lines_as_list ::punk::lib::lines_as_list
linelist ::punk::lib::linelist

2
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm

@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates {
#we will ignore any .tm files that don't have versions that tcl understands - but warn
#this reduces the cases we have to test later
set fname [file tail $tf]
lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver
lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver
if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} {
puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)"
if {[string match *-* $mname]} {

4
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/char-0.1.0.tm

@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char {
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
#if {![regexp {[\u100-\U10FFFF]} $text]} {
# return [tcl::string::length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
if {![regexp "\[\u100-\U10FFFF\]" $text]} {
return [tcl::string::length $text]
#punk::char::wcswidth has to split and examine dec value of each code
#By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW

2
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm

@ -563,7 +563,7 @@ tcl::namespace::eval punk::config {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::config::show
@cmd -name punk::config::get -help\
@cmd -name punk::config::show -help\
"Display configuration values from a config.
Accepts globs eg XDG*"
@leaders -min 1 -max 1

18
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm

@ -1947,15 +1947,15 @@ namespace eval punk::du {
#e.g we can populate compsizes for files (compressed size)
proc du_dirlisting_zipfs {folderpath args} {
puts stderr "zipfs: $folderpath"
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

373
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm

@ -2011,6 +2011,7 @@ namespace eval punk::lib {
#for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution.
#---------------------------------
proc tclscript_info {script {nscontext ""}} {
package require parser
#if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing.
if {[punk::ansi::ta::detect $script]} {
#we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts.
@ -3414,85 +3415,10 @@ namespace eval punk::lib {
return [list $stdout $stderr 0]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] "
}
set argspec [string map [list %sep% $sep] {
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "%sep%"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
#puts stderr "$argspec"
set argd [punk::args::parse $args withdef $argspec]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
@ -3532,6 +3458,77 @@ namespace eval punk::lib {
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "${$DYN_SEP}"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
set argd [punk::args::parse $args withid ::punk::lib::pdict]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@dynamic
@ -4935,6 +4932,23 @@ namespace eval punk::lib {
the range will come out the same, so the result needs to be treated as a 1-based
set of indices when performing further operations.
"
-return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels {
indices
" return a list of all indices in the order specified by the indexset,
with duplicates if specified by the indexset.
So for example
indexset_resolve 6 3..0,2,4,end
would return 3 2 1 0 2 4 5"
pairs
" return a list of index pairs representing the start and end of each range,
which may be increasing or decreasing, or just a single index
(where start and end are the same).
So for example
indexset_resolve -return pairs 6 3..0,2,4,end
would return {3 0} {2 2} {4 5}
indexset_resolve -return pairs 7 3..0,2,4,end
would return {3 0} {2 2} {4 4} {6 6}"
}
@values -min 2 -max 3
numitems -type integer
indexset -type indexset -help "comma delimited specification for indices to return"
@ -4948,32 +4962,66 @@ namespace eval punk::lib {
# for the unhappy path - the punk::args::parse is fine to generate the usage/error information.
# --------------------------------------------------
if {[llength $args] < 2} {
#too few args - use parser to generate error message
punk::args::resolve $args withid ::punk::lib::indexset_resolve
}
set indexset [lindex $args end]
set numitems [lindex $args end-1]
set indexset [lindex $args end]
if {![string is integer -strict $numitems] || ![is_indexset $indexset]} {
#use parser on unhappy path only
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
#assert we have 2 or more args
set optlist [lrange $args 0 end-2]
if {[llength $optlist] % 2 != 0} {
#options should come in pairs
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype "indices" ;#default
set base 0 ;#default
if {[llength $args] > 2} {
#if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
if {[llength $args] != 4} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set optname [lindex $args 0]
set optval [lindex $args 1]
set fulloptname [tcl::prefix::match -error "" -base $optname]
if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
dict for {opt val} $optlist {
set fulloptname [tcl::prefix::match -error "" {-base -return} $opt]
switch -exact -- $fulloptname {
-return {
set fullval [tcl::prefix::match -error "" {indices pairs} $val]
if {$fullval ni {indices pairs}} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype $fullval
}
-base {
if {![string is integer -strict $val]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set base $val
}
default {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
}
set base $optval
}
#set base 0 ;#default
#if {[llength $args] > 2} {
# #if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
# if {[llength $args] != 4} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set optname [lindex $args 0]
# set optval [lindex $args 1]
# set fulloptname [tcl::prefix::match -error "" -base $optname]
# if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set base $optval
#}
# --------------------------------------------------
@ -5160,8 +5208,84 @@ namespace eval punk::lib {
}
}
}
if {$returntype eq "pairs"} {
return [indices_to_pairs $index_list]
}
return $index_list
}
proc indices_to_pairs {indices} {
#convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing.
#if the direction of the run changes - we end the previous run and start a new one
if {[llength $indices] == 0} {
return [list]
}
set pairs [list]
set start [lindex $indices 0]
set prev $start
set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing
for {set i 1} {$i < [llength $indices]} {incr i} {
set idx [lindex $indices $i]
if {![string is integer -strict $idx]} {
error "non-integer index '$idx' in indices list"
}
if {$idx == $prev + 1} {
#increase since prev
if {$direction == 0} {
set direction 1
} elseif {$direction == -1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
} else {
#still increasing
}
set prev $idx
} elseif {$idx == $prev - 1} {
#decrease since prev
if {$direction == 0} {
set direction -1
} elseif {$direction == 1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
}
set prev $idx
} else {
#run ended - add pair to list
lappend pairs [list $start $prev]
set start $idx
set prev $idx
set direction 0
}
}
# add final run
lappend pairs [list $start $prev]
return $pairs
}
#proc indices_to_pairs {indices} {
# #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices
# set pairs [list]
# set start [lindex $indices 0]
# set prev $start
# for {set i 1} {$i < [llength $indices]} {} {
# set idx [lindex $indices $i]
# if {$idx == $prev + 1} {
# #still in a run
# set prev $idx
# } else {
# #run ended - add pair to list
# lappend pairs [list $start $prev]
# set start $idx
# set prev $idx
# }
# }
# # add final run
# lappend pairs [list $start $prev]
# return $pairs
#}
# showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side
#This doesn't need the list itself - just the length suffices.
punk::args::define {
@ -7835,6 +7959,77 @@ namespace eval punk::lib {
}
}
#sugar
#exclusive end - more intuitive for some cases and more consistent with other languages
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::lib::FOR
@cmd -name punk::lib::FOR\
-summary\
"BASIC style integer 'for loop'"\
-help\
"Sugar syntax for a common looping pattern.
The loop variable takes on values from start to end (exclusive) in increments of step.
If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end.
This is a common looping pattern that isn't directly supported by Tcl's built in control structures,
and this syntax is more concise for convenient interactive usage. For example:
FOR i 0 10 {puts $i}
will print the numbers 0 to 9.
This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops,
so may not be suitable for performance critical inner loops.
See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop
"
@values -min 3 -max 4
varname -type string -help "loop variable name"
start -type integer -help "initial value for loop variable"
end -type integer -help "end value for loop variable (exclusive)"
step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)"
script -type script -help "script to execute for each loop iteration"
}]
}
proc FOR { var args } {
switch -- [llength $args] {
3 {
# FOR x start end {}
lassign $args start end script
set step [ expr {$start > $end ? - 1 : 1} ]
}
4 {
# FOR x start end step {}
lassign $args start end step script
}
default {
error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script"
}
}
if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} {
error "FOR: start,end and step values must be integers"
}
upvar $var loopVar
set loopVar [expr {$start - $step}]
#to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition
if {$start < $end} {
if {$step <= 0} {
error "FOR: step value must be positive when start < end"
}
while {[incr loopVar $step] < $end} {
uplevel $script
}
} else {
if {$step >= 0} {
error "FOR: step value must be negative when start > end"
}
while {[incr loopVar $step] > $end} {
uplevel $script
}
}
}
#review - there are various type of uuid - we should use something consistent across platforms
#twapi is used on windows because it's about 5 times faster - but is this more important than consistency?
#twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway

39
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm

@ -677,7 +677,7 @@ namespace eval punk::mix::base {
if {$opt_use_tar != 0} {
set target [file tail $path]
set tmplocation [punk::mix::util::tmpdir]
set archivename $tmplocation/[punk::mix::util::tmpfile].tar
set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file.
cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues)
@ -687,14 +687,38 @@ namespace eval punk::mix::base {
set tsstart [clock millis]
if {[set tarpath [auto_execok tar]] ne ""} {
#using an external binary is *significantly* faster than tar::create - but comes with some risks
#review - need to check behaviour/flag variances across platforms
set versioninfo [exec {*}$tarpath --version]
#look for "bsdtar" vs "GNU"
#GNU tar is more common on linux - but also available on windows via gnuutils or msys
#<system32>/tar.exe on windows is likely to be bsdtar.
#GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag
#review - need to further check behaviour/flag variances across platforms
#don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum
#also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?)
exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " tar -cf done ($ms ms)"
} else {
if {[string match "*GNU*" $versioninfo]} {
set flags "--force-local"
} else {
#presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths
set flags ""
}
if {[catch {
#{*}$tarpath needed in case spaces in tarpath
exec {*}$tarpath -cf {*}$flags $archivename $target
} errMsg]} {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg"
} else {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' done ($ms ms)"
}
}
if {![file exists $archivename]} {
#fallback to tar library approach if external tar failed to create the archive.
set tsstart [clock millis] ;#don't include auto_exec search time for tar::create
tar::create $archivename $target
set tsend [clock millis]
@ -703,6 +727,7 @@ namespace eval punk::mix::base {
puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing"
}
if {$ftype eq "file"} {
set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)"
} else {

32
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -333,11 +333,11 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-errorprefix projectname
}]
if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"}
if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"}
set known_opts [dict keys $defaults]
foreach k [dict keys $args] {
if {$k ni $known_opts} {
error "validate_modulename error: unknown option $k. known options: $known_opts"
error "validate_projectname error: unknown option $k. known options: $known_opts"
}
}
set opts [dict merge $defaults $args]
@ -381,34 +381,6 @@ namespace eval punk::mix::cli {
return $name
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
#
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
proc get_status {{workingdir ""} args} {
set result ""

16
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm

@ -9,7 +9,7 @@
# @@ Meta Begin
# Application punk::mix::commandset::module 0.1.0
# Meta platform tcl
# Meta license BSD
# Meta license BSD
# @@ Meta End
@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module {
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_version_supplied [dict get $opts -version]
set opt_version $opt_version_supplied
if {![util::is_valid_tm_version $opt_version]} {
if {![punk::mix::util::is_valid_tm_version $opt_version]} {
error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version"
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module {
set mversion_supplied "" ;#version supplied directly in module argument
if {[string first - $module]> 0} {
#if it has a dash then version is required to be valid
lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion
if {![util::is_valid_tm_version $mversion]} {
lassign [punk::mix::util::split_modulename_version $module] modulename mversion
if {![punk::mix::util::is_valid_tm_version $mversion]} {
error "deck module.new error - unable to determine modulename-version from supplied value '$module'"
}
set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name
@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module {
} else {
set module [string range [string length $vendor.] end]
}
lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion
lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion
lappend key_version_list [list $m $mversion]
}
if {[llength $matches]} {
@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module {
} else {
error "module.new error: Unable to interpret filename components of template file '$templatefile'"
}
lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version
lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version
#t_version may be empty string if template is unversioned e.g template_whatever.tm
set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd
@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module {
} else {
#
if {[util::is_valid_tm_version $t_version]} {
if {[punk::mix::util::is_valid_tm_version $t_version]} {
if {$mversion_supplied eq ""} {
set build_version $t_version
} else {
@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module {
set name_version_pairs [list]
lappend name_version_pairs [list $moduletail $infile_version]
foreach existing $existing_versions {
lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
if {[string match #modpod-* $namepart]} {
set namepart [string range $namepart 8 end]
}

30
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm

@ -330,6 +330,35 @@ namespace eval punk::mix::util {
return 0
}
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
#Note that semver only has a small overlap with tcl tm versions.
#todo - work out what overlap and whether it's even useful
#see also TIP #439: Semantic Versioning (tcl 9??)
@ -337,6 +366,7 @@ namespace eval punk::mix::util {
set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$}
}
#todo - semver conversion/validation for other systems?
proc magic_tm_version {} {
set magicbase 999999 ;#deliberately large so given load-preference when testing!
#we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version

5
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm

@ -4186,7 +4186,7 @@ y" {return quirkykeyscript}
#this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces)
#we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace.
nseval $targetns [list apply [list {procname argname} {
set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]]
set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]]
if {$has_default} {
set answer [dict create exists 1 default $::punk::ns::corp_defvar]
} else {
@ -5025,7 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
#puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -7191,6 +7191,7 @@ y" {return quirkykeyscript}
nstest eval {package require punk::ns}
set ns ""
if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} {
#review
set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}]
set ns [nstest eval $script]
} else {

23
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm

@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference {
#[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names
#[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall
if {![catch {commandstack::get_stack} cstack]} {
if {[dict exists $cstack ::package]} {
set pstack [dict get $cstack ::package]
foreach record $pstack {
if {[dict get $record rename] eq "punk::packagepreference"} {
#already installed - silently ignore.
return 0
}
}
}
}
#todo - review/update commandstack package
#modern module/lib names should preferably be lower case
#see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9)
@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference {
if {[llength $pkgloadedinfo]} {
if {[llength $available_versions] > 1} {
puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
#puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid"
}
lassign $pkgloadedinfo loaded_path name
set lc_loadedpath [string tolower $loaded_path]
@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference {
puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg"
}
} else {
puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid"
}
}
#---------------------------------------------------------------

133
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm

@ -432,7 +432,7 @@ proc repl::start {inchan args} {
variable codethread
#review
if {$codethread eq ""} {
error "start - no codethread. call init first. (options -safe 0|1)"
error "start - no codethread. call init first. (options -type 0|1)"
}
variable commandstr
@ -478,7 +478,7 @@ proc repl::start {inchan args} {
#set ::punk::repl::codethread::running 1
#the interp in which commands such as d/ run
#we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values
#we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values
#punk::repl::codethread::running is required whether safe or not.
interp eval code {
namespace eval ::punk::repl::codethread {}
@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} {
}
#add to sliding buffer of last x chars emmitted to screen by repl
# add to sliding buffer of last x chars emmitted to screen by repl
#(we could maintain only one char - more kept merely for debug assistance)
#will not detect emissions from exec with stdout redirected and presumably some extensions etc
proc repl::screen_last_char_add {c what {why ""}} {
@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#if {$chunk eq "\x1b\[C"} {
#}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_off
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
$editbuf add_chunk $chunk
@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
lappend input_chunks_waiting($inputchan) $waiting
}
}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_on
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
if {$editbuf_linenum_submitted == 0} {
#(there is no line 0 - lines start at 1)
@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap1 POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error1 in repl_handler: $repl_error"
rputs stderr "error1 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "erroropts: $erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
variable codethread_cond
variable codethread_mutex
lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#JJJJ JMN test
#REVIEW
#lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#thread::transfer $codethread stderr
#chan configure stdout -buffering none
@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error in repl_handler: $repl_error"
rputs stderr "error2 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "$erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2986,10 +2996,10 @@ namespace eval repl {
variable codethread_cond
variable codethread_mutex
set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
foreach {k v} $args {
switch -- $k {
-force - -safe - -safelog - -paths - -callback_interp {
-force - -type - -safelog - -paths - -callback_interp {
dict set opts $k $v
}
default {
@ -2998,7 +3008,7 @@ namespace eval repl {
}
}
set opt_force [dict get $opts -force]
set opt_safe [dict get $opts -safe]
set opt_repltype [dict get $opts -type]
set opt_safelog [dict get $opts -safelog]
if {$opt_safelog eq "0"} {
set opt_safelog ""
@ -3113,46 +3123,50 @@ namespace eval repl {
package require punk::args
#package require Thread
if {[catch {package require thread} errM]} {
puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
#if {[catch {package require thread} errM]} {
# puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
if {[catch {package require Thread} errM2]} {
puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2"
puts stdout ">>repl::init auto_path : $::auto_path"
puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]"
}
}
#}
namespace eval ::punk::repl::codethread {}
# -------------------------------------------------------------------------------------------------------------------------------------------------------
#-----
#review - icomm as a possible way to talk to thread outside of the code interp.
#thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow
#thread::send to caller defined interp targets (reference?)
#snit required for icomm
if {[catch {package require snit} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
}
if {[catch {package require punk::icomm} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
}
#if {[catch {package require snit} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
#}
#if {[catch {package require punk::icomm} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
#}
#-----
namespace eval ::punk::repl::codethread {}
#todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
catch {package require tcl::chan::fifo2}
if {[catch {
#first use can raise error being a version number e.g 0.1.0 - why?
lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
} errMsg]} {
puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
} else {
#experimental?
#puts stdout "transferring chan $replside to thread %replthread%"
#flush stdout
#if {[catch {
# #after 0 [list thread::transfer %replthread% $replside]
#} errMsg]} {
# #puts stdout "---thread::transfer error: $errMsg"
#}
}
#----------------------------------------------------------------------------------------------------------------------
# todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
# catch {package require tcl::chan::fifo2}
# if {[catch {
# #first use can raise error being a version number e.g 0.1.0 - why?
# lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
# } errMsg]} {
# puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
# } else {
# #experimental?
# #puts stdout "transferring chan $replside to thread %replthread%"
# #flush stdout
# # if {[catch {
# # #after 0 [list thread::transfer %replthread% $replside]
# # } errMsg]} {
# # #puts stdout "---thread::transfer error: $errMsg"
# # }
# }
#----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------------------------------------------------------------------
package require punk::console
package require punk::repl::codethread
@ -3333,7 +3347,7 @@ namespace eval repl {
set ts_start [clock seconds]
set replresult [interp eval code {
package require punk::repl
repl::init -safe punk
repl::init -type punk
repl::start stdin
}]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3343,7 +3357,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe safe {*}$args]
interp eval code [list repl::init -type safe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3353,7 +3367,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
set codethread [interp eval code [list repl::init -safe safebase {*}$args]]
set codethread [interp eval code [list repl::init -type safebase {*}$args]]
puts stdout "safebase codethread:$codethread"
set replresult [interp eval code [list repl::start stdin]]
@ -3364,7 +3378,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe punksafe {*}$args]
interp eval code [list repl::init -type punksafe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3376,14 +3390,14 @@ namespace eval repl {
#flush stdout
set args %args%
set safe [dict get $args -safe]
set repltype [dict get $args -type]
set safelog [dict get $args -safelog]
set paths [list]
if {[dict exists $args -paths]} {
set paths [dict get $args -paths]
}
switch -- $safe {
switch -- $repltype {
safe {
interp create -safe -- code
code eval [list namespace eval ::punk::libunknown {}]
@ -3444,7 +3458,7 @@ namespace eval repl {
#todo a specific punk::libunknown 'package unknown' handler for safe interps
#pull in code via calls to source cached code?
switch -- $safe {
switch -- $repltype {
safe {
if {[llength $paths]} {
package require punk::island
@ -3797,8 +3811,27 @@ namespace eval repl {
}
punk - 0 {
#------------------------------------------------------------------------
#Test - experimental 2026
if {"stdout" in [chan names]} {
interp share {} stdout code
} else {
interp share {} [shellfilter::stack::item_tophandle stdout] code
}
if {"stderr" in [chan names]} {
interp share {} stderr code
} else {
interp share {} [shellfilter::stack::item_tophandle stderr] code
}
foreach nm {shellspyout shellspyerr} {
set thandle [shellfilter::stack::item_tophandle $nm]
if {$thandle ne ""} {
interp share {} $thandle code
}
}
#------------------------------------------------------------------------
interp eval code {
#safe !=1 and safe !=2, tmlist: %tmlist%
#repltype !=1 and repltype !=2, tmlist: %tmlist%
set ::argv0 %argv0%
set ::argv %argv%
set ::argc %argc%
@ -3978,8 +4011,8 @@ namespace eval repl {
thread::id
}
set init_script [string map $scriptmap $init_script]
#REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args%
#we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that?
#REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args%
#we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that?
#thread::send $codethread $init_script
if {![catch {
@ -3994,7 +4027,7 @@ namespace eval repl {
error $errMsg
}
}
#init - don't auto init - require init with possible options e.g -safe
#init - don't auto init - require init with possible options e.g -type
}
package provide punk::repl [namespace eval punk::repl {
variable version

34
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm

@ -83,34 +83,38 @@ namespace eval punk::repo {
proc get_fossil_usage {} {
set allcmds [runout -n fossil help -a]
set allcmds [punk::ansi::ansistrip $allcmds]
set mainhelp [runout -n fossil help]
set mainhelp [punk::ansi::ansistrip $mainhelp]
set maincommands [list]
#only start parsing for TOPICS after a line such as "Other comman values for TOPIC:"
set parsing_topics 0
foreach ln [split $mainhelp \n] {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
lappend maincommands {*}$ln
}
lappend maincommands {*}$ln
}
}
#fossil output was ordered in columns, but we loaded list in row-wise, messing up the order
set maincommands [lsort $maincommands]
set allcmds [lsort $allcmds]
set othercmds [punk::lib::ldiff $allcmds $maincommands]
set fossil_setting_names [lsort [runout -n fossil help -s]]
set setting_info [runout -n fossil help -s]
set setting_info [punk::ansi::ansistrip $setting_info]
set fossil_setting_names [lsort $setting_info]
set result "@leaders -min 0\n"
@ -186,6 +190,8 @@ namespace eval punk::repo {
foreach ln $basic_opt_lines {
set ln [string trim $ln]
#fossil sometimes emits cursor control sequences e.g CSI 3 q
set ln [punk::ansi::ansistrip $ln]
if {$ln eq ""} {
continue
}
@ -250,6 +256,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage add]}
@form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}}
} ""]
@ -264,6 +271,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage diff]}
@form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}}
} ""]

20
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm

@ -121,14 +121,6 @@ namespace eval punkcheck {
}
method as_record {} {
#set fields [list\
# -targets $o_targets\
# -keep_installrecords $o_keep_installrecords\
# -keep_skipped $o_keep_skipped\
# -keep_inprogress $o_keep_inprogress\
# body $o_records\
#]
dict create {*}{
} tag FILEINFO {*}{
} -targets $o_targets {*}{
@ -216,18 +208,6 @@ namespace eval punkcheck {
} else {
set tsiso_end ""
}
#set fields [list\
# -tsiso_begin $tsiso_begin\
# -ts_begin $o_ts_begin\
# -tsiso_end $tsiso_end\
# -ts_end $o_ts_end\
# -id $o_id\
# -source $o_rel_sourceroot\
# -targets $o_rel_targetroot\
# -types $o_types\
# -config $o_configdict\
#]
#set record [dict create tag EVENT {*}$fields]
dict create {*}{
} tag EVENT {*}{

444
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm

@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log {
}
proc ::shellfilter::log::close {tag} {
#shellthread::manager::close_worker $tag
#unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag.
shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed
}
@ -106,6 +108,13 @@ namespace eval shellfilter::pipe {
package require shellthread
#we are only using the fifo in a single direction to pipe to another thread
# - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each
#----------------------------------------------------------------------------------------
# Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations:
# (not necessarily a comprehensive list)
# - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms
# tcl::chan::fifo2 is a pure Tcl implementation.
# - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2.
#----------------------------------------------------------------------------------------
if {![catch {package require Memchan}]} {
lassign [fifo2] wchan rchan
} else {
@ -289,7 +298,8 @@ namespace eval shellfilter::chan {
set o_enc [tcl::dict::get $tf -encoding]
set o_encbuf ""
set settingsdict [tcl::dict::get $tf -settings]
set varname [tcl::dict::get $settingsdict -varname]
set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them?
#these are not upvared - must be fully qualified variable names.
set o_datavars $varname
if {[tcl::dict::exists $tf -junction]} {
set o_is_junction [tcl::dict::get $tf -junction]
@ -298,18 +308,18 @@ namespace eval shellfilter::chan {
}
}
method initialize {ch mode} {
return [list initialize finalize write flush clear]
if {"read" in $mode} {
#this should raise an error in 'chan push'
error "shellfilter::chan::tee_to_var transform does not support read mode"
}
return [list initialize finalize write flush]
}
method finalize {ch} {
my destroy
}
method clear {ch} {
return
}
method watch {ch events} {
# must be present but we ignore it because we do not
# post any events
}
#method clear {ch} {
# return
#}
#method read {ch count} {
# return ?
#}
@ -320,9 +330,14 @@ namespace eval shellfilter::chan {
#puts stdout "<flush>"
#review - just clear o_encbuf and emit nothing?
#we wouldn't have a value there if it was convertable from the channel encoding?
set clear $o_encbuf
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {ch bytes} {
#test with set x [string repeat " \U1f6c8" 2043]
@ -387,10 +402,12 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read drain write flush clear finalize]
#return [list initialize read drain write flush clear finalize]
return [list initialize write flush clear finalize]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#Note that an error in the finalize can stop 'chan pop' from running properly.
#::shellfilter::log::close $o_logsource
my destroy
}
method watch {transform_handle events} {
@ -398,26 +415,53 @@ namespace eval shellfilter::chan {
# post any events
}
method clear {transform_handle} {
set o_encbuf ""
return
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set logdata [tcl::encoding::convertfrom $o_enc $bytes]
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $logdata
return $bytes
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set logdata [tcl::encoding::convertfrom $o_enc $bytes]
# #::shellfilter::log::write $o_logsource $logdata
# puts -nonewline $o_localchan $logdata
# return $bytes
#}
#method flush {transform_handle} {
# #return ""
# set clear $o_encbuf[set o_encbuf ""]
# if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
# #if we can't convert the buffer contents to a string - put it back and try again with more data later
# #REVIEW?
# set o_encbuf $clear
# puts -nonewline $o_localchan ""
# return ""
# }
# #jjj
# puts -nonewline $o_localchan $stringdata
# flush $o_localchan
# return $clear
#}
method flush {transform_handle} {
#return ""
set clear $o_encbuf
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {transform_handle bytes} {
#set logdata [tcl::encoding::convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
if {$inputbytes eq ""} {
#review - do we even get empty writes?
puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write."
return ""
}
set o_encbuf ""
set tail_offset 0
while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} {
@ -426,18 +470,23 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
#nothing convertable in the buffer - put it back and try again with more data later
set stringdata ""
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $stringdata
#return $bytes
return [::tcl::string::range $inputbytes 0 end-$tail_offset]
return $converted ;#same data as $stringdata but in bytes form.
}
#a tee is not a redirection - because data still flows along the main path
method meta_is_redirection {} {
@ -501,6 +550,7 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
@ -508,10 +558,15 @@ namespace eval shellfilter::chan {
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
#set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
::shellfilter::log::write $o_logsource $stringdata
return $bytes
#return $bytes
return $converted ;#same data as $stringdata but in bytes form.
}
method meta_is_redirection {} {
return $o_is_junction
@ -519,6 +574,10 @@ namespace eval shellfilter::chan {
}
#see TIP 230 Tcl Channel Transformation Reflection API
#the logonly transform does not emit any data downwards towards the base channel - it only writes to the log.
# - ie we are redirecting all data to the log.
oo::class create logonly {
variable o_tid
variable o_logsource
@ -537,19 +596,35 @@ namespace eval shellfilter::chan {
set o_tid [::shellfilter::log::open $o_logsource $settingsdict]
}
method initialize {transform_handle mode} {
return [list initialize finalize write]
#mode is a list containing any of the strings 'read' or 'write'.
if {"read" in $mode} {
#Note that raising an error prevents creation of the transformation.
#The thrown error will appear as a error thrown by 'chan push'.
error "logonly transform does not support read mode"
}
#return all methods supported by the handler
#(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter)
return [list initialize finalize write flush]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#review - tip 230 states that any return value or error raised by finalize is ignored
#we wrap in catch to ensure the 'my destroy' is always called.
catch {::shellfilter::log::close $o_logsource}
my destroy
}
method watch {transform_handle events} {
# must be present but we ignore it because we do not
# post any events
#clear?
method flush {transform_handle} {
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log?
#REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string.
#This may be useful for debugging issues, but it may also result in garbage data in the log.
::shellfilter::log::write $o_logsource $o_encbuf
set o_encbuf ""
}
return
}
#method read {transform_handle count} {
# return ?
#}
method write {transform_handle bytes} {
#set logdata [encoding convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
@ -600,26 +675,49 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read write clear flush drain finalize]
#return [list initialize read write clear flush drain finalize]
#REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work.
#this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel.
#----------------------
#don't do this
#----------------------
#if {"read" in $mode} {
# #Note that raising an error prevents creation of the transformation.
# #The thrown error will appear as a error thrown by 'chan push'.
# error "shellfilter::chan::ansistrip transform does not support read mode"
#}
#----------------------
return [list initialize read write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method clear {transform_handle} {
return
}
method watch {transform_handle events} {
}
method drain {transform_handle} {
return ""
}
#method clear {transform_handle} {
# return
#}
#method drain {transform_handle} {
# return ""
#}
method read {transform_handle bytes} {
set instring [encoding convertfrom $o_enc $bytes]
set outstring [punk::ansi::ansistrip $instring]
return [encoding convertto $o_enc $outstring]
}
#method flush {transform_handle} {
# return ""
#}
method flush {transform_handle} {
return ""
#return ""
set clear $o_encbuf[set o_encbuf ""]
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - put it back and try again with more data later
#REVIEW?
set o_encbuf $clear
return ""
}
#review
return $stringdata
}
#method write {transform_handle bytes} {
# #broken due to occasional unexpected byte sequence
@ -989,29 +1087,44 @@ namespace eval shellfilter::chan {
}
method initialize {transform_handle mode} {
#clear undesirable in terminal output channels (review)
return [list initialize write flush read drain finalize]
#return [list initialize write flush read drain finalize]
if {$mode eq "read"} {
error "shellfilter::chan::ansiwrap channel transform does not support read mode"
}
return [list initialize write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method watch {transform_handle events} {
}
method clear {transform_handle} {
#In the context of stderr/stdout - we probably don't want clear to run.
#Terminals might call it in the middle of a split ansi code - resulting in broken output.
#Leave clear of it the init call
#Leave clear out of the initialize call for now
puts stdout "<clear>"
set emit [tcl::encoding::convertto $o_enc $o_buffered]
set o_buffered ""
return $emit
}
#method flush {transform_handle} {
# #puts stdout "<flush>"
# set inputbytes $o_buffered$o_encbuf
# set emit [tcl::encoding::convertto $o_enc $inputbytes]
# set o_buffered ""
# set o_encbuf ""
# return $emit
#}
method flush {transform_handle} {
#puts stdout "<flush>"
set inputbytes $o_buffered$o_encbuf
set emit [tcl::encoding::convertto $o_enc $inputbytes]
#return ""
set clear $o_buffered$o_encbuf
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes?
# - probably not.
#REVIEW?
return ""
}
set o_buffered ""
set o_encbuf ""
return $emit
return $stringdata
}
method write {transform_handle bytes} {
#set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally
@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan {
#set outstring ">>>$instring"
return [tcl::encoding::convertto $o_enc $outstring]
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set instring [tcl::encoding::convertfrom $o_enc $bytes]
set outstring "$o_do_colour$instring$o_do_normal"
return [tcl::encoding::convertto $o_enc $outstring]
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set instring [tcl::encoding::convertfrom $o_enc $bytes]
# set outstring "$o_do_colour$instring$o_do_normal"
# return [tcl::encoding::convertto $o_enc $outstring]
#}
method meta_is_redirection {} {
return $o_is_junction
}
@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack {
}
proc status {{pipename *} args} {
variable pipelines
package require textblock
set pipecount [dict size $pipelines]
set tabletitle "$pipecount pipelines active"
set t [textblock::class::table new $tabletitle]
$t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts.
$t add_column -headers [list channel-ident]
$t add_column -headers [list device-info localchan]
$t configure_column 1 -header_colspans {3}
@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack {
$t add_column -headers [list stack-info]
foreach k [dict keys $pipelines $pipename] {
set lc [dict get $pipelines $k device localchan]
if {[catch {chan configure $lc -encoding} lc_enc]} {
set lc_enc "<err>"
}
set rc [dict get $pipelines $k device remotechan]
if {[dict exists $k device workertid]} {
set tid [dict get $pipelines $k device workertid]
@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack {
set stackinfo ""
} else {
set tbl_inner [textblock::class::table new]
$tbl_inner configure -frametype ascii
$tbl_inner configure -show_edge 0
$tbl_inner add_column -headers id
$tbl_inner add_column -headers transform
$tbl_inner add_column -headers handle
$tbl_inner add_column -headers settings
$tbl_inner add_column -headers aside
foreach rec $stack {
set handle [punk::lib::dict_getdef $rec -handle ""]
set id [punk::lib::dict_getdef $rec -id ""]
set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]]
set handle [punk::lib::dict_getdef $rec -handle ""]
if {$handle ne ""} {
if {[catch {chan configure $handle -encoding} handle_enc]} {
set handle_enc "<err>"
}
} else {
set handle_enc ""
}
set settings [punk::lib::dict_getdef $rec -settings ""]
$tbl_inner add_row [list $id $transform $handle $settings]
set aside [punk::lib::dict_getdef $rec -aside ""]
if {$aside ne ""} {
set aside [punk::lib::showdict $aside]
}
$tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside]
}
set stackinfo [$tbl_inner print]
$tbl_inner destroy
}
$t add_row [list $k $lc $rc $tid $stackinfo]
$t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo]
}
set result [$t print]
$t destroy
@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack {
proc unwind {pipename} {
variable pipelines
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
foreach tf [lreverse $stack] {
chan pop $localchan
if {[catch {chan eof $localchan} _eof]} {
#We don't actually care about eof state - but we use this to test if the channel exists.
#('chan names' doesn't reliably show all channels in some cases)
#do nothing.
} else {
#if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel
# but here we should only be calling it when there are transforms on the channel as indicated by the stack variable.
chan pop $localchan
}
}
dict set pipelines $pipename [list]
}
#todo
proc delete {pipename {wait 0}} {
#::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"]
variable pipelines
set pipeinfo [dict get $pipelines $pipename]
set deviceinfo [dict get $pipeinfo device]
@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack {
thread::release $tid
}
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why?
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"'
#- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed.
catch {chan close $localchan}
#if {[catch {chan close $localchan} errMsg]} {
# ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg"
#}
}
#review - proc name clarity is questionable. remove_stackitem?
proc remove {pipename remove_id} {
#::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"]
variable pipelines
if {![dict exists $pipelines $pipename]} {
puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
#puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
return
}
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set previous_blockingstate [chan configure $localchan -blocking]
if {$previous_blockingstate} {
chan configure $localchan -blocking 0
}
set posn 0
set idposn -1
set asideposn -1
@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack {
dict set container -aside {}
lset stack $asideposn $container
dict set pipelines $pipename stack $stack
#::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'"
} else {
if {$idposn < 0} {
::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
#::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
#puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
return 0
}
set removed_item [lindex $stack $idposn]
@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack {
#pop all chans before adding anything back in!
foreach p $poplist {
#review
#update idletasks
#puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]"
#::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---"
#after 0 [list chan pop $localchan]
chan pop $localchan
#::shellfilter::log::write shellfilter-remove "POPPED"
#update idletasks
}
#after 5
#::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
#puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
set obj [$t new $restore]
set h [chan push $localchan $obj]
dict set restore -handle $h
dict set restore -obj $obj
lappend stack $restore
#puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
}
#after 5
#put popped back except for the first one, which we want to remove
foreach p [lrange $poplist 1 end] {
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set obj [$t new $p]
set h [chan push $localchan $obj]
dict set p -handle $h
dict set p -obj $obj
lappend stack $p
#update idletasks
#puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
}
#after 5
dict set pipelines $pipename stack $stack
}
#puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE"
#::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE"
if {$previous_blockingstate} {
chan configure $localchan -blocking 1
}
#JMNJMN 2025 review!
#show_pipeline $pipename -note "after_remove $remove_id"
return 1
@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack {
namespace eval shellfilter {
variable sources [list]
variable stacks [dict create]
#-------------------------------------------------------------------------------------------------
#tcllib logger infrastructure.
#-------------------------------------------------------------------------------------------------
namespace eval ::shellfilter::loggerprocs {
#container for procs/aliases to be pointed to by tcllib logger using log::logproc
proc Dolog {lvl txt} {
#logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted
set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'"
puts stderr $msg
}
proc Runlog {lvl script} {
uplevel 1 $script
}
}
if {![catch {
package require logger
}]} {
logger::initNamespace ::shellfilter
foreach lvl [logger::levels] {
interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl
log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl
}
logger::setlevel warn
#namespace path ::shellfilter::log
} else {
#e.g tcllib not available, safe interp?
#fake out the logger calls
namespace eval ::shellfilter::log {
foreach lvl {debug info notice warn error critical alert emergency} {
proc $lvl {args} {}
}
}
}
#-------------------------------------------------------------------------------------------------
proc ::shellfilter::redir_channel_to_log {chan args} {
variable sources
@ -2396,13 +2613,9 @@ namespace eval shellfilter {
#must be a list. If it was a shell commandline string. convert it elsewhere first.
variable sources
set runtag "shellfilter-run"
#set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
set tid [::shellfilter::log::open $runtag [list -syslog ""]]
if {[catch {llength $commandlist} listlen]} {
set listlen "<not-a-tcl-list>"
}
::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"
#flush stdout
#flush stderr
@ -2416,6 +2629,7 @@ namespace eval shellfilter {
-errchan stderr
-inchan stdin
-tclscript 0
-syslog ""
}]
set opts [dict merge $defaults $args]
@ -2432,6 +2646,13 @@ namespace eval shellfilter {
set teehandle_err ${teehandle}err
set teehandle_in ${teehandle}in
set syslog [dict get $opts -syslog]
dict unset opts -syslog
set runtag "shellfilter-run"
set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
#set tid [::shellfilter::log::open $runtag [list -syslog $syslog]]
log::info {::shellfilter::log::write $runtag " opts: $opts"}
log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"}
#puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err"
@ -2441,19 +2662,32 @@ namespace eval shellfilter {
lappend sources $source
}
}
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
#set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device]
#set inpipechan [dict get $indeviceinfo localchan]
#---------------------
# #TEST
# chan configure $outpipechan -blocking 0
# chan configure $errpipechan -blocking 0
log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"}
log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"}
#---------------------
log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"}
#NOTE:These transforms are not necessarily at the top of each stack!
#The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit.
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
}
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2493,6 +2727,7 @@ namespace eval shellfilter {
set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr]
}
}
log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"}
} else {
if {[catch {
#script result
@ -2500,29 +2735,45 @@ namespace eval shellfilter {
} errMsg]} {
set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"]
}
log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"}
}
#puts "shellfilter::run finished call"
#-------------------------
#warning - without flush stdout - we can get hang, but only on some terminals
# - mechanism for this problem not understood!
#todo - test/document.
flush stdout
flush stderr
#update idletasks
#-------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
shellfilter::stack::remove stdout $id_out
shellfilter::stack::remove stderr $id_err
#shellfilter::stack::remove stderr $id_in
#puts stderr "shellfilter::run complete..."
#----------------------------------------------------------------------------------------------
# wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug.
# Logger allows the contents to be evaluated only if logging is switched on.
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#chan configure stderr -buffering line
#flush stdout
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
::shellfilter::log::write $runtag " return '$exitinfo'"
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
::shellfilter::log::close $runtag
return $exitinfo
}
@ -2552,6 +2803,7 @@ namespace eval shellfilter {
}
if {$close} {
lappend tidied_sources $s
#unsubscribe from source tag s.
shellfilter::log::close $s
lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s]
}

177
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellthread-1.6.2.tm

@ -119,29 +119,58 @@ namespace eval shellthread::worker {
set waitvar ::shellthread::worker::wait($inpipe,[clock micros])
#tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan
chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
vwait $waitvar
}
proc pipe_read {chan source waitfor readbuffering writebuffering} {
#chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
if {$readbuffering eq "line"} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
}
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
} else {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
}
vwait $waitvar
}
#proc pipe_read {chan source waitfor readbuffering writebuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
# } else {
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# }
# } else {
# set chunk [chan read $chan]
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $chan
# }
#}
proc start_pipe_write {source writechan args} {
variable outpipe
@ -181,8 +210,8 @@ namespace eval shellthread::worker {
chan configure $writechan -blocking 0
set waitvar ::shellthread::worker::wait($outpipe,[clock micros])
chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
if {$readbuffering eq "line"} {
if {$readbuffering eq "line"} {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
@ -191,19 +220,55 @@ namespace eval shellthread::worker {
puts -nonewline $writechan $chunk
}
}
} else {
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
flush $writechan ;#2026-05-19 - ensure all data is sent before closing
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}} $readchan $writechan $source $waitvar]
} else {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunk [chan read $chan]
puts -nonewline $writechan $chunk
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}
}} $readchan $writechan $source $waitvar $readbuffering]
}} $readchan $writechan $source $waitvar]
}
# chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# puts $writechan $chunk
# } else {
# puts -nonewline $writechan $chunk
# }
# }
# } else {
# set chunk [chan read $chan]
# puts -nonewline $writechan $chunk
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $writechan
# if {$chan ne "stdin"} {
# chan close $chan
# }
# }
# }} $readchan $writechan $source $waitvar $readbuffering]
vwait $waitvar
}
@ -479,6 +544,10 @@ namespace eval shellthread::manager {
set sourcetag [lindex $sourcetaglist 0] ;#todo - use all
set defaults [dict create {*}{
-raw 0
-file {}
-syslog {}
-direction out
-workertype message
}]
set settingsdict [dict merge $defaults $settingsdict]
@ -501,6 +570,10 @@ namespace eval shellthread::manager {
return [dict get $winfo tid]
} elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} {
#review - magic dict seems brittle - shouldn't hard code here.???
#existing worker has default settings - so we'll assume it's a placeholder and update it with our settings
#review - where/when do we override the default settings?
dict lappend winfo list_client_tids $tidclient
dict set workers $sourcetag $winfo ;#writeback
return [dict get $winfo tid]
@ -575,11 +648,11 @@ namespace eval shellthread::manager {
package require Thread
package require shellthread
if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} {
unset ::settingsinfo
set ::shellthread_init "ok"
unset ::settingsinfo
set ::shellthread_init "ok"
} else {
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
}
}]
@ -622,15 +695,23 @@ namespace eval shellthread::manager {
proc write_log {source msg args} {
variable workers
set ts_micros_sent [clock micros]
set defaults [list -async 1 -level info]
set opts [dict merge $defaults $args]
if {[dict exists $workers $source]} {
if {[dict exists $workers $source tid]} {
set tidworker [dict get $workers $source tid]
if {$tidworker eq "noop"} {
return
}
} else {
set tidworker ""
}
set ts_micros_sent [clock micros]
set defaults [list {*}{
-async 1
-level info
}]
set opts [dict merge $defaults $args]
if {$tidworker ne ""} {
if {![thread::exists $tidworker]} {
# -syslog -file ?
set tidworker [new_worker $source]
@ -674,10 +755,7 @@ namespace eval shellthread::manager {
if {[dict exists $workers $source]} {
set list_client_tids [dict get $workers $source list_client_tids]
if {[set posn [lsearch $list_client_tids $mytid]] >= 0} {
#set list_client_tids [lreplace $list_client_tids $posn $posn]
#set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn]
ledit list_client_tids $posn $posn
dict set workers $source list_client_tids $list_client_tids
}
if {![llength $list_client_tids]} {
@ -685,7 +763,6 @@ namespace eval shellthread::manager {
}
}
}
#we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all.
set subscriberless_workers [list]
@ -696,8 +773,8 @@ namespace eval shellthread::manager {
set subscriber_count 0
set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed
foreach taginfo $worker_tags {
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
}
if {$subscriber_count == 0} {
lappend subscriberless_workers $workertid
@ -760,7 +837,7 @@ namespace eval shellthread::manager {
set ::shellthread::waitfor waiting
#after $timeout [list set ::shellthread::waitfor]
#2025-07 timed-out untested review
set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]]
set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}]
set waiting_for [list]
set ended [list]
@ -769,7 +846,9 @@ namespace eval shellthread::manager {
if {[thread::exists $tid]} {
lappend waiting_for $tid
#thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads)
thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor
set tid_client [thread::id]
#shellthread::worker::terminate will return thread id of terminating thread (or empty string)
thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor
}
}
if {[llength $waiting_for]} {
@ -779,13 +858,13 @@ namespace eval shellthread::manager {
set timedout 1
break
} else {
after cancel $cancelid
after cancel $timeout_timer
lappend ended $::shellthread::waitfor
}
}
}
set free_threads [list]
return [dict create existed $waiting_for ended $ended timedout $timedout]
return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]]
}
#TODO - important.

59
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm

@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock {
tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}]
}
if {[catch {package require parser}]} {
#tclparser c extension not available - use tcl string functions to count line-endings
#try not to load parser (and associated punk::args::moduledoc::parser) immediately.
if {[package provide parser] ne ""} {
#parser already loaded.
proc height {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
#Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal)
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
} else {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
} else {
#parser not loaded - but might be loadable.
#install a 'height' function that will load parser
proc _height_tcl {textblock} {
#This is the height as it will/would-be rendered - not the number of input lines purely in terms of le
#empty string still has height 1 (at least for left-right/right-left languages)
@ -5119,8 +5135,7 @@ tcl::namespace::eval textblock {
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
}
} else {
proc height {textblock} {
proc _height_c {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
#oneshot height function - renames itself on first call to the appropriate implementation.
proc height {textblock} {
if {[catch {package require parser}]} {
#parser not available - use tcl implementation
rename ::textblock::height ""
rename ::textblock::_height_tcl ::textblock::height
} else {
#parser available - use c implementation
rename ::textblock::height ""
rename ::textblock::_height_c ::textblock::height
}
tailcall ::textblock::height $textblock
}
}
#MAINTENANCE - same as overtype::blocksize?
proc size {textblock} {
if {$textblock eq ""} {
@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock {
-etabs -default 0\
-help "expanding tabs - experimental/unimplemented."
#review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0 -choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0\
-choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements.
passing an empty string will result in no box, but title/subtitle will still appear if supplied.
${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}"

BIN
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/zipper-0.14.tm

Binary file not shown.

10
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm

@ -26,13 +26,11 @@ namespace eval punk {
#we call tcl::tm::list to trigger the initial set of tm paths before
#we can override it, otherwise our changes will be lost
#REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
return "\
apply {{ap tmlist} {
set ::auto_path \$ap
list apply {{ap tmlist} {
set ::auto_path $ap
tcl::tm::list
set ::tcl::tm::paths \$tmlist
}} {$::auto_path} {[tcl::tm::list]}
"
set ::tcl::tm::paths $tmlist
}} $::auto_path [tcl::tm::list]
}

1
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm

@ -109,6 +109,7 @@ tcl::namespace::eval punk::aliascore {
set aliases [tcl::dict::create {*}{
val ::punk::pipe::val
tstr ::punk::args::lib::tstr
FOR ::punk::lib::FOR
list_as_lines ::punk::lib::list_as_lines
lines_as_list ::punk::lib::lines_as_list
linelist ::punk::lib::linelist

2
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm

@ -628,7 +628,7 @@ namespace eval punk::cap::handlers::templates {
#we will ignore any .tm files that don't have versions that tcl understands - but warn
#this reduces the cases we have to test later
set fname [file tail $tf]
lassign [split [punk::mix::cli::lib::split_modulename_version $fname]] mname ver
lassign [split [punk::mix::util::split_modulename_version $fname]] mname ver
if {[catch {punk::mix::cli::lib::validate_modulename $mname} errM]} {
puts stderr "Invalid module name/version $tf - please rename with standard Tcl .tm module name and version (or leave out version)"
if {[string match *-* $mname]} {

4
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/char-0.1.0.tm

@ -2468,10 +2468,10 @@ tcl::namespace::eval punk::char {
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
#if {![regexp {[\u100-\U10FFFF]} $text]} {
# return [tcl::string::length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
if {![regexp "\[\u100-\U10FFFF\]" $text]} {
return [tcl::string::length $text]
#punk::char::wcswidth has to split and examine dec value of each code
#By stripping controls + 7F (leaving tab) we've already eliminated the non-printable ascii - REVIEW

2
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm

@ -563,7 +563,7 @@ tcl::namespace::eval punk::config {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::config::show
@cmd -name punk::config::get -help\
@cmd -name punk::config::show -help\
"Display configuration values from a config.
Accepts globs eg XDG*"
@leaders -min 1 -max 1

18
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm

@ -1947,15 +1947,15 @@ namespace eval punk::du {
#e.g we can populate compsizes for files (compressed size)
proc du_dirlisting_zipfs {folderpath args} {
puts stderr "zipfs: $folderpath"
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

373
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm

@ -2011,6 +2011,7 @@ namespace eval punk::lib {
#for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution.
#---------------------------------
proc tclscript_info {script {nscontext ""}} {
package require parser
#if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing.
if {[punk::ansi::ta::detect $script]} {
#we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts.
@ -3414,85 +3415,10 @@ namespace eval punk::lib {
return [list $stdout $stderr 0]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] "
}
set argspec [string map [list %sep% $sep] {
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "%sep%"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
#puts stderr "$argspec"
set argd [punk::args::parse $args withdef $argspec]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
@ -3532,6 +3458,77 @@ namespace eval punk::lib {
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
The pdict function operates on variable names - passing the value to the showdict function which operates on values
(see also showdict)"
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "${$DYN_SEP}"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
"existing channel - or 'none' to return as string"
@values -min 1 -max -1
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
set argd [punk::args::parse $args withid ::punk::lib::pdict]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
set isarray [uplevel 1 [list ::tcl::array::exists $dvar]]
if {$isarray} {
set dvalue [uplevel 1 [list ::tcl::array::get $dvar]]
if {![dict exists $opts -keytemplates]} {
set arrdisplay [string map [list %dvar% $dvar] {${[if {[lindex $key 1] eq "query"} {val "%dvar% [lindex $key 0]"} {val "%dvar%($key)"}]}}]
dict set opts -keytemplates [list $arrdisplay]
}
dict set opts -keysorttype dictionary
} else {
set dvalue [uplevel 1 [list set $dvar]]
}
showdict {*}$opts $dvalue {*}$patterns
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@dynamic
@ -4935,6 +4932,23 @@ namespace eval punk::lib {
the range will come out the same, so the result needs to be treated as a 1-based
set of indices when performing further operations.
"
-return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels {
indices
" return a list of all indices in the order specified by the indexset,
with duplicates if specified by the indexset.
So for example
indexset_resolve 6 3..0,2,4,end
would return 3 2 1 0 2 4 5"
pairs
" return a list of index pairs representing the start and end of each range,
which may be increasing or decreasing, or just a single index
(where start and end are the same).
So for example
indexset_resolve -return pairs 6 3..0,2,4,end
would return {3 0} {2 2} {4 5}
indexset_resolve -return pairs 7 3..0,2,4,end
would return {3 0} {2 2} {4 4} {6 6}"
}
@values -min 2 -max 3
numitems -type integer
indexset -type indexset -help "comma delimited specification for indices to return"
@ -4948,32 +4962,66 @@ namespace eval punk::lib {
# for the unhappy path - the punk::args::parse is fine to generate the usage/error information.
# --------------------------------------------------
if {[llength $args] < 2} {
#too few args - use parser to generate error message
punk::args::resolve $args withid ::punk::lib::indexset_resolve
}
set indexset [lindex $args end]
set numitems [lindex $args end-1]
set indexset [lindex $args end]
if {![string is integer -strict $numitems] || ![is_indexset $indexset]} {
#use parser on unhappy path only
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
#assert we have 2 or more args
set optlist [lrange $args 0 end-2]
if {[llength $optlist] % 2 != 0} {
#options should come in pairs
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype "indices" ;#default
set base 0 ;#default
if {[llength $args] > 2} {
#if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
if {[llength $args] != 4} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set optname [lindex $args 0]
set optval [lindex $args 1]
set fulloptname [tcl::prefix::match -error "" -base $optname]
if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
dict for {opt val} $optlist {
set fulloptname [tcl::prefix::match -error "" {-base -return} $opt]
switch -exact -- $fulloptname {
-return {
set fullval [tcl::prefix::match -error "" {indices pairs} $val]
if {$fullval ni {indices pairs}} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype $fullval
}
-base {
if {![string is integer -strict $val]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set base $val
}
default {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
}
set base $optval
}
#set base 0 ;#default
#if {[llength $args] > 2} {
# #if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
# if {[llength $args] != 4} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set optname [lindex $args 0]
# set optval [lindex $args 1]
# set fulloptname [tcl::prefix::match -error "" -base $optname]
# if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set base $optval
#}
# --------------------------------------------------
@ -5160,8 +5208,84 @@ namespace eval punk::lib {
}
}
}
if {$returntype eq "pairs"} {
return [indices_to_pairs $index_list]
}
return $index_list
}
proc indices_to_pairs {indices} {
#convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing.
#if the direction of the run changes - we end the previous run and start a new one
if {[llength $indices] == 0} {
return [list]
}
set pairs [list]
set start [lindex $indices 0]
set prev $start
set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing
for {set i 1} {$i < [llength $indices]} {incr i} {
set idx [lindex $indices $i]
if {![string is integer -strict $idx]} {
error "non-integer index '$idx' in indices list"
}
if {$idx == $prev + 1} {
#increase since prev
if {$direction == 0} {
set direction 1
} elseif {$direction == -1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
} else {
#still increasing
}
set prev $idx
} elseif {$idx == $prev - 1} {
#decrease since prev
if {$direction == 0} {
set direction -1
} elseif {$direction == 1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
}
set prev $idx
} else {
#run ended - add pair to list
lappend pairs [list $start $prev]
set start $idx
set prev $idx
set direction 0
}
}
# add final run
lappend pairs [list $start $prev]
return $pairs
}
#proc indices_to_pairs {indices} {
# #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices
# set pairs [list]
# set start [lindex $indices 0]
# set prev $start
# for {set i 1} {$i < [llength $indices]} {} {
# set idx [lindex $indices $i]
# if {$idx == $prev + 1} {
# #still in a run
# set prev $idx
# } else {
# #run ended - add pair to list
# lappend pairs [list $start $prev]
# set start $idx
# set prev $idx
# }
# }
# # add final run
# lappend pairs [list $start $prev]
# return $pairs
#}
# showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side
#This doesn't need the list itself - just the length suffices.
punk::args::define {
@ -7835,6 +7959,77 @@ namespace eval punk::lib {
}
}
#sugar
#exclusive end - more intuitive for some cases and more consistent with other languages
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::lib::FOR
@cmd -name punk::lib::FOR\
-summary\
"BASIC style integer 'for loop'"\
-help\
"Sugar syntax for a common looping pattern.
The loop variable takes on values from start to end (exclusive) in increments of step.
If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end.
This is a common looping pattern that isn't directly supported by Tcl's built in control structures,
and this syntax is more concise for convenient interactive usage. For example:
FOR i 0 10 {puts $i}
will print the numbers 0 to 9.
This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops,
so may not be suitable for performance critical inner loops.
See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop
"
@values -min 3 -max 4
varname -type string -help "loop variable name"
start -type integer -help "initial value for loop variable"
end -type integer -help "end value for loop variable (exclusive)"
step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)"
script -type script -help "script to execute for each loop iteration"
}]
}
proc FOR { var args } {
switch -- [llength $args] {
3 {
# FOR x start end {}
lassign $args start end script
set step [ expr {$start > $end ? - 1 : 1} ]
}
4 {
# FOR x start end step {}
lassign $args start end step script
}
default {
error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script"
}
}
if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} {
error "FOR: start,end and step values must be integers"
}
upvar $var loopVar
set loopVar [expr {$start - $step}]
#to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition
if {$start < $end} {
if {$step <= 0} {
error "FOR: step value must be positive when start < end"
}
while {[incr loopVar $step] < $end} {
uplevel $script
}
} else {
if {$step >= 0} {
error "FOR: step value must be negative when start > end"
}
while {[incr loopVar $step] > $end} {
uplevel $script
}
}
}
#review - there are various type of uuid - we should use something consistent across platforms
#twapi is used on windows because it's about 5 times faster - but is this more important than consistency?
#twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway

39
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/base-0.1.tm

@ -677,7 +677,7 @@ namespace eval punk::mix::base {
if {$opt_use_tar != 0} {
set target [file tail $path]
set tmplocation [punk::mix::util::tmpdir]
set archivename $tmplocation/[punk::mix::util::tmpfile].tar
set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file.
cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues)
@ -687,14 +687,38 @@ namespace eval punk::mix::base {
set tsstart [clock millis]
if {[set tarpath [auto_execok tar]] ne ""} {
#using an external binary is *significantly* faster than tar::create - but comes with some risks
#review - need to check behaviour/flag variances across platforms
set versioninfo [exec {*}$tarpath --version]
#look for "bsdtar" vs "GNU"
#GNU tar is more common on linux - but also available on windows via gnuutils or msys
#<system32>/tar.exe on windows is likely to be bsdtar.
#GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag
#review - need to further check behaviour/flag variances across platforms
#don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum
#also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?)
exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " tar -cf done ($ms ms)"
} else {
if {[string match "*GNU*" $versioninfo]} {
set flags "--force-local"
} else {
#presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths
set flags ""
}
if {[catch {
#{*}$tarpath needed in case spaces in tarpath
exec {*}$tarpath -cf {*}$flags $archivename $target
} errMsg]} {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg"
} else {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' done ($ms ms)"
}
}
if {![file exists $archivename]} {
#fallback to tar library approach if external tar failed to create the archive.
set tsstart [clock millis] ;#don't include auto_exec search time for tar::create
tar::create $archivename $target
set tsend [clock millis]
@ -703,6 +727,7 @@ namespace eval punk::mix::base {
puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing"
}
if {$ftype eq "file"} {
set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)"
} else {

32
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm

@ -333,11 +333,11 @@ namespace eval punk::mix::cli {
set defaults [list {*}{
-errorprefix projectname
}]
if {[llength $args] %2 != 0} {error "validate_modulename args must be name-value pairs: received '$args'"}
if {[llength $args] %2 != 0} {error "validate_projectname args must be name-value pairs: received '$args'"}
set known_opts [dict keys $defaults]
foreach k [dict keys $args] {
if {$k ni $known_opts} {
error "validate_modulename error: unknown option $k. known options: $known_opts"
error "validate_projectname error: unknown option $k. known options: $known_opts"
}
}
set opts [dict merge $defaults $args]
@ -381,34 +381,6 @@ namespace eval punk::mix::cli {
return $name
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[punk::mix::util::is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
#
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
proc get_status {{workingdir ""} args} {
set result ""

16
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm

@ -9,7 +9,7 @@
# @@ Meta Begin
# Application punk::mix::commandset::module 0.1.0
# Meta platform tcl
# Meta license BSD
# Meta license BSD
# @@ Meta End
@ -204,7 +204,7 @@ namespace eval punk::mix::commandset::module {
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_version_supplied [dict get $opts -version]
set opt_version $opt_version_supplied
if {![util::is_valid_tm_version $opt_version]} {
if {![punk::mix::util::is_valid_tm_version $opt_version]} {
error "deck module.new error - supplied -version $opt_version doesn't appear to be a valid Tcl module version"
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -213,8 +213,8 @@ namespace eval punk::mix::commandset::module {
set mversion_supplied "" ;#version supplied directly in module argument
if {[string first - $module]> 0} {
#if it has a dash then version is required to be valid
lassign [punk::mix::cli::lib::split_modulename_version $module] modulename mversion
if {![util::is_valid_tm_version $mversion]} {
lassign [punk::mix::util::split_modulename_version $module] modulename mversion
if {![punk::mix::util::is_valid_tm_version $mversion]} {
error "deck module.new error - unable to determine modulename-version from supplied value '$module'"
}
set mversion_supplied $mversion ;#record as may need to compare to version from templatefile name
@ -315,7 +315,7 @@ namespace eval punk::mix::commandset::module {
} else {
set module [string range [string length $vendor.] end]
}
lassign [punk::mix::cli::lib::split_modulename_version $m] _tailmname mversion
lassign [punk::mix::util::split_modulename_version $m] _tailmname mversion
lappend key_version_list [list $m $mversion]
}
if {[llength $matches]} {
@ -383,7 +383,7 @@ namespace eval punk::mix::commandset::module {
} else {
error "module.new error: Unable to interpret filename components of template file '$templatefile'"
}
lassign [punk::mix::cli::lib::split_modulename_version $template_modulename_part] t_mname t_version
lassign [punk::mix::util::split_modulename_version $template_modulename_part] t_mname t_version
#t_version may be empty string if template is unversioned e.g template_whatever.tm
set fd [open $templatefile r]; set template_filedata [read $fd]; close $fd
@ -398,7 +398,7 @@ namespace eval punk::mix::commandset::module {
} else {
#
if {[util::is_valid_tm_version $t_version]} {
if {[punk::mix::util::is_valid_tm_version $t_version]} {
if {$mversion_supplied eq ""} {
set build_version $t_version
} else {
@ -500,7 +500,7 @@ namespace eval punk::mix::commandset::module {
set name_version_pairs [list]
lappend name_version_pairs [list $moduletail $infile_version]
foreach existing $existing_versions {
lassign [punk::mix::cli::lib::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
lassign [punk::mix::util::split_modulename_version $existing] namepart version ;# .tm is stripped and ignored
if {[string match #modpod-* $namepart]} {
set namepart [string range $namepart 8 end]
}

30
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/util-0.1.0.tm

@ -330,6 +330,35 @@ namespace eval punk::mix::util {
return 0
}
}
#split modulename (as present in a filename or namespaced name) into name/version ignoring leading namespace path
#ignore trailing .tm .TM if present
#if version doesn't pass validation - treat it as part of the modulename and return empty version string without error
#Up to caller to validate.
proc split_modulename_version {fullmodulename} {
set lastpart [namespace tail $fullmodulename]
set lastpart [file tail $lastpart] ;# should be ok to use file tail now that we've ensured no namespace components
if {[string equal -nocase [file extension $fullmodulename] ".tm"]} {
set fileparts [split [file rootname $lastpart] -]
} else {
set fileparts [split $lastpart -]
}
if {[is_valid_tm_version [lindex $fileparts end]]} {
set versionsegment [lindex $fileparts end]
set namesegment [join [lrange $fileparts 0 end-1] -];#re-stitch
} else {
set namesegment [join $fileparts -]
set versionsegment ""
}
set base [namespace qualifiers $fullmodulename]
if {$base ne ""} {
set modulename "${base}::$namesegment"
} else {
set modulename $namesegment
}
return [list $modulename $versionsegment]
}
#Note that semver only has a small overlap with tcl tm versions.
#todo - work out what overlap and whether it's even useful
#see also TIP #439: Semantic Versioning (tcl 9??)
@ -337,6 +366,7 @@ namespace eval punk::mix::util {
set re {^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$}
}
#todo - semver conversion/validation for other systems?
proc magic_tm_version {} {
set magicbase 999999 ;#deliberately large so given load-preference when testing!
#we split the literal to avoid the literal appearing here - reduce risk of accidentally converting to a release version

5
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm

@ -4186,7 +4186,7 @@ y" {return quirkykeyscript}
#this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces)
#we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace.
nseval $targetns [list apply [list {procname argname} {
set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]]
set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]]
if {$has_default} {
set answer [dict create exists 1 default $::punk::ns::corp_defvar]
} else {
@ -5025,7 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
#puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -7191,6 +7191,7 @@ y" {return quirkykeyscript}
nstest eval {package require punk::ns}
set ns ""
if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} {
#review
set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}]
set ns [nstest eval $script]
} else {

23
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm

@ -110,6 +110,19 @@ tcl::namespace::eval punk::packagepreference {
#[para]This comes at some slight cost for packages that are only available with uppercase letters in the name - but at minimal cost for recommended lowercase package names
#[para]Return to the standard ::package builtin by calling punk::packagepreference::uninstall
if {![catch {commandstack::get_stack} cstack]} {
if {[dict exists $cstack ::package]} {
set pstack [dict get $cstack ::package]
foreach record $pstack {
if {[dict get $record rename] eq "punk::packagepreference"} {
#already installed - silently ignore.
return 0
}
}
}
}
#todo - review/update commandstack package
#modern module/lib names should preferably be lower case
#see tip 590 - "Recommend lowercase Package names". Where non-lowercase are deprecated (but not removed even in Tcl9)
@ -170,7 +183,10 @@ tcl::namespace::eval punk::packagepreference {
if {[llength $pkgloadedinfo]} {
if {[llength $available_versions] > 1} {
puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
#puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available. $available_versions threadid: $threadid"
}
lassign $pkgloadedinfo loaded_path name
set lc_loadedpath [string tolower $loaded_path]
@ -302,7 +318,10 @@ tcl::namespace::eval punk::packagepreference {
puts stderr "Failed to load punk::args::moduledoc::$dp - error was: $errMsg"
}
} else {
puts stdout "Loaded punk::args::moduledoc::$dp for package $pkg threadid: [thread::id]"
if {[catch {thread::id} threadid]} {
set threadid "unknown-threadid"
}
puts stdout "punk::packagepreference overloaded 'package require': Loaded punk::args::moduledoc::$dp for package $pkg threadid: $threadid"
}
}
#---------------------------------------------------------------

133
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm

@ -432,7 +432,7 @@ proc repl::start {inchan args} {
variable codethread
#review
if {$codethread eq ""} {
error "start - no codethread. call init first. (options -safe 0|1)"
error "start - no codethread. call init first. (options -type 0|1)"
}
variable commandstr
@ -478,7 +478,7 @@ proc repl::start {inchan args} {
#set ::punk::repl::codethread::running 1
#the interp in which commands such as d/ run
#we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values
#we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values
#punk::repl::codethread::running is required whether safe or not.
interp eval code {
namespace eval ::punk::repl::codethread {}
@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} {
}
#add to sliding buffer of last x chars emmitted to screen by repl
# add to sliding buffer of last x chars emmitted to screen by repl
#(we could maintain only one char - more kept merely for debug assistance)
#will not detect emissions from exec with stdout redirected and presumably some extensions etc
proc repl::screen_last_char_add {c what {why ""}} {
@ -2123,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#if {$chunk eq "\x1b\[C"} {
#}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_off
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
$editbuf add_chunk $chunk
@ -2235,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
lappend input_chunks_waiting($inputchan) $waiting
}
}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_on
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
if {$editbuf_linenum_submitted == 0} {
#(there is no line 0 - lines start at 1)
@ -2264,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap1 POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error1 in repl_handler: $repl_error"
rputs stderr "error1 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "erroropts: $erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2485,7 +2489,13 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
variable codethread_cond
variable codethread_mutex
lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#JJJJ JMN test
#REVIEW
#lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#thread::transfer $codethread stderr
#chan configure stdout -buffering none
@ -2939,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error in repl_handler: $repl_error"
rputs stderr "error2 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "$erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2986,10 +2996,10 @@ namespace eval repl {
variable codethread_cond
variable codethread_mutex
set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
foreach {k v} $args {
switch -- $k {
-force - -safe - -safelog - -paths - -callback_interp {
-force - -type - -safelog - -paths - -callback_interp {
dict set opts $k $v
}
default {
@ -2998,7 +3008,7 @@ namespace eval repl {
}
}
set opt_force [dict get $opts -force]
set opt_safe [dict get $opts -safe]
set opt_repltype [dict get $opts -type]
set opt_safelog [dict get $opts -safelog]
if {$opt_safelog eq "0"} {
set opt_safelog ""
@ -3113,46 +3123,50 @@ namespace eval repl {
package require punk::args
#package require Thread
if {[catch {package require thread} errM]} {
puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
#if {[catch {package require thread} errM]} {
# puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
if {[catch {package require Thread} errM2]} {
puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2"
puts stdout ">>repl::init auto_path : $::auto_path"
puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]"
}
}
#}
namespace eval ::punk::repl::codethread {}
# -------------------------------------------------------------------------------------------------------------------------------------------------------
#-----
#review - icomm as a possible way to talk to thread outside of the code interp.
#thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow
#thread::send to caller defined interp targets (reference?)
#snit required for icomm
if {[catch {package require snit} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
}
if {[catch {package require punk::icomm} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
}
#if {[catch {package require snit} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
#}
#if {[catch {package require punk::icomm} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
#}
#-----
namespace eval ::punk::repl::codethread {}
#todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
catch {package require tcl::chan::fifo2}
if {[catch {
#first use can raise error being a version number e.g 0.1.0 - why?
lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
} errMsg]} {
puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
} else {
#experimental?
#puts stdout "transferring chan $replside to thread %replthread%"
#flush stdout
#if {[catch {
# #after 0 [list thread::transfer %replthread% $replside]
#} errMsg]} {
# #puts stdout "---thread::transfer error: $errMsg"
#}
}
#----------------------------------------------------------------------------------------------------------------------
# todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
# catch {package require tcl::chan::fifo2}
# if {[catch {
# #first use can raise error being a version number e.g 0.1.0 - why?
# lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
# } errMsg]} {
# puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
# } else {
# #experimental?
# #puts stdout "transferring chan $replside to thread %replthread%"
# #flush stdout
# # if {[catch {
# # #after 0 [list thread::transfer %replthread% $replside]
# # } errMsg]} {
# # #puts stdout "---thread::transfer error: $errMsg"
# # }
# }
#----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------------------------------------------------------------------
package require punk::console
package require punk::repl::codethread
@ -3333,7 +3347,7 @@ namespace eval repl {
set ts_start [clock seconds]
set replresult [interp eval code {
package require punk::repl
repl::init -safe punk
repl::init -type punk
repl::start stdin
}]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3343,7 +3357,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe safe {*}$args]
interp eval code [list repl::init -type safe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3353,7 +3367,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
set codethread [interp eval code [list repl::init -safe safebase {*}$args]]
set codethread [interp eval code [list repl::init -type safebase {*}$args]]
puts stdout "safebase codethread:$codethread"
set replresult [interp eval code [list repl::start stdin]]
@ -3364,7 +3378,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe punksafe {*}$args]
interp eval code [list repl::init -type punksafe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3376,14 +3390,14 @@ namespace eval repl {
#flush stdout
set args %args%
set safe [dict get $args -safe]
set repltype [dict get $args -type]
set safelog [dict get $args -safelog]
set paths [list]
if {[dict exists $args -paths]} {
set paths [dict get $args -paths]
}
switch -- $safe {
switch -- $repltype {
safe {
interp create -safe -- code
code eval [list namespace eval ::punk::libunknown {}]
@ -3444,7 +3458,7 @@ namespace eval repl {
#todo a specific punk::libunknown 'package unknown' handler for safe interps
#pull in code via calls to source cached code?
switch -- $safe {
switch -- $repltype {
safe {
if {[llength $paths]} {
package require punk::island
@ -3797,8 +3811,27 @@ namespace eval repl {
}
punk - 0 {
#------------------------------------------------------------------------
#Test - experimental 2026
if {"stdout" in [chan names]} {
interp share {} stdout code
} else {
interp share {} [shellfilter::stack::item_tophandle stdout] code
}
if {"stderr" in [chan names]} {
interp share {} stderr code
} else {
interp share {} [shellfilter::stack::item_tophandle stderr] code
}
foreach nm {shellspyout shellspyerr} {
set thandle [shellfilter::stack::item_tophandle $nm]
if {$thandle ne ""} {
interp share {} $thandle code
}
}
#------------------------------------------------------------------------
interp eval code {
#safe !=1 and safe !=2, tmlist: %tmlist%
#repltype !=1 and repltype !=2, tmlist: %tmlist%
set ::argv0 %argv0%
set ::argv %argv%
set ::argc %argc%
@ -3978,8 +4011,8 @@ namespace eval repl {
thread::id
}
set init_script [string map $scriptmap $init_script]
#REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args%
#we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that?
#REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args%
#we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that?
#thread::send $codethread $init_script
if {![catch {
@ -3994,7 +4027,7 @@ namespace eval repl {
error $errMsg
}
}
#init - don't auto init - require init with possible options e.g -safe
#init - don't auto init - require init with possible options e.g -type
}
package provide punk::repl [namespace eval punk::repl {
variable version

34
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repo-0.1.1.tm

@ -83,34 +83,38 @@ namespace eval punk::repo {
proc get_fossil_usage {} {
set allcmds [runout -n fossil help -a]
set allcmds [punk::ansi::ansistrip $allcmds]
set mainhelp [runout -n fossil help]
set mainhelp [punk::ansi::ansistrip $mainhelp]
set maincommands [list]
#only start parsing for TOPICS after a line such as "Other comman values for TOPIC:"
set parsing_topics 0
foreach ln [split $mainhelp \n] {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
lappend maincommands {*}$ln
}
lappend maincommands {*}$ln
}
}
#fossil output was ordered in columns, but we loaded list in row-wise, messing up the order
set maincommands [lsort $maincommands]
set allcmds [lsort $allcmds]
set othercmds [punk::lib::ldiff $allcmds $maincommands]
set fossil_setting_names [lsort [runout -n fossil help -s]]
set setting_info [runout -n fossil help -s]
set setting_info [punk::ansi::ansistrip $setting_info]
set fossil_setting_names [lsort $setting_info]
set result "@leaders -min 0\n"
@ -186,6 +190,8 @@ namespace eval punk::repo {
foreach ln $basic_opt_lines {
set ln [string trim $ln]
#fossil sometimes emits cursor control sequences e.g CSI 3 q
set ln [punk::ansi::ansistrip $ln]
if {$ln eq ""} {
continue
}
@ -250,6 +256,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage add]}
@form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}}
} ""]
@ -264,6 +271,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage diff]}
@form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}}
} ""]

20
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm

@ -121,14 +121,6 @@ namespace eval punkcheck {
}
method as_record {} {
#set fields [list\
# -targets $o_targets\
# -keep_installrecords $o_keep_installrecords\
# -keep_skipped $o_keep_skipped\
# -keep_inprogress $o_keep_inprogress\
# body $o_records\
#]
dict create {*}{
} tag FILEINFO {*}{
} -targets $o_targets {*}{
@ -216,18 +208,6 @@ namespace eval punkcheck {
} else {
set tsiso_end ""
}
#set fields [list\
# -tsiso_begin $tsiso_begin\
# -ts_begin $o_ts_begin\
# -tsiso_end $tsiso_end\
# -ts_end $o_ts_end\
# -id $o_id\
# -source $o_rel_sourceroot\
# -targets $o_rel_targetroot\
# -types $o_types\
# -config $o_configdict\
#]
#set record [dict create tag EVENT {*}$fields]
dict create {*}{
} tag EVENT {*}{

444
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm

@ -67,6 +67,8 @@ tcl::namespace::eval shellfilter::log {
}
proc ::shellfilter::log::close {tag} {
#shellthread::manager::close_worker $tag
#unsubscribe involves a thread::send to the worker thread if we are the last subscriber to the tag.
shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed
}
@ -106,6 +108,13 @@ namespace eval shellfilter::pipe {
package require shellthread
#we are only using the fifo in a single direction to pipe to another thread
# - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each
#----------------------------------------------------------------------------------------
# Differences beetween Memchan's fifo2 and tcl::chan::fifo2 implementations:
# (not necessarily a comprehensive list)
# - Memchan's fifo2 is reportedly faster and more efficient than tcl::chan::fifo2, but it may not be available on all platforms
# tcl::chan::fifo2 is a pure Tcl implementation.
# - Closing one side of a tcl::chan::fifo2 (ver 1.1) will cause the other side to close whereas this is not the case with Memchan's fifo2.
#----------------------------------------------------------------------------------------
if {![catch {package require Memchan}]} {
lassign [fifo2] wchan rchan
} else {
@ -289,7 +298,8 @@ namespace eval shellfilter::chan {
set o_enc [tcl::dict::get $tf -encoding]
set o_encbuf ""
set settingsdict [tcl::dict::get $tf -settings]
set varname [tcl::dict::get $settingsdict -varname]
set varname [tcl::dict::get $settingsdict -varname] ;#review - should we support multiple vars here? e.g with a list of varnames in settings and append to all of them?
#these are not upvared - must be fully qualified variable names.
set o_datavars $varname
if {[tcl::dict::exists $tf -junction]} {
set o_is_junction [tcl::dict::get $tf -junction]
@ -298,18 +308,18 @@ namespace eval shellfilter::chan {
}
}
method initialize {ch mode} {
return [list initialize finalize write flush clear]
if {"read" in $mode} {
#this should raise an error in 'chan push'
error "shellfilter::chan::tee_to_var transform does not support read mode"
}
return [list initialize finalize write flush]
}
method finalize {ch} {
my destroy
}
method clear {ch} {
return
}
method watch {ch events} {
# must be present but we ignore it because we do not
# post any events
}
#method clear {ch} {
# return
#}
#method read {ch count} {
# return ?
#}
@ -320,9 +330,14 @@ namespace eval shellfilter::chan {
#puts stdout "<flush>"
#review - just clear o_encbuf and emit nothing?
#we wouldn't have a value there if it was convertable from the channel encoding?
set clear $o_encbuf
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_var with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {ch bytes} {
#test with set x [string repeat " \U1f6c8" 2043]
@ -387,10 +402,12 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read drain write flush clear finalize]
#return [list initialize read drain write flush clear finalize]
return [list initialize write flush clear finalize]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#Note that an error in the finalize can stop 'chan pop' from running properly.
#::shellfilter::log::close $o_logsource
my destroy
}
method watch {transform_handle events} {
@ -398,26 +415,53 @@ namespace eval shellfilter::chan {
# post any events
}
method clear {transform_handle} {
set o_encbuf ""
return
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set logdata [tcl::encoding::convertfrom $o_enc $bytes]
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $logdata
return $bytes
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set logdata [tcl::encoding::convertfrom $o_enc $bytes]
# #::shellfilter::log::write $o_logsource $logdata
# puts -nonewline $o_localchan $logdata
# return $bytes
#}
#method flush {transform_handle} {
# #return ""
# set clear $o_encbuf[set o_encbuf ""]
# if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
# #if we can't convert the buffer contents to a string - put it back and try again with more data later
# #REVIEW?
# set o_encbuf $clear
# puts -nonewline $o_localchan ""
# return ""
# }
# #jjj
# puts -nonewline $o_localchan $stringdata
# flush $o_localchan
# return $clear
#}
method flush {transform_handle} {
#return ""
set clear $o_encbuf
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the channel or var?
#REVIEW - log that we are discarding the buffer contents on flush?
puts stderr "WARNING: flush called on tee_to_pipe with non-empty buffer. This probably indicates an encoding mismatch between the channel encoding and the encoding expected by the transform. Discarding buffer contents: '$o_encbuf'"
}
set o_encbuf ""
return $o_encbuf
return ""
}
method write {transform_handle bytes} {
#set logdata [tcl::encoding::convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
if {$inputbytes eq ""} {
#review - do we even get empty writes?
puts stderr "WARNING: write called on tee_to_pipe with empty inputbytes. This may be a no-op, but it may also indicate an issue with the upstream transform or channel. Emitting no data to the pipe for this write."
return ""
}
set o_encbuf ""
set tail_offset 0
while {$tail_offset < [::tcl::string::length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [::tcl::string::range $inputbytes 0 end-$tail_offset]} stringdata]} {
@ -426,18 +470,23 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
#nothing convertable in the buffer - put it back and try again with more data later
set stringdata ""
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
#::shellfilter::log::write $o_logsource $logdata
puts -nonewline $o_localchan $stringdata
#return $bytes
return [::tcl::string::range $inputbytes 0 end-$tail_offset]
return $converted ;#same data as $stringdata but in bytes form.
}
#a tee is not a redirection - because data still flows along the main path
method meta_is_redirection {} {
@ -501,6 +550,7 @@ namespace eval shellfilter::chan {
if {$tail_offset > 0} {
if {$tail_offset < [::tcl::string::length $inputbytes]} {
#stringdata from catch statement must be a valid result
set converted [::tcl::string::range $inputbytes 0 end-$tail_offset]
set t [expr {$tail_offset - 1}]
set o_encbuf [::tcl::string::range $inputbytes end-$t end]
} else {
@ -508,10 +558,15 @@ namespace eval shellfilter::chan {
set o_encbuf $inputbytes
return ""
}
} else {
#no catch on conversion - everything is convertable
#stringdata must be valid result from convertfrom of whole $o_encbuf$bytes
set converted $inputbytes
}
set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
#set bytes [::tcl::string::range $inputbytes 0 end-$tail_offset]
::shellfilter::log::write $o_logsource $stringdata
return $bytes
#return $bytes
return $converted ;#same data as $stringdata but in bytes form.
}
method meta_is_redirection {} {
return $o_is_junction
@ -519,6 +574,10 @@ namespace eval shellfilter::chan {
}
#see TIP 230 Tcl Channel Transformation Reflection API
#the logonly transform does not emit any data downwards towards the base channel - it only writes to the log.
# - ie we are redirecting all data to the log.
oo::class create logonly {
variable o_tid
variable o_logsource
@ -537,19 +596,35 @@ namespace eval shellfilter::chan {
set o_tid [::shellfilter::log::open $o_logsource $settingsdict]
}
method initialize {transform_handle mode} {
return [list initialize finalize write]
#mode is a list containing any of the strings 'read' or 'write'.
if {"read" in $mode} {
#Note that raising an error prevents creation of the transformation.
#The thrown error will appear as a error thrown by 'chan push'.
error "logonly transform does not support read mode"
}
#return all methods supported by the handler
#(we exclude our additional meta_is_redirection method becuase it is not a standard transform method called by the Tcl framework - it's only for our own use in shellfilter)
return [list initialize finalize write flush]
}
method finalize {transform_handle} {
::shellfilter::log::close $o_logsource
#review - tip 230 states that any return value or error raised by finalize is ignored
#we wrap in catch to ensure the 'my destroy' is always called.
catch {::shellfilter::log::close $o_logsource}
my destroy
}
method watch {transform_handle events} {
# must be present but we ignore it because we do not
# post any events
#clear?
method flush {transform_handle} {
#we wouldn't have a value in o_encbuf if it was convertable from the channel encoding?
if {[string length $o_encbuf]} {
#if we have data in the buffer that we haven't been able to convert to a string
#- then we probably have some kind of encoding mismatch. Is it safer to discard it than to emit garbage chars to the log?
#REVIEW. - we are writing the raw bytes to the log here because we can't convert them to a string.
#This may be useful for debugging issues, but it may also result in garbage data in the log.
::shellfilter::log::write $o_logsource $o_encbuf
set o_encbuf ""
}
return
}
#method read {transform_handle count} {
# return ?
#}
method write {transform_handle bytes} {
#set logdata [encoding convertfrom $o_enc $bytes]
set inputbytes $o_encbuf$bytes
@ -600,26 +675,49 @@ namespace eval shellfilter::chan {
}
}
method initialize {transform_handle mode} {
return [list initialize read write clear flush drain finalize]
#return [list initialize read write clear flush drain finalize]
#REVIEW - we aren't using 'read' mode - but if we raise an error if 'read' is in the mode then the system doesn't work.
#this is probably because we add it to one end of a fifo2 channel, which although we only use for writing is a bidirectional channel.
#----------------------
#don't do this
#----------------------
#if {"read" in $mode} {
# #Note that raising an error prevents creation of the transformation.
# #The thrown error will appear as a error thrown by 'chan push'.
# error "shellfilter::chan::ansistrip transform does not support read mode"
#}
#----------------------
return [list initialize read write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method clear {transform_handle} {
return
}
method watch {transform_handle events} {
}
method drain {transform_handle} {
return ""
}
#method clear {transform_handle} {
# return
#}
#method drain {transform_handle} {
# return ""
#}
method read {transform_handle bytes} {
set instring [encoding convertfrom $o_enc $bytes]
set outstring [punk::ansi::ansistrip $instring]
return [encoding convertto $o_enc $outstring]
}
#method flush {transform_handle} {
# return ""
#}
method flush {transform_handle} {
return ""
#return ""
set clear $o_encbuf[set o_encbuf ""]
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - put it back and try again with more data later
#REVIEW?
set o_encbuf $clear
return ""
}
#review
return $stringdata
}
#method write {transform_handle bytes} {
# #broken due to occasional unexpected byte sequence
@ -989,29 +1087,44 @@ namespace eval shellfilter::chan {
}
method initialize {transform_handle mode} {
#clear undesirable in terminal output channels (review)
return [list initialize write flush read drain finalize]
#return [list initialize write flush read drain finalize]
if {$mode eq "read"} {
error "shellfilter::chan::ansiwrap channel transform does not support read mode"
}
return [list initialize write flush finalize]
}
method finalize {transform_handle} {
my destroy
}
method watch {transform_handle events} {
}
method clear {transform_handle} {
#In the context of stderr/stdout - we probably don't want clear to run.
#Terminals might call it in the middle of a split ansi code - resulting in broken output.
#Leave clear of it the init call
#Leave clear out of the initialize call for now
puts stdout "<clear>"
set emit [tcl::encoding::convertto $o_enc $o_buffered]
set o_buffered ""
return $emit
}
#method flush {transform_handle} {
# #puts stdout "<flush>"
# set inputbytes $o_buffered$o_encbuf
# set emit [tcl::encoding::convertto $o_enc $inputbytes]
# set o_buffered ""
# set o_encbuf ""
# return $emit
#}
method flush {transform_handle} {
#puts stdout "<flush>"
set inputbytes $o_buffered$o_encbuf
set emit [tcl::encoding::convertto $o_enc $inputbytes]
#return ""
set clear $o_buffered$o_encbuf
if {[catch {tcl::encoding::convertfrom $o_enc $clear} stringdata]} {
#if we can't convert the buffer contents to a string - does it make sense to emit the raw bytes?
# - probably not.
#REVIEW?
return ""
}
set o_buffered ""
set o_encbuf ""
return $emit
return $stringdata
}
method write {transform_handle bytes} {
#set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally
@ -1076,14 +1189,14 @@ namespace eval shellfilter::chan {
#set outstring ">>>$instring"
return [tcl::encoding::convertto $o_enc $outstring]
}
method drain {transform_handle} {
return ""
}
method read {transform_handle bytes} {
set instring [tcl::encoding::convertfrom $o_enc $bytes]
set outstring "$o_do_colour$instring$o_do_normal"
return [tcl::encoding::convertto $o_enc $outstring]
}
#method drain {transform_handle} {
# return ""
#}
#method read {transform_handle bytes} {
# set instring [tcl::encoding::convertfrom $o_enc $bytes]
# set outstring "$o_do_colour$instring$o_do_normal"
# return [tcl::encoding::convertto $o_enc $outstring]
#}
method meta_is_redirection {} {
return $o_is_junction
}
@ -1326,9 +1439,11 @@ namespace eval shellfilter::stack {
}
proc status {{pipename *} args} {
variable pipelines
package require textblock
set pipecount [dict size $pipelines]
set tabletitle "$pipecount pipelines active"
set t [textblock::class::table new $tabletitle]
$t configure -frametype ascii; #be conservative here - may need to emit in various debugging contexts.
$t add_column -headers [list channel-ident]
$t add_column -headers [list device-info localchan]
$t configure_column 1 -header_colspans {3}
@ -1337,6 +1452,9 @@ namespace eval shellfilter::stack {
$t add_column -headers [list stack-info]
foreach k [dict keys $pipelines $pipename] {
set lc [dict get $pipelines $k device localchan]
if {[catch {chan configure $lc -encoding} lc_enc]} {
set lc_enc "<err>"
}
set rc [dict get $pipelines $k device remotechan]
if {[dict exists $k device workertid]} {
set tid [dict get $pipelines $k device workertid]
@ -1348,18 +1466,35 @@ namespace eval shellfilter::stack {
set stackinfo ""
} else {
set tbl_inner [textblock::class::table new]
$tbl_inner configure -frametype ascii
$tbl_inner configure -show_edge 0
$tbl_inner add_column -headers id
$tbl_inner add_column -headers transform
$tbl_inner add_column -headers handle
$tbl_inner add_column -headers settings
$tbl_inner add_column -headers aside
foreach rec $stack {
set handle [punk::lib::dict_getdef $rec -handle ""]
set id [punk::lib::dict_getdef $rec -id ""]
set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]]
set handle [punk::lib::dict_getdef $rec -handle ""]
if {$handle ne ""} {
if {[catch {chan configure $handle -encoding} handle_enc]} {
set handle_enc "<err>"
}
} else {
set handle_enc ""
}
set settings [punk::lib::dict_getdef $rec -settings ""]
$tbl_inner add_row [list $id $transform $handle $settings]
set aside [punk::lib::dict_getdef $rec -aside ""]
if {$aside ne ""} {
set aside [punk::lib::showdict $aside]
}
$tbl_inner add_row [list $id $transform $handle\n$handle_enc $settings $aside]
}
set stackinfo [$tbl_inner print]
$tbl_inner destroy
}
$t add_row [list $k $lc $rc $tid $stackinfo]
$t add_row [list $k "$lc\n$lc_enc" $rc $tid $stackinfo]
}
set result [$t print]
$t destroy
@ -1511,15 +1646,24 @@ namespace eval shellfilter::stack {
proc unwind {pipename} {
variable pipelines
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
foreach tf [lreverse $stack] {
chan pop $localchan
if {[catch {chan eof $localchan} _eof]} {
#We don't actually care about eof state - but we use this to test if the channel exists.
#('chan names' doesn't reliably show all channels in some cases)
#do nothing.
} else {
#if there are no transforms on the the channel - this is equivalent to 'chan close' of the channel
# but here we should only be calling it when there are transforms on the channel as indicated by the stack variable.
chan pop $localchan
}
}
dict set pipelines $pipename [list]
}
#todo
proc delete {pipename {wait 0}} {
#::shellfilter::log::open shellfilter-delete [list -syslog "127.0.0.1:514"]
variable pipelines
set pipeinfo [dict get $pipelines $pipename]
set deviceinfo [dict get $pipeinfo device]
@ -1534,18 +1678,29 @@ namespace eval shellfilter::stack {
thread::release $tid
}
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why?
#Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"'
#- REVIEW. why? It could have something to do with the fact that tcl::memchan::fifo2 closes both sides when one side is closed.
catch {chan close $localchan}
#if {[catch {chan close $localchan} errMsg]} {
# ::shellfilter::log::write shellfilter-delete "WARNING: error closing localchan '$localchan' for pipename '$pipename': $errMsg"
#}
}
#review - proc name clarity is questionable. remove_stackitem?
proc remove {pipename remove_id} {
#::shellfilter::log::open shellfilter-remove [list -syslog "127.0.0.1:514"]
variable pipelines
if {![dict exists $pipelines $pipename]} {
puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
#puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
::shellfilter::log::write shellfilter-remove "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]"
return
}
set stack [dict get $pipelines $pipename stack]
set localchan [dict get $pipelines $pipename device localchan]
set previous_blockingstate [chan configure $localchan -blocking]
if {$previous_blockingstate} {
chan configure $localchan -blocking 0
}
set posn 0
set idposn -1
set asideposn -1
@ -1572,10 +1727,11 @@ namespace eval shellfilter::stack {
dict set container -aside {}
lset stack $asideposn $container
dict set pipelines $pipename stack $stack
#::shellfilter::log::write shellfilter-remove "cleared '-aside' record for pipename $pipename aside_posn $asideposn remove_id:'$remove_id'"
} else {
if {$idposn < 0} {
::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
#::shellfilter::log::write shellfilter-remove "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found"
#puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found"
return 0
}
set removed_item [lindex $stack $idposn]
@ -1589,32 +1745,58 @@ namespace eval shellfilter::stack {
#pop all chans before adding anything back in!
foreach p $poplist {
#review
#update idletasks
#puts stderr "DEBUG> popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist]"
#::shellfilter::log::write shellfilter-remove "popping transform from pipename $pipename for stack p:$p poplist len:[llength $poplist] ---"
#after 0 [list chan pop $localchan]
chan pop $localchan
#::shellfilter::log::write shellfilter-remove "POPPED"
#update idletasks
}
#after 5
#::shellfilter::log::write shellfilter-remove "remove. popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
#puts stderr "DEBUG> popped all transforms above and including idposn $idposn for pipename $pipename poplist len:[llength $poplist]"
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
if {[llength [dict get $removed_item -aside]]} {
set restore [dict get $removed_item -aside]
set t [dict get $restore -transform]
set tsettings [dict get $restore -settings]
set obj [$t new $restore]
set h [chan push $localchan $obj]
dict set restore -handle $h
dict set restore -obj $obj
lappend stack $restore
#puts stderr "DEBUG> restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored aside for pipename $pipename asideposn $asideposn remove_id:'$remove_id' transform: $t handle:$h obj:$obj"
}
#after 5
#put popped back except for the first one, which we want to remove
foreach p [lrange $poplist 1 end] {
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set obj [$t new $p]
set h [chan push $localchan $obj]
dict set p -handle $h
dict set p -obj $obj
lappend stack $p
#update idletasks
#puts stderr "DEBUG> restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
#::shellfilter::log::write shellfilter-remove "restored for pipename $pipename id '$remove_id' transform:$t handle $h obj:$obj"
}
#after 5
dict set pipelines $pipename stack $stack
}
#puts stderr "DEBUG> pipename $pipename id '$remove_id' DONE"
#::shellfilter::log::write shellfilter-remove "pipename $pipename id '$remove_id' DONE"
if {$previous_blockingstate} {
chan configure $localchan -blocking 1
}
#JMNJMN 2025 review!
#show_pipeline $pipename -note "after_remove $remove_id"
return 1
@ -1853,6 +2035,41 @@ namespace eval shellfilter::stack {
namespace eval shellfilter {
variable sources [list]
variable stacks [dict create]
#-------------------------------------------------------------------------------------------------
#tcllib logger infrastructure.
#-------------------------------------------------------------------------------------------------
namespace eval ::shellfilter::loggerprocs {
#container for procs/aliases to be pointed to by tcllib logger using log::logproc
proc Dolog {lvl txt} {
#logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted
set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'"
puts stderr $msg
}
proc Runlog {lvl script} {
uplevel 1 $script
}
}
if {![catch {
package require logger
}]} {
logger::initNamespace ::shellfilter
foreach lvl [logger::levels] {
interp alias {} ::shellfilter::loggerprocs::Log_$lvl {} ::shellfilter::loggerprocs::Runlog $lvl
log::logproc $lvl ::shellfilter::loggerprocs::Log_$lvl
}
logger::setlevel warn
#namespace path ::shellfilter::log
} else {
#e.g tcllib not available, safe interp?
#fake out the logger calls
namespace eval ::shellfilter::log {
foreach lvl {debug info notice warn error critical alert emergency} {
proc $lvl {args} {}
}
}
}
#-------------------------------------------------------------------------------------------------
proc ::shellfilter::redir_channel_to_log {chan args} {
variable sources
@ -2396,13 +2613,9 @@ namespace eval shellfilter {
#must be a list. If it was a shell commandline string. convert it elsewhere first.
variable sources
set runtag "shellfilter-run"
#set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
set tid [::shellfilter::log::open $runtag [list -syslog ""]]
if {[catch {llength $commandlist} listlen]} {
set listlen "<not-a-tcl-list>"
}
::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"
#flush stdout
#flush stderr
@ -2416,6 +2629,7 @@ namespace eval shellfilter {
-errchan stderr
-inchan stdin
-tclscript 0
-syslog ""
}]
set opts [dict merge $defaults $args]
@ -2432,6 +2646,13 @@ namespace eval shellfilter {
set teehandle_err ${teehandle}err
set teehandle_in ${teehandle}in
set syslog [dict get $opts -syslog]
dict unset opts -syslog
set runtag "shellfilter-run"
set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]]
#set tid [::shellfilter::log::open $runtag [list -syslog $syslog]]
log::info {::shellfilter::log::write $runtag " opts: $opts"}
log::info {::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]"}
#puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err"
@ -2441,19 +2662,32 @@ namespace eval shellfilter {
lappend sources $source
}
}
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device]
set outpipechan [dict get $outdeviceinfo localchan]
set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device]
set errpipechan [dict get $errdeviceinfo localchan]
#set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device]
#set inpipechan [dict get $indeviceinfo localchan]
#---------------------
# #TEST
# chan configure $outpipechan -blocking 0
# chan configure $errpipechan -blocking 0
log::debug {::shellfilter::log::write $runtag " outchan $outchan -pipechan $outpipechan config: [chan configure $outpipechan]"}
log::debug {::shellfilter::log::write $runtag " errchan $errchan -pipechan $errpipechan config: [chan configure $errpipechan]"}
#---------------------
log::info {::shellfilter::log::write $runtag " calling shellfilter::stack::add for outchan:$outchan and errchan:$errchan with tee_to_pipe transforms. out tag: $teehandle_out outpipechan:$outpipechan err tag $teehandle_err errpipechan:$errpipechan"}
#NOTE:These transforms are not necessarily at the top of each stack!
#The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit.
set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]]
set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]]
log::critical {
::shellfilter::log::write $runtag "[punk::ansi::ansistrip [shellfilter::stack status]]\nchan names:[chan names]"
}
# need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this
# If non os-level channel - the command can't be run with the redirection
# stderr/stdout can be run with non-os handles in the call -
@ -2493,6 +2727,7 @@ namespace eval shellfilter {
set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr]
}
}
log::notice {::shellfilter::log::write $runtag "finished shell command execution with exitinfo '$exitinfo'"}
} else {
if {[catch {
#script result
@ -2500,29 +2735,45 @@ namespace eval shellfilter {
} errMsg]} {
set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"]
}
log::notice {::shellfilter::log::write $runtag "finished script execution with exitinfo '$exitinfo'"}
}
#puts "shellfilter::run finished call"
#-------------------------
#warning - without flush stdout - we can get hang, but only on some terminals
# - mechanism for this problem not understood!
#todo - test/document.
flush stdout
flush stderr
#update idletasks
#-------------------------
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
shellfilter::stack::remove stdout $id_out
shellfilter::stack::remove stderr $id_err
#shellfilter::stack::remove stderr $id_in
#puts stderr "shellfilter::run complete..."
#----------------------------------------------------------------------------------------------
# wrapped using tcllib logger - avoid even generating the shellfilter::stack status table if log level above debug.
# Logger allows the contents to be evaluated only if logging is switched on.
#----------------------------------------------------------------------------------------------
#todo - change to log::debug
log::critical {
if {![catch {package require punk::ansi}]} {
set stackstatus [punk::ansi::ansistrip [shellfilter::stack status]]
} else {
set stackstatus [shellfilter::stack status]
}
::shellfilter::log::write $runtag "shellfilter::stack status after execution: \n$stackstatus\nchan:names [chan names]"
}
#----------------------------------------------------------------------------------------------
#chan configure stderr -buffering line
#flush stdout
#the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal
#Remove execution-time Tees from stack
log::debug {::shellfilter::log::write $runtag "removing $id_out from stdout stack"}
shellfilter::stack::remove $outchan $id_out
log::debug {::shellfilter::log::write $runtag "removing $id_err from stderr stack"}
shellfilter::stack::remove $errchan $id_err
::shellfilter::log::write $runtag " return '$exitinfo'"
log::info {::shellfilter::log::write $runtag " return '$exitinfo'"}
::shellfilter::log::close $runtag
return $exitinfo
}
@ -2552,6 +2803,7 @@ namespace eval shellfilter {
}
if {$close} {
lappend tidied_sources $s
#unsubscribe from source tag s.
shellfilter::log::close $s
lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s]
}

177
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellthread-1.6.2.tm

@ -119,29 +119,58 @@ namespace eval shellthread::worker {
set waitvar ::shellthread::worker::wait($inpipe,[clock micros])
#tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan
chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
vwait $waitvar
}
proc pipe_read {chan source waitfor readbuffering writebuffering} {
#chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
if {$readbuffering eq "line"} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
}
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
} else {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
chan event $readchan readable [list apply {{chan source waitfor writebuffering} {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}} $readchan $source $waitvar $writebuffering]
}
vwait $waitvar
}
#proc pipe_read {chan source waitfor readbuffering writebuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# ::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
# } else {
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# }
# } else {
# set chunk [chan read $chan]
# ::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $chan
# }
#}
proc start_pipe_write {source writechan args} {
variable outpipe
@ -181,8 +210,8 @@ namespace eval shellthread::worker {
chan configure $writechan -blocking 0
set waitvar ::shellthread::worker::wait($outpipe,[clock micros])
chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
if {$readbuffering eq "line"} {
if {$readbuffering eq "line"} {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
@ -191,19 +220,55 @@ namespace eval shellthread::worker {
puts -nonewline $writechan $chunk
}
}
} else {
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
flush $writechan ;#2026-05-19 - ensure all data is sent before closing
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}} $readchan $writechan $source $waitvar]
} else {
chan event $readchan readable [list apply {{chan writechan source waitfor} {
set chunk [chan read $chan]
puts -nonewline $writechan $chunk
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}
}} $readchan $writechan $source $waitvar $readbuffering]
}} $readchan $writechan $source $waitvar]
}
# chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
# if {$readbuffering eq "line"} {
# set chunksize [chan gets $chan chunk]
# if {$chunksize >= 0} {
# if {![chan eof $chan]} {
# puts $writechan $chunk
# } else {
# puts -nonewline $writechan $chunk
# }
# }
# } else {
# set chunk [chan read $chan]
# puts -nonewline $writechan $chunk
# }
# if {[chan eof $chan]} {
# chan event $chan readable {}
# set $waitfor "pipe"
# chan close $writechan
# if {$chan ne "stdin"} {
# chan close $chan
# }
# }
# }} $readchan $writechan $source $waitvar $readbuffering]
vwait $waitvar
}
@ -479,6 +544,10 @@ namespace eval shellthread::manager {
set sourcetag [lindex $sourcetaglist 0] ;#todo - use all
set defaults [dict create {*}{
-raw 0
-file {}
-syslog {}
-direction out
-workertype message
}]
set settingsdict [dict merge $defaults $settingsdict]
@ -501,6 +570,10 @@ namespace eval shellthread::manager {
return [dict get $winfo tid]
} elseif {$existing_settings eq {-raw 0 -file {} -syslog {} -direction out}} {
#review - magic dict seems brittle - shouldn't hard code here.???
#existing worker has default settings - so we'll assume it's a placeholder and update it with our settings
#review - where/when do we override the default settings?
dict lappend winfo list_client_tids $tidclient
dict set workers $sourcetag $winfo ;#writeback
return [dict get $winfo tid]
@ -575,11 +648,11 @@ namespace eval shellthread::manager {
package require Thread
package require shellthread
if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} {
unset ::settingsinfo
set ::shellthread_init "ok"
unset ::settingsinfo
set ::shellthread_init "ok"
} else {
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
}
}]
@ -622,15 +695,23 @@ namespace eval shellthread::manager {
proc write_log {source msg args} {
variable workers
set ts_micros_sent [clock micros]
set defaults [list -async 1 -level info]
set opts [dict merge $defaults $args]
if {[dict exists $workers $source]} {
if {[dict exists $workers $source tid]} {
set tidworker [dict get $workers $source tid]
if {$tidworker eq "noop"} {
return
}
} else {
set tidworker ""
}
set ts_micros_sent [clock micros]
set defaults [list {*}{
-async 1
-level info
}]
set opts [dict merge $defaults $args]
if {$tidworker ne ""} {
if {![thread::exists $tidworker]} {
# -syslog -file ?
set tidworker [new_worker $source]
@ -674,10 +755,7 @@ namespace eval shellthread::manager {
if {[dict exists $workers $source]} {
set list_client_tids [dict get $workers $source list_client_tids]
if {[set posn [lsearch $list_client_tids $mytid]] >= 0} {
#set list_client_tids [lreplace $list_client_tids $posn $posn]
#set list_client_tids [lreplace $list_client_tids[set list_client_tids {}] $posn $posn]
ledit list_client_tids $posn $posn
dict set workers $source list_client_tids $list_client_tids
}
if {![llength $list_client_tids]} {
@ -685,7 +763,6 @@ namespace eval shellthread::manager {
}
}
}
#we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all.
set subscriberless_workers [list]
@ -696,8 +773,8 @@ namespace eval shellthread::manager {
set subscriber_count 0
set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed
foreach taginfo $worker_tags {
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
}
if {$subscriber_count == 0} {
lappend subscriberless_workers $workertid
@ -760,7 +837,7 @@ namespace eval shellthread::manager {
set ::shellthread::waitfor waiting
#after $timeout [list set ::shellthread::waitfor]
#2025-07 timed-out untested review
set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]]
set timeout_timer [after $timeout {set ::shellthread::waitfor timed-out}]
set waiting_for [list]
set ended [list]
@ -769,7 +846,9 @@ namespace eval shellthread::manager {
if {[thread::exists $tid]} {
lappend waiting_for $tid
#thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads)
thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor
set tid_client [thread::id]
#shellthread::worker::terminate will return thread id of terminating thread (or empty string)
thread::send -async $tid [list shellthread::worker::terminate $tid_client] ::shellthread::waitfor
}
}
if {[llength $waiting_for]} {
@ -779,13 +858,13 @@ namespace eval shellthread::manager {
set timedout 1
break
} else {
after cancel $cancelid
after cancel $timeout_timer
lappend ended $::shellthread::waitfor
}
}
}
set free_threads [list]
return [dict create existed $waiting_for ended $ended timedout $timedout]
return [dict create existed $waiting_for ended $ended timedout $timedout allthreads [thread::names]]
}
#TODO - important.

59
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm

@ -5107,9 +5107,25 @@ tcl::namespace::eval textblock {
tcl::mathfunc::min {*}[lmap v [split $textblock \n] {tcl::string::length $v}]
}
if {[catch {package require parser}]} {
#tclparser c extension not available - use tcl string functions to count line-endings
#try not to load parser (and associated punk::args::moduledoc::parser) immediately.
if {[package provide parser] ne ""} {
#parser already loaded.
proc height {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
#Whether or not the terminal in use actually does this - we need to calculate as if it does. (there might not even be a terminal)
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
} else {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
} else {
#parser not loaded - but might be loadable.
#install a 'height' function that will load parser
proc _height_tcl {textblock} {
#This is the height as it will/would-be rendered - not the number of input lines purely in terms of le
#empty string still has height 1 (at least for left-right/right-left languages)
@ -5119,8 +5135,7 @@ tcl::namespace::eval textblock {
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map [list \n {} \v {}] $textblock]]}] ;#faster than splitting into single-char list
return [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
}
} else {
proc height {textblock} {
proc _height_c {textblock} {
if {[string first \v $textblock] >= 0} {
#use standard (slower) mechanism for counting lines
#vertical tab on a proper terminal should move directly down.
@ -5131,7 +5146,22 @@ tcl::namespace::eval textblock {
return [expr {[parse countnewline $textblock {}] + 1}]
}
}
#oneshot height function - renames itself on first call to the appropriate implementation.
proc height {textblock} {
if {[catch {package require parser}]} {
#parser not available - use tcl implementation
rename ::textblock::height ""
rename ::textblock::_height_tcl ::textblock::height
} else {
#parser available - use c implementation
rename ::textblock::height ""
rename ::textblock::_height_c ::textblock::height
}
tailcall ::textblock::height $textblock
}
}
#MAINTENANCE - same as overtype::blocksize?
proc size {textblock} {
if {$textblock eq ""} {
@ -8141,16 +8171,17 @@ tcl::namespace::eval textblock {
-etabs -default 0\
-help "expanding tabs - experimental/unimplemented."
#review - -choicelabels placeholder dollarsign of textblock::frame_samples must be left aligned with -choicelabels
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0 -choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-type -default light\
-type dict\
-typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\
-choices {${$DYN_FRAMETYPES}}\
-choicerestricted 0\
-choicecolumns 8\
-unindentedfields {-choicelabels}\
-choicelabels {
${$DYN_FRAMESAMPLES}
}\
-help "Type of border for frame."
-boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements.
passing an empty string will result in no box, but title/subtitle will still appear if supplied.
${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}"

BIN
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/zipper-0.14.tm

Binary file not shown.

34
src/project_layouts/vendor/punk/project-0.1/tclint.toml vendored

@ -0,0 +1,34 @@
# patterns to exclude when searching directories. defaults to empty list.
# follows gitignore pattern format: https://git-scm.com/docs/gitignore#_pattern_format
# the one exception is that a leading "#" character will be automatically escaped
#exclude = ["ignore_me/", "ignore*.tcl", "/ignore_from_here"]
# lint violations to ignore. defaults to empty list.
# can also supply an inline table with a path and a list of violations to ignore under that path.
#ignore = [
# "unbraced-expr",
# { path = "files_with_long_lines/", rules = ["line-length"] }
#]
# extensions of files to lint when searching directories. defaults to tcl, sdc,
# xdc, and upf.
extensions = ["tcl", "tm", "sdc"]
# path to command spec defining tool-specific commands and arguments, generated by
# `tclint-plugins make-spec`.
#commands = "~/.tclint/openroad.json"
# with the exception of line-length, the [style] settings affect tclfmt rather than tclint.
[style]
# number of spaces to indent. can also be set to "tab". defaults to 4.
#indent = 2
# maximum allowed line length. defaults to 100.
line-length = 400
# maximum allowed number of consecutive blank lines. defaults to 2.
max-blank-lines = 10
# whether to require indenting of "namespace eval" blocks. defaults to true.
#indent-namespace-eval = false
# whether to expect a single space (true) or no spaces (false) surrounding the contents of a braced expression or script argument.
# defaults to false.
#spaces-in-braces = true

39
src/vfs/_vfscommon.vfs/modules/punk/mix/base-0.1.tm

@ -677,7 +677,7 @@ namespace eval punk::mix::base {
if {$opt_use_tar != 0} {
set target [file tail $path]
set tmplocation [punk::mix::util::tmpdir]
set archivename $tmplocation/[punk::mix::util::tmpfile].tar
set archivename $tmplocation/[punk::mix::util::tmpfile].tar ;#generates a unique filename - does not create the file.
cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues)
@ -687,14 +687,38 @@ namespace eval punk::mix::base {
set tsstart [clock millis]
if {[set tarpath [auto_execok tar]] ne ""} {
#using an external binary is *significantly* faster than tar::create - but comes with some risks
#review - need to check behaviour/flag variances across platforms
set versioninfo [exec {*}$tarpath --version]
#look for "bsdtar" vs "GNU"
#GNU tar is more common on linux - but also available on windows via gnuutils or msys
#<system32>/tar.exe on windows is likely to be bsdtar.
#GNU tar on windows will commonly fail with 'cannot connect to C: resolve failed' - may need --force-local flag
#review - need to further check behaviour/flag variances across platforms
#don't use -z flag. On at least some tar versions the zipped file will contain a timestamped subfolder of filename.tar - which ruins the checksum
#also - tar is generally faster without the compression (although this may vary depending on file size and disk speed?)
exec {*}$tarpath -cf $archivename $target ;#{*} needed in case spaces in tarpath
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " tar -cf done ($ms ms)"
} else {
if {[string match "*GNU*" $versioninfo]} {
set flags "--force-local"
} else {
#presumably bsdtar - which is more likely to be present on windows - and doesn't seem to have the same issue with drive letters in paths
set flags ""
}
if {[catch {
#{*}$tarpath needed in case spaces in tarpath
exec {*}$tarpath -cf {*}$flags $archivename $target
} errMsg]} {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' ERROR ($ms ms) - falling back to tar::create\n error info: $errMsg"
} else {
set tsend [clock millis]
set ms [expr {$tsend - $tsstart}]
puts stdout " 'tar -cf $flags' done ($ms ms)"
}
}
if {![file exists $archivename]} {
#fallback to tar library approach if external tar failed to create the archive.
set tsstart [clock millis] ;#don't include auto_exec search time for tar::create
tar::create $archivename $target
set tsend [clock millis]
@ -703,6 +727,7 @@ namespace eval punk::mix::base {
puts stdout " NOTE: install tar executable for potentially *much* faster directory checksum processing"
}
if {$ftype eq "file"} {
set sizeinfo "(size [punk::lib::format_number [file size $target]] bytes)"
} else {

34
src/vfs/_vfscommon.vfs/modules/punk/repo-0.1.1.tm

@ -83,34 +83,38 @@ namespace eval punk::repo {
proc get_fossil_usage {} {
set allcmds [runout -n fossil help -a]
set allcmds [punk::ansi::ansistrip $allcmds]
set mainhelp [runout -n fossil help]
set mainhelp [punk::ansi::ansistrip $mainhelp]
set maincommands [list]
#only start parsing for TOPICS after a line such as "Other comman values for TOPIC:"
set parsing_topics 0
foreach ln [split $mainhelp \n] {
set ln [string trim $ln]
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
if {$ln eq ""} {
continue
}
if {[string match "*values for TOPIC*" $ln]} {
set parsing_topics 1
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
if {$parsing_topics} {
#lines starting with uppercase are topic headers - we want to ignore these and any blank lines
if {[regexp {^[A-Z]+} $ln]} {
continue
}
lappend maincommands {*}$ln
}
lappend maincommands {*}$ln
}
}
#fossil output was ordered in columns, but we loaded list in row-wise, messing up the order
set maincommands [lsort $maincommands]
set allcmds [lsort $allcmds]
set othercmds [punk::lib::ldiff $allcmds $maincommands]
set fossil_setting_names [lsort [runout -n fossil help -s]]
set setting_info [runout -n fossil help -s]
set setting_info [punk::ansi::ansistrip $setting_info]
set fossil_setting_names [lsort $setting_info]
set result "@leaders -min 0\n"
@ -186,6 +190,8 @@ namespace eval punk::repo {
foreach ln $basic_opt_lines {
set ln [string trim $ln]
#fossil sometimes emits cursor control sequences e.g CSI 3 q
set ln [punk::ansi::ansistrip $ln]
if {$ln eq ""} {
continue
}
@ -250,6 +256,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage add]}
@form -form "raw" -synopsis "exec fossil add \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help add" -body {${[runout -n fossil help add]}}
} ""]
@ -264,6 +271,7 @@ namespace eval punk::repo {
${[punk::repo::get_fossil_subcommand_usage diff]}
@form -form "raw" -synopsis "exec fossil diff \[OPTIONS\] FILE1 \[FILE2\]..."
#fossil help may have ansi - review
@formdisplay -header "fossil help diff" -body {${[runout -n fossil help diff]}}
} ""]

3395
src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.1.tm

File diff suppressed because it is too large Load Diff

3347
src/vfs/_vfscommon.vfs/modules/shellfilter-0.2.tm

File diff suppressed because it is too large Load Diff

829
src/vfs/_vfscommon.vfs/modules/shellthread-1.6.1.tm

@ -1,829 +0,0 @@
#package require logger
package require Thread
namespace eval shellthread {
proc iso8601 {{tsmicros ""}} {
if {$tsmicros eq ""} {
set tsmicros [tcl::clock::microseconds]
} else {
set microsnow [tcl::clock::microseconds]
if {[tcl::string::length $tsmicros] != [tcl::string::length $microsnow]} {
error "iso8601 requires 'clock micros' or empty string to create timestamp"
}
}
set seconds [expr {$tsmicros / 1000000}]
return [tcl::clock::format $seconds -format "%Y-%m-%d_%H-%M-%S"]
}
}
namespace eval shellthread::worker {
variable settings
variable sysloghost_port
variable sock
variable logfile ""
variable fd
variable client_ids [list]
variable ts_start_micros
variable errorlist [list]
variable inpipe ""
proc bgerror {args} {
variable errorlist
lappend errorlist $args
}
proc send_errors_now {tidcli} {
variable errorlist
thread::send -async $tidcli [list shellthread::manager::report_worker_errors [list worker_tid [thread::id] errors $errorlist]]
}
proc add_client_tid {tidcli} {
variable client_ids
if {$tidcli ni $client_ids} {
lappend client_ids $tidcli
}
}
proc init {tidclient start_m settingsdict} {
variable sysloghost_port
variable logfile
variable settings
interp bgerror {} shellthread::worker::bgerror
#package require overtype ;#overtype uses tcllib textutil, punk::char etc - currently too heavyweight in terms of loading time for use in threads.
variable client_ids
variable ts_start_micros
lappend client_ids $tidclient
set ts_start_micros $start_m
set defaults [list -raw 0 -file "" -syslog "" -direction out]
set settings [dict merge $defaults $settingsdict]
set syslog [dict get $settings -syslog]
if {[string length $syslog]} {
lassign [split $syslog :] s_host s_port
set sysloghost_port [list $s_host $s_port]
if {[catch {package require udp} errm]} {
#disable rather than bomb and interfere with any -file being written
#review - log/notify?
set sysloghost_port ""
}
} else {
set sysloghost_port ""
}
set logfile [dict get $settings -file]
}
proc start_pipe_read {source readchan args} {
#assume 1 inpipe for now
variable inpipe
variable sysloghost_port
variable logfile
set defaults [dict create -buffering \uFFFF ]
set opts [dict merge $defaults $args]
if {[dict exists $opts -readbuffering]} {
set readbuffering [dict get $opts -readbuffering]
} else {
if {[dict get $opts -buffering] eq "\uFFFF"} {
#get buffering setting from the channel as it was set prior to thread::transfer
set readbuffering [chan configure $readchan -buffering]
} else {
set readbuffering [dict get $opts -buffering]
chan configure $readchan -buffering $readbuffering
}
}
if {[dict exists $opts -writebuffering]} {
set writebuffering [dict get $opts -writebuffering]
} else {
if {[dict get $opts -buffering] eq "\uFFFF"} {
set writebuffering line
#set writebuffering [chan configure $writechan -buffering]
} else {
set writebuffering [dict get $opts -buffering]
#can configure $writechan -buffering $writebuffering
}
}
chan configure $readchan -translation lf
if {$readchan ni [chan names]} {
error "shellthread::worker::start_pipe_read - inpipe not configured. Use shellthread::manager::set_pipe_read_from_client to thread::transfer the pipe end"
}
set inpipe $readchan
chan configure $readchan -blocking 0
set waitvar ::shellthread::worker::wait($inpipe,[clock micros])
#tcl::chan::fifo2 based pipe seems slower to establish events upon than Memchan
chan event $readchan readable [list ::shellthread::worker::pipe_read $readchan $source $waitvar $readbuffering $writebuffering]
vwait $waitvar
}
proc pipe_read {chan source waitfor readbuffering writebuffering} {
if {$readbuffering eq "line"} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
::shellthread::worker::log pipe 0 - $source - info $chunk\n $writebuffering
} else {
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
}
} else {
set chunk [chan read $chan]
::shellthread::worker::log pipe 0 - $source - info $chunk $writebuffering
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $chan
}
}
proc start_pipe_write {source writechan args} {
variable outpipe
set defaults [dict create -buffering \uFFFF ]
set opts [dict merge $defaults $args]
#todo!
set readchan stdin
if {[dict exists $opts -readbuffering]} {
set readbuffering [dict get $opts -readbuffering]
} else {
if {[dict get $opts -buffering] eq "\uFFFF"} {
set readbuffering [chan configure $readchan -buffering]
} else {
set readbuffering [dict get $opts -buffering]
chan configure $readchan -buffering $readbuffering
}
}
if {[dict exists $opts -writebuffering]} {
set writebuffering [dict get $opts -writebuffering]
} else {
if {[dict get $opts -buffering] eq "\uFFFF"} {
#nothing explicitly set - take from transferred channel
set writebuffering [chan configure $writechan -buffering]
} else {
set writebuffering [dict get $opts -buffering]
can configure $writechan -buffering $writebuffering
}
}
if {$writechan ni [chan names]} {
error "shellthread::worker::start_pipe_write - outpipe not configured. Use shellthread::manager::set_pipe_write_to_client to thread::transfer the pipe end"
}
set outpipe $writechan
chan configure $readchan -blocking 0
chan configure $writechan -blocking 0
set waitvar ::shellthread::worker::wait($outpipe,[clock micros])
chan event $readchan readable [list apply {{chan writechan source waitfor readbuffering} {
if {$readbuffering eq "line"} {
set chunksize [chan gets $chan chunk]
if {$chunksize >= 0} {
if {![chan eof $chan]} {
puts $writechan $chunk
} else {
puts -nonewline $writechan $chunk
}
}
} else {
set chunk [chan read $chan]
puts -nonewline $writechan $chunk
}
if {[chan eof $chan]} {
chan event $chan readable {}
set $waitfor "pipe"
chan close $writechan
if {$chan ne "stdin"} {
chan close $chan
}
}
}} $readchan $writechan $source $waitvar $readbuffering]
vwait $waitvar
}
proc _initsock {} {
variable sysloghost_port
variable sock
if {[string length $sysloghost_port]} {
if {[catch {chan configure $sock} state]} {
set sock [udp_open]
chan configure $sock -buffering none -translation binary
chan configure $sock -remote $sysloghost_port
}
}
}
proc _reconnect {} {
variable sock
catch {close $sock}
_initsock
return [chan configure $sock]
}
proc send_info {client_tid ts_sent source msg} {
set ts_received [clock micros]
set lag_micros [expr {$ts_received - $ts_sent}]
set lag [expr {$lag_micros / 1000000.0}] ;#lag as x.xxxxxx seconds
log $client_tid $ts_sent $lag $source - info $msg line 1
}
proc log {client_tid ts_sent lag source service level msg writebuffering {islog 0}} {
variable sock
variable fd
variable sysloghost_port
variable logfile
variable settings
set logchunk $msg
if {![dict get $settings -raw]} {
set tail_crlf 0
set tail_lf 0
set tail_cr 0
#for cooked - always remove the trailing newline before splitting..
#
#note that if we got our data from reading a non-line-buffered binary channel - then this naive line splitting will not split neatly for mixed line-endings.
#
#Possibly not critical as cooked is for logging and we are still preserving all \r and \n chars - but review and consider implementing a better split
#but add it back exactly as it was afterwards
#we can always split on \n - and any adjacent \r will be preserved in the rejoin
set lastchar [string range $logchunk end end]
if {[string range $logchunk end-1 end] eq "\r\n"} {
set tail_crlf 1
set logchunk [string range $logchunk 0 end-2]
} else {
if {$lastchar eq "\n"} {
set tail_lf 1
set logchunk [string range $logchunk 0 end-1]
} elseif {$lastchar eq "\r"} {
#\r line-endings are obsolete..and unlikely... and ugly as they can hide characters on the console. but we'll pass through anyway.
set tail_cr 1
set logchunk [string range $logchunk 0 end-1]
} else {
#possibly a single line with no linefeed.. or has linefeeds only in the middle
}
}
if {$ts_sent != 0} {
set micros [lindex [split [expr {$ts_sent / 1000000.0}] .] end]
set time_info [::shellthread::iso8601 $ts_sent].$micros
#set time_info "${time_info}+$lag"
set lagfp "+[format %f $lag]"
} else {
#from pipe - no ts_sent/lag info available
set time_info ""
set lagfp ""
}
set idtail [string range $client_tid end-8 end] ;#enough for display purposes id - mostly zeros anyway
#set col0 [string repeat " " 9]
#set col1 [string repeat " " 27]
#set col2 [string repeat " " 11]
#set col3 [string repeat " " 22]
##do not columnize the final data column or append to tail - or we could muck up the crlf integrity
#lassign [list [overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 $lagfp] [overtype::left $col3 $source]] c0 c1 c2 c3
set w0 9
set w1 27
set w2 11
set w3 22 ;#review - this can truncate source name without indication tail is missing
#do not columnize the final data column or append to tail - or we could muck up the crlf integrity
lassign [list \
[format %-${w0}s $idtail]\
[format %-${w1}s $time_info]\
[format %-${w2}s $lagfp]\
[format %-${w3}s $source]\
] c0 c1 c2 c3
set c2_blank [string repeat " " $w2]
#split on \n no matter the actual line-ending in use
#shouldn't matter as long as we don't add anything at the end of the line other than the raw data
#ie - don't quote or add spaces
set lines [split $logchunk \n]
set i 1
set outlines [list]
foreach ln $lines {
if {$i == 1} {
lappend outlines "$c0 $c1 $c2 $c3 $ln"
} else {
lappend outlines "$c0 $c1 $c2_blank $c3 $ln"
}
incr i
}
if {$tail_lf} {
set logchunk "[join $outlines \n]\n"
} elseif {$tail_crlf} {
set logchunk "[join $outlines \r\n]\r\n"
} elseif {$tail_cr} {
set logchunk "[join $outlines \r]\r"
} else {
#no trailing linefeed
set logchunk [join $outlines \n]
}
#set logchunk "[overtype::left $col0 $idtail] [overtype::left $col1 $time_info] [overtype::left $col2 "+$lagfp"] [overtype::left $col3 $source] $msg"
}
if {[string length $sysloghost_port]} {
_initsock
catch {puts -nonewline $sock $logchunk}
}
#todo - sockets etc?
if {[string length $logfile]} {
#todo - setting to maintain open filehandle and reduce io.
# possible settings for buffersize - and maybe logrotation, although this could be left to client
#for now - default to safe option of open/close each write despite the overhead.
set fd [open $logfile a]
chan configure $fd -translation auto -buffering $writebuffering
#whether line buffered or not - by now our logchunk includes newlines
puts -nonewline $fd $logchunk
close $fd
}
}
# - withdraw just this client
proc finish {tidclient} {
variable client_ids
if {($tidclient in $clientids) && ([llength $clientids] == 1)} {
terminate $tidclient
} else {
set posn [lsearch $client_ids $tidclient]
set client_ids [lreplace $clientids $posn $posn]
}
}
#allow any client to terminate
proc terminate {tidclient} {
variable sock
variable fd
variable client_ids
if {$tidclient in $client_ids} {
catch {close $sock}
catch {close $fd}
set client_ids [list]
#review use of thread::release -wait
#docs indicate deprecated for regular use, and that we should use thread::join
#however.. how can we set a timeout on a thread::join ?
#by telling the thread to release itself - we can wait on the thread::send variable
# This needs review - because it's unclear that -wait even works on self
# (what does it mean to wait for the target thread to exit if the target is self??)
thread::release -wait
return [thread::id]
} else {
return ""
}
}
}
namespace eval shellthread::manager {
variable workers [dict create]
variable worker_errors [list]
variable timeouts
variable free_threads [list]
#variable log_threads
proc dict_getdef {dictValue args} {
if {[llength $args] < 2} {
error {wrong # args: should be "dict_getdef dictValue ?key ...? key default"}
}
set keys [lrange $args 0 end-1]
if {[tcl::dict::exists $dictValue {*}$keys]} {
return [tcl::dict::get $dictValue {*}$keys]
} else {
return [lindex $args end]
}
}
#new datastructure regarding workers and sourcetags required.
#one worker can service multiple sourcetags - but each sourcetag may be used by multiple threads too.
#generally each thread will use a specific sourcetag - but we may have pools doing similar things which log to same destination.
#
#As a convention we may use a sourcetag for the thread which started the worker that isn't actually used for logging - but as a common target for joins
#If the thread which started the thread calls leave_worker with that 'primary' sourcetag it means others won't be able to use that target - which seems reasonable.
#If another thread want's to maintain joinability beyond the span provided by the starting client,
#it can join with both the primary tag and a tag it will actually use for logging.
#A thread can join the logger with any existingtag - not just the 'primary'
#(which is arbitrary anyway. It will usually be the first in the list - but may be unsubscribed by clients and disappear)
proc join_worker {existingtag sourcetaglist} {
set client_tid [thread::id]
#todo - allow a source to piggyback on existing worker by referencing one of the sourcetags already using the worker
}
proc new_pipe_worker {sourcetaglist {settingsdict {}}} {
if {[dict exists $settingsdict -workertype]} {
if {[string tolower [dict get $settingsdict -workertype]] ne "pipe"} {
error "new_pipe_worker error: -workertype ne 'pipe'. Set to 'pipe' or leave empty"
}
}
dict set settingsdict -workertype pipe
new_worker $sourcetaglist $settingsdict
}
#it is up to caller to use a unique sourcetag (e.g by prefixing with own thread::id etc)
# This allows multiple threads to more easily write to the same named sourcetag if necessary
# todo - change sourcetag for a list of tags which will be handled by the same thread. e.g for multiple threads logging to same file
#
# todo - some protection mechanism for case where target is a file to stop creation of multiple worker threads writing to same file.
# Even if we use open fd,close fd wrapped around writes.. it is probably undesirable to have multiple threads with same target
# On the other hand socket targets such as UDP can happily be written to by multiple threads.
# For now the mechanism is that a call to new_worker (rename to open_worker?) will join the same thread if a sourcetag matches.
# but, as sourcetags can get removed(unsubbed via leave_worker) this doesn't guarantee two threads with same -file settings won't fight.
# Also.. the settingsdict is ignored when joining with a tag that exists.. this is problematic.. e.g logrotation where previous file still being written by existing worker
# todo - rename 'sourcetag' concept to 'targettag' ?? the concept is a mixture of both.. it is somewhat analagous to a syslog 'facility'
# probably new_worker should disallow auto-joining and we allow different workers to handle same tags simultaneously to support overlap during logrotation etc.
proc new_worker {sourcetaglist {settingsdict {}}} {
variable workers
set ts_start [clock micros]
set tidclient [thread::id]
set sourcetag [lindex $sourcetaglist 0] ;#todo - use all
set defaults [dict create\
-workertype message\
]
set settingsdict [dict merge $defaults $settingsdict]
set workertype [string tolower [dict get $settingsdict -workertype]]
set known_workertypes [list pipe message]
if {$workertype ni $known_workertypes} {
error "new_worker - unknown -workertype $workertype. Expected one of '$known_workertypes'"
}
if {[dict exists $workers $sourcetag]} {
set winfo [dict get $workers $sourcetag]
if {[dict get $winfo tid] ne "noop" && [thread::exists [dict get $winfo tid]]} {
#add our client-info to existing worker thread
dict lappend winfo list_client_tids $tidclient
dict set workers $sourcetag $winfo ;#writeback
return [dict get $winfo tid]
}
}
#noop fake worker for empty syslog and empty file
if {$workertype eq "message"} {
if {[dict_getdef $settingsdict -syslog ""] eq "" && [dict_getdef $settingsdict -file ""] eq ""} {
set winfo [dict create tid noop list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype "message"]
dict set workers $sourcetag $winfo
return noop
}
}
#check if there is an existing unsubscribed thread first
#don't use free_threads for pipe workertype for now..
variable free_threads
if {$workertype ne "pipe"} {
if {[llength $free_threads]} {
#todo - re-use from tail - as most likely to have been doing similar work?? review
set free_threads [lassign $free_threads tidworker]
#todo - keep track of real ts_start of free threads... kill when too old
set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list] workertype [dict get $settingsdict -workertype]]
#puts stderr "shellfilter::new_worker Re-using free worker thread: $tidworker with tag $sourcetag"
dict set workers $sourcetag $winfo
return $tidworker
}
}
#set ts_start [::shellthread::iso8601]
set tidworker [thread::create -preserved]
set init_script [string map [list %ts_start% $ts_start %mp% [tcl::tm::list] %ap% $::auto_path %tidcli% $tidclient %sd% $settingsdict] {
#set tclbase [file dirname [file dirname [info nameofexecutable]]]
#set tcllib $tclbase/lib
#if {$tcllib ni $::auto_path} {
# lappend ::auto_path $tcllib
#}
set ::settingsinfo [dict create %sd%]
#if the executable running things is something like a tclkit,
# then it's likely we will need to use the caller's auto_path and tcl::tm::list to find things
#The caller can tune the thread's package search by providing a settingsdict
#tcl::tm::add * must add in reverse order to get reulting list in same order as original
if {![dict exists $::settingsinfo tcl_tm_list]} {
#JMN2
::tcl::tm::add {*}[lreverse [list %mp%]]
} else {
tcl::tm::remove {*}[tcl::tm::list]
::tcl::tm::add {*}[lreverse [dict get $::settingsinfo tcl_tm_list]]
}
if {![dict exists $::settingsinfo auto_path]} {
set ::auto_path [list %ap%]
} else {
set ::auto_path [dict get $::settingsinfo auto_path]
}
package require punk::packagepreference
punk::packagepreference::install
package require Thread
package require shellthread
if {![catch {::shellthread::worker::init %tidcli% %ts_start% $::settingsinfo} errmsg]} {
unset ::settingsinfo
set ::shellthread_init "ok"
} else {
unset ::settingsinfo
set ::shellthread_init "err $errmsg"
}
}]
thread::send -async $tidworker $init_script
#thread::send $tidworker $init_script
set winfo [dict create tid $tidworker list_client_tids [list $tidclient] ts_start $ts_start ts_end_list [list]]
dict set workers $sourcetag $winfo
return $tidworker
}
proc set_pipe_read_from_client {tag_pipename worker_tid rchan args} {
variable workers
if {![dict exists $workers $tag_pipename]} {
error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename not found"
}
set match_worker_tid [dict get $workers $tag_pipename tid]
if {$worker_tid ne $match_worker_tid} {
error "workerthread::manager::set_pipe_read_from_client source/pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'"
}
#buffering set during channel creation will be preserved on thread::transfer
thread::transfer $worker_tid $rchan
#start_pipe_read will vwait - so we have to send async
thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_read $tag_pipename $rchan]
#client may start writing immediately - but presumably it will buffer in fifo2
}
proc set_pipe_write_to_client {tag_pipename worker_tid wchan args} {
variable workers
if {![dict exists $workers $tag_pipename]} {
error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename not found"
}
set match_worker_tid [dict get $workers $tag_pipename tid]
if {$worker_tid ne $match_worker_tid} {
error "workerthread::manager::set_pipe_write_to_client pipename $tag_pipename workert_tid mismatch '$worker_tid' vs existing:'$match_worker_tid'"
}
#buffering set during channel creation will be preserved on thread::transfer
thread::transfer $worker_tid $wchan
thread::send -async $worker_tid [list ::shellthread::worker::start_pipe_write $tag_pipename $wchan]
}
proc write_log {source msg args} {
variable workers
set ts_micros_sent [clock micros]
set defaults [list -async 1 -level info]
set opts [dict merge $defaults $args]
if {[dict exists $workers $source]} {
set tidworker [dict get $workers $source tid]
if {$tidworker eq "noop"} {
return
}
if {![thread::exists $tidworker]} {
# -syslog -file ?
set tidworker [new_worker $source]
}
} else {
#auto create with no requirement to call new_worker.. warn?
# -syslog -file ?
error "write_log no log opened for source: $source"
set tidworker [new_worker $source]
}
set client_tid [thread::id]
if {[dict get $opts -async]} {
thread::send -async $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg]
} else {
thread::send $tidworker [list ::shellthread::worker::send_info $client_tid $ts_micros_sent $source $msg]
}
}
proc report_worker_errors {errdict} {
variable workers
set reporting_tid [dict get $errdict worker_tid]
dict for {src srcinfo} $workers {
if {[dict get $srcinfo tid] eq $reporting_tid} {
dict set srcinfo errors [dict get $errdict errors]
dict set workers $src $srcinfo ;#writeback updated
break
}
}
}
#aka leave_worker
#Note that the tags may be on separate workertids, or some tags may share workertids
proc unsubscribe {sourcetaglist} {
variable workers
#workers structure example:
#[list sourcetag1 [list tid <tidworker> list_client_tids <clients>] ts_start <ts_start> ts_end_list {}]
variable free_threads
set mytid [thread::id] ;#caller of shellthread::manager::xxx is the client thread
set subscriberless_tags [list]
foreach source $sourcetaglist {
if {[dict exists $workers $source]} {
set list_client_tids [dict get $workers $source list_client_tids]
if {[set posn [lsearch $list_client_tids $mytid]] >= 0} {
set list_client_tids [lreplace $list_client_tids $posn $posn]
dict set workers $source list_client_tids $list_client_tids
}
if {![llength $list_client_tids]} {
lappend subscriberless_tags $source
}
}
}
#we've removed our own tid from all the tags - possibly across multiplew workertids, and possibly leaving some workertids with no subscribers for a particular tag - or no subscribers at all.
set subscriberless_workers [list]
set shuttingdown_workers [list]
foreach deadtag $subscriberless_tags {
set workertid [dict get $workers $deadtag tid]
set worker_tags [get_worker_tagstate $workertid]
set subscriber_count 0
set kill_count 0 ;#number of ts_end_list entries - even one indicates thread is doomed
foreach taginfo $worker_tags {
incr subscriber_count [llength [dict get $taginfo list_client_tids]]
incr kill_count [llength [dict get $taginfo ts_end_list]]
}
if {$subscriber_count == 0} {
lappend subscriberless_workers $workertid
}
if {$kill_count > 0} {
lappend shuttingdown_workers $workertid
}
}
#if worker isn't shutting down - add it to free_threads list
foreach workertid $subscriberless_workers {
if {$workertid ni $shuttingdown_workers} {
if {$workertid ni $free_threads && $workertid ne "noop"} {
lappend free_threads $workertid
}
}
}
#todo
#unsub this client_tid from the sourcetags in the sourcetaglist. if no more client_tids exist for sourcetag, remove sourcetag,
#if no more sourcetags - add worker to free_threads
}
proc get_worker_tagstate {workertid} {
variable workers
set taginfo_list [list]
dict for {source sourceinfo} $workers {
if {[dict get $sourceinfo tid] eq $workertid} {
lappend taginfo_list $sourceinfo
}
}
return $taginfo_list
}
#finalisation
proc shutdown_free_threads {{timeout 2500}} {
variable free_threads
if {![llength $free_threads]} {
return
}
upvar ::shellthread::manager::timeouts timeoutarr
if {[info exists timeoutarr(shutdown_free_threads)]} {
#already called
return false
}
#set timeoutarr(shutdown_free_threads) waiting
#after $timeout [list set timeoutarr(shutdown_free_threads) timed-out]
set ::shellthread::waitfor waiting
#after $timeout [list set ::shellthread::waitfor]
#2025-07 timed-out untested review
set cancelid [after $timeout [list set ::shellthread::waitfor timed-out]]
set waiting_for [list]
set ended [list]
set timedout 0
foreach tid $free_threads {
if {[thread::exists $tid]} {
lappend waiting_for $tid
#thread::send -async $tid [list shellthread::worker::terminate [thread::id]] timeoutarr(shutdown_free_threads)
thread::send -async $tid [list shellthread::worker::terminate [thread::id]] ::shellthread::waitfor
}
}
if {[llength $waiting_for]} {
for {set i 0} {$i < [llength $waiting_for]} {incr i} {
vwait ::shellthread::waitfor
if {$::shellthread::waitfor eq "timed-out"} {
set timedout 1
break
} else {
after cancel $cancelid
lappend ended $::shellthread::waitfor
}
}
}
set free_threads [list]
return [dict create existed $waiting_for ended $ended timedout $timedout]
}
#TODO - important.
#REVIEW!
#since moving to the unsubscribe mechansm - close_worker $source isn't being called
# - we need to set a limit to the number of free threads and shut down excess when detected during unsubscription
#instruction to shut-down the thread that has this source.
#instruction to shut-down the thread that has this source.
proc close_worker {source {timeout 2500}} {
variable workers
variable worker_errors
variable free_threads
upvar ::shellthread::manager::timeouts timeoutarr
set ts_now [clock micros]
#puts stderr "close_worker $source"
if {[dict exists $workers $source]} {
set tidworker [dict get $workers $source tid]
if {$tidworker in $freethreads} {
#make sure a thread that is being closed is removed from the free_threads list
set posn [lsearch $freethreads $tidworker]
set freethreads [lreplace $freethreads $posn $posn]
}
set mytid [thread::id]
set client_tids [dict get $workers $source list_client_tids]
if {[set posn [lsearch $client_tids $mytid]] >= 0} {
set client_tids [lreplace $client_tids $posn $posn]
#remove self from list of clients
dict set workers $source list_client_tids $client_tids
}
set ts_end_list [dict get $workers $source ts_end_list] ;#ts_end_list is just a list of timestamps of closing calls for this source - only one is needed to close, but they may all come in a flurry.
if {[llength $ts_end_list]} {
set last_end_ts [lindex $ts_end_list end]
if {(($tsnow - $last_end_ts) / 1000) >= $timeout} {
lappend ts_end_list $ts_now
dict set workers $source ts_end_list $ts_end_list
} else {
#existing close in progress.. assume it will work
return
}
}
if {[thread::exists $tidworker]} {
#puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source still running.. terminating"
#review - timeoutarr is local var (?)
set timeoutarr($source) 0
after $timeout [list set timeoutarr($source) 2]
thread::send -async $tidworker [list shellthread::worker::send_errors_now [thread::id]]
thread::send -async $tidworker [list shellthread::worker::terminate [thread::id]] timeoutarr($source)
#thread::send -async $tidworker [string map [list %tidclient% [thread::id]] {
# shellthread::worker::terminate %tidclient%
#}] timeoutarr($source)
vwait timeoutarr($source)
#puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE1"
thread::release $tidworker
#puts stderr "shellthread::manager::close_worker: thread $tidworker for source $source DONE2"
if {[dict exists $workers $source errors]} {
set errlist [dict get $workers $source errors]
if {[llength $errlist]} {
lappend worker_errors [list $source [dict get $workers $source]]
}
}
dict unset workers $source
} else {
#thread may have been closed by call to close_worker with another source with same worker
#clear workers record for this source
#REVIEW - race condition for re-creation of source with new workerid?
#check that record is subscriberless to avoid this
if {[llength [dict get $workers $source list_client_tids]] == 0} {
dict unset workers $source
}
}
}
#puts stdout "close_worker $source - end"
}
#worker errors only available for a source after close_worker called on that source
#It is possible for there to be multiple entries for a source because new_worker can be called multiple times with same sourcetag,
proc get_and_clear_errors {source} {
variable worker_errors
set source_errors [lsearch -all -inline -index 0 $worker_errors $source]
set worker_errors [lsearch -all -inline -index 0 -not $worker_errors $source]
return $source_errors
}
}
package provide shellthread [namespace eval shellthread {
variable version
set version 1.6.1
}]

873
src/vfs/_vfscommon.vfs/modules/sqids-0.3.0.tm

@ -1,873 +0,0 @@
#Do not update version of this file. Update sqids-buildversion.txt and run make.tcl to update the version in this file and copy to modules folder.
package require Tcl 8.6-
#MIT license
#Julian Noble 2026
#example:
# % package require sqids
# % set s1 [sqids::idscope new]
# ::oo::Obj275
# % $s1 encode {1 2 3}
# 86Rf07
# % $s1 decode 86Rf07
# 1 2 3
namespace eval sqids {
oo::class create idscope {
variable o_alphabet
variable o_alphabet_configured
variable o_alpha_re
variable o_minlength
variable o_blocklist
#note that methods beginning with uppercase letters are private.
constructor {args} {
set defaults [dict create {*}{
-alphabet ""
-minlength ""
-blocklist ""
}]
if {[llength $args] %2 !=0} {
error "sqids::idscope constructor: Require option value pairs. Known options:[dict keys $defaults]."
}
set useropts [dict create]
set explicit_empty_blocklist 0 ;#as opposed to default due to being unspecified.
dict for {k v} $args {
set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $k]
switch -exact -- $fullmatch {
-alphabet - -minlength {
dict set useropts $fullmatch $v
}
-blocklist {
if {[llength $v] == 0} {
set explicit_empty_blocklist 1
}
dict set useropts -blocklist $v
}
default {
error "sqids::idscope constructor: unknown option '$k'. Known options:[dict keys $defaults]."
}
}
}
set opts [dict merge $defaults $useropts]
set opt_alphabet [dict get $opts -alphabet]
if {$opt_alphabet eq ""} {
set o_alphabet $::sqids::data::default_alphabet
} else {
if {[string length $opt_alphabet] < 3} {
error "sqids::idscope constructor: -alphabet length must be at least 3."
}
#review - deny multibyte
if {[regexp {[^\u00-\u7F]} $opt_alphabet]} {
error "sqids::idscope constructor: -alphabet must not contain multibyte characters."
}
if {[regexp {(.).*\1} $opt_alphabet]} {
error "sqids::idscope constructor: -alphabet must contain unique characters."
}
set o_alphabet $opt_alphabet
}
set o_alphabet_configured $o_alphabet ;#for use in public method alphabet, which returns the configured alphabet in the order it was configured, not the shuffled order used for encoding.
set alphamatch [string map [list . \\. \[ \\\[ \] \\\] \{ \\\{ \} \\\}] $o_alphabet] ;#review
set o_alpha_re "^\[$alphamatch\]+\$" ;#independent of shuffled order.
set o_alphabet [my shuffle $o_alphabet[set o_alphabet {}]]
set opt_minlength [dict get $opts -minlength]
if {$opt_minlength eq ""} {
set o_minlength $::sqids::data::default_minlength
} else {
set maxval 255
if {![string is integer -strict $opt_minlength] || $opt_minlength < 0 || $opt_minlength > $maxval} {
error "sqids constructor: -minlength must be an integer from 0 to $maxval inclusive."
}
set o_minlength $opt_minlength
}
set opt_blocklist [dict get $opts -blocklist]
if {!$explicit_empty_blocklist && $opt_blocklist eq ""} {
set o_blocklist $::sqids::data::default_blocklist
} else {
set o_blocklist $opt_blocklist
}
set o_blocklist [string tolower $o_blocklist]
}
method config {{option {}}} {
#introspection method.
#return a dict of the configured options if no option specified, otherwise return the value of the specified option.
#no facility is provided to change options after construction as a new idscope object should be used for different configurations (different scope of sqid ids).
#note that -alphabet refers to the configured alphabet in the order it was configured, not the shuffled order used for encoding.
if {$option eq ""} {
#
return [dict create {*}{
} -blocklist $o_blocklist {*}{
} -minlength $o_minlength {*}{
} -alphabet $o_alphabet_configured {*}{
}
]
}
set fullmatch [tcl::prefix::match -error "" {-alphabet -minlength -blocklist} $option]
switch -exact -- $fullmatch {
-alphabet {return $o_alphabet_configured}
-minlength {return $o_minlength}
-blocklist {return $o_blocklist}
default {
error "sqids::idscope config: unknown option '$option'. Known options:-alphabet -minlength -blocklist."
}
}
}
method encode {numlist} {
if {[llength $numlist] == 0} {return}
#cannot encode negative numbers, or non-integers.
foreach num $numlist {
#tcl 9 'string is integer' supports bignums - which can be arbitrarily large.
#tcl 8.6 'string is integer' is limited to 2**32-1
if {![string is integer -strict $num] || $num < 0} {
error "sqids encode: can only encode non-negative integers. Invalid value: '$num'"
}
}
return [my EncodeNumbers $numlist]
}
method EncodeNumbers {numlist {increment 0}} {
#assert number of letters in o_alphabet and number of letters in local alpha are the same and don't effectively change during this function.
#('set alpha {}' in calls to my shuffle is an optimization to avoid shared string and copy-on-write overhead. As alpha is set to the result, it doesn't violate the previous assertion.)
set alpha_len [string length $o_alphabet]
if {$increment > $alpha_len} {
error "sqids EncodeNumbers: Reached max attempts to re-generate the ID"
}
set offset [llength $numlist]
set i -1
foreach v $numlist {
incr i
set x [scan [string index $o_alphabet [expr {$v % $alpha_len}]] %c]
set offset [expr {$offset + $x + $i}]
}
set offset [expr {$offset % $alpha_len}]
set offset [expr {($offset + $increment) % $alpha_len}]
set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1]
set prefix [string index $alpha 0]
set alpha [string reverse $alpha]
set id $prefix
set i -1
foreach num $numlist {
incr i
append id [my ToId $num [string range $alpha 1 end]]
if {$i < [llength $numlist]-1} {
append id [string index $alpha 0]
set alpha [my shuffle $alpha[set alpha {}]]
}
}
if {$o_minlength > [string length $id]} {
append id [string index $alpha 0]
while {$o_minlength - [string length $id] > 0} {
set alpha [my shuffle $alpha[set alpha {}]]
set numchars [expr {min($o_minlength - [string length $id],$alpha_len)}]
append id [string range $alpha 0 $numchars-1]
}
}
if {[my is_blocked $id]} {
set id [my EncodeNumbers $numlist [expr {$increment+1}]]
}
return $id
}
method is_blocked {id} {
#deliberately public method.
if {![llength $o_blocklist]} {
return 0
}
#o_blocklist is stored in lowercase, so compare against lowercase id.
set idtest [string tolower $id]
set idlen [string length $idtest]
if {$idlen < 3} {
#sqids rule: short ids less than 3 chars will not be blocked.
return 0
}
if {$idlen == 3} {
if {$idtest in $o_blocklist} {
return 1
}
} else {
foreach blocked $o_blocklist {
if {[string length $blocked] <= 3} {
#sqids rule: blocklist entries of 3 chars will only be blocked if they match the entire id exactly,
#so skip them in this loop as we've already checked for exact matches of the whole id when idlen == 3.
continue
}
set posn [string first $blocked $idtest]
if {$posn == -1} {
continue
}
if {$posn == 0} {
#whether leetspeak or not, blocklist entries that match at the beginning of the id will be blocked.
return 1
}
if {[regexp {[0-9]} $blocked]} {
#sqids rule: blocklist entries with digits (leetspeak) will only be blocked if the match is at the beginning or end of the id.
#we've already checked the beginning, so check the end now.
set endpos [expr {$idlen - [string length $blocked]}]
if {$posn == $endpos} {
return 1
}
} else {
#sqids rule: blocklist entries without digits will be blocked if they match anywhere in the id.
return 1
}
}
}
return 0
}
method ToId {num alpha} {
set id ""
set alpha_len [string length $alpha]
while 1 {
set id [string index $alpha [expr {$num % $alpha_len}]]$id
set num [expr {$num / $alpha_len}]
if {$num == 0} break
}
return $id
}
method ToNumber {id alpha} {
set number 0
set alpha_len [string length $alpha]
for {set i 0} {$i < [string length $id]} {incr i} {
set posn [string first [string index $id $i] $alpha]
set number [expr {($number * $alpha_len) + $posn}]
}
return $number
}
method shuffle {alpha} {
#public method. Primarily for internal use but can be used externally to examine the shuffled alphabet being used for encoding.
#e.g myscopeobject shuffle [myscopeobject config -alphabet] would show the shuffled alphabet being used for encoding.
#consistent shuffle (always produce the same result for same input)
set alpha_len [string length $alpha]
if {$alpha_len < 2} {
return $alpha
}
set chars [split $alpha ""]
for {set i 0; set j [expr {$alpha_len-1}]} {$j > 0} {incr i; incr j -1} {
set iv [scan [lindex $chars $i] %c]
set jv [scan [lindex $chars $j] %c]
set r [expr {($i * $j + $iv + $jv) % $alpha_len}]
set item2 [lindex $chars $r]
lset chars $r [lindex $chars $i]
lset chars $i $item2
}
return [join $chars ""]
}
method decode {id} {
if {$id eq ""} {return}
set result [list]
if {![regexp $o_alpha_re $id]} {
puts stderr "sqids decode: ID contains characters not in the alphabet. re: $o_alpha_re id: $id"
return [list]
}
set prefix [string index $id 0]
set offset [string first $prefix $o_alphabet]
set alpha [string range $o_alphabet $offset end][string range $o_alphabet 0 $offset-1]
set alpha [string reverse $alpha]
set id [string range $id 1 end]
while {[string length $id] > 0} {
set separator [string index $alpha 0]
#split on first occurrence of separator only.
set sep_posn [string first $separator $id]
if {$sep_posn == -1} {
set parts [list $id]
} else {
set parts [list [string range $id 0 $sep_posn-1] [string range $id $sep_posn+1 end]]
}
#assert parts has 1 or 2 elements
if {[lindex $parts 0] eq ""} {
#separator was at start of the id - done.
return $result
}
lappend result [my ToNumber [lindex $parts 0] [string range $alpha 1 end]]
if {[llength $parts] == 2} {
set alpha [my shuffle $alpha[set alpha {}]]
set id [lindex $parts 1]
} else {
set id ""
}
}
return $result
}
}
}
namespace eval sqids::data {
variable default_alphabet {abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}
variable default_minlength 0
variable default_blocklist {
0rgasm
1d10t
1d1ot
1di0t
1diot
1eccacu10
1eccacu1o
1eccacul0
1eccaculo
1mbec11e
1mbec1le
1mbeci1e
1mbecile
a11upat0
a11upato
a1lupat0
a1lupato
aand
ah01e
ah0le
aho1e
ahole
al1upat0
al1upato
allupat0
allupato
ana1
ana1e
anal
anale
anus
arrapat0
arrapato
arsch
arse
ass
b00b
b00be
b01ata
b0ceta
b0iata
b0ob
b0obe
b0sta
b1tch
b1te
b1tte
ba1atkar
balatkar
bastard0
bastardo
batt0na
battona
bitch
bite
bitte
bo0b
bo0be
bo1ata
boceta
boiata
boob
boobe
bosta
bran1age
bran1er
bran1ette
bran1eur
bran1euse
branlage
branler
branlette
branleur
branleuse
c0ck
c0g110ne
c0g11one
c0g1i0ne
c0g1ione
c0gl10ne
c0gl1one
c0gli0ne
c0glione
c0na
c0nnard
c0nnasse
c0nne
c0u111es
c0u11les
c0u1l1es
c0u1lles
c0ui11es
c0ui1les
c0uil1es
c0uilles
c11t
c11t0
c11to
c1it
c1it0
c1ito
cabr0n
cabra0
cabrao
cabron
caca
cacca
cacete
cagante
cagar
cagare
cagna
cara1h0
cara1ho
caracu10
caracu1o
caracul0
caraculo
caralh0
caralho
cazz0
cazz1mma
cazzata
cazzimma
cazzo
ch00t1a
ch00t1ya
ch00tia
ch00tiya
ch0d
ch0ot1a
ch0ot1ya
ch0otia
ch0otiya
ch1asse
ch1avata
ch1er
ch1ng0
ch1ngadaz0s
ch1ngadazos
ch1ngader1ta
ch1ngaderita
ch1ngar
ch1ngo
ch1ngues
ch1nk
chatte
chiasse
chiavata
chier
ching0
chingadaz0s
chingadazos
chingader1ta
chingaderita
chingar
chingo
chingues
chink
cho0t1a
cho0t1ya
cho0tia
cho0tiya
chod
choot1a
choot1ya
chootia
chootiya
cl1t
cl1t0
cl1to
clit
clit0
clito
cock
cog110ne
cog11one
cog1i0ne
cog1ione
cogl10ne
cogl1one
cogli0ne
coglione
cona
connard
connasse
conne
cou111es
cou11les
cou1l1es
cou1lles
coui11es
coui1les
couil1es
couilles
cracker
crap
cu10
cu1att0ne
cu1attone
cu1er0
cu1ero
cu1o
cul0
culatt0ne
culattone
culer0
culero
culo
cum
cunt
d11d0
d11do
d1ck
d1ld0
d1ldo
damn
de1ch
deich
depp
di1d0
di1do
dick
dild0
dildo
dyke
encu1e
encule
enema
enf01re
enf0ire
enfo1re
enfoire
estup1d0
estup1do
estupid0
estupido
etr0n
etron
f0da
f0der
f0ttere
f0tters1
f0ttersi
f0tze
f0utre
f1ca
f1cker
f1ga
fag
fica
ficker
figa
foda
foder
fottere
fotters1
fottersi
fotze
foutre
fr0c10
fr0c1o
fr0ci0
fr0cio
fr0sc10
fr0sc1o
fr0sci0
fr0scio
froc10
froc1o
froci0
frocio
frosc10
frosc1o
frosci0
froscio
fuck
g00
g0o
g0u1ne
g0uine
gandu
go0
goo
gou1ne
gouine
gr0gnasse
grognasse
haram1
harami
haramzade
hund1n
hundin
id10t
id1ot
idi0t
idiot
imbec11e
imbec1le
imbeci1e
imbecile
j1zz
jerk
jizz
k1ke
kam1ne
kamine
kike
leccacu10
leccacu1o
leccacul0
leccaculo
m1erda
m1gn0tta
m1gnotta
m1nch1a
m1nchia
m1st
mam0n
mamahuev0
mamahuevo
mamon
masturbat10n
masturbat1on
masturbate
masturbati0n
masturbation
merd0s0
merd0so
merda
merde
merdos0
merdoso
mierda
mign0tta
mignotta
minch1a
minchia
mist
musch1
muschi
n1gger
neger
negr0
negre
negro
nerch1a
nerchia
nigger
orgasm
p00p
p011a
p01la
p0l1a
p0lla
p0mp1n0
p0mp1no
p0mpin0
p0mpino
p0op
p0rca
p0rn
p0rra
p0uff1asse
p0uffiasse
p1p1
p1pi
p1r1a
p1rla
p1sc10
p1sc1o
p1sci0
p1scio
p1sser
pa11e
pa1le
pal1e
palle
pane1e1r0
pane1e1ro
pane1eir0
pane1eiro
panele1r0
panele1ro
paneleir0
paneleiro
patakha
pec0r1na
pec0rina
pecor1na
pecorina
pen1s
pendej0
pendejo
penis
pip1
pipi
pir1a
pirla
pisc10
pisc1o
pisci0
piscio
pisser
po0p
po11a
po1la
pol1a
polla
pomp1n0
pomp1no
pompin0
pompino
poop
porca
porn
porra
pouff1asse
pouffiasse
pr1ck
prick
pussy
put1za
puta
puta1n
putain
pute
putiza
puttana
queca
r0mp1ba11e
r0mp1ba1le
r0mp1bal1e
r0mp1balle
r0mpiba11e
r0mpiba1le
r0mpibal1e
r0mpiballe
rand1
randi
rape
recch10ne
recch1one
recchi0ne
recchione
retard
romp1ba11e
romp1ba1le
romp1bal1e
romp1balle
rompiba11e
rompiba1le
rompibal1e
rompiballe
ruff1an0
ruff1ano
ruffian0
ruffiano
s1ut
sa10pe
sa1aud
sa1ope
sacanagem
sal0pe
salaud
salope
saugnapf
sb0rr0ne
sb0rra
sb0rrone
sbattere
sbatters1
sbattersi
sborr0ne
sborra
sborrone
sc0pare
sc0pata
sch1ampe
sche1se
sche1sse
scheise
scheisse
schlampe
schwachs1nn1g
schwachs1nnig
schwachsinn1g
schwachsinnig
schwanz
scopare
scopata
sexy
sh1t
shit
slut
sp0mp1nare
sp0mpinare
spomp1nare
spompinare
str0nz0
str0nza
str0nzo
stronz0
stronza
stronzo
stup1d
stupid
succh1am1
succh1ami
succhiam1
succhiami
sucker
t0pa
tapette
test1c1e
test1cle
testic1e
testicle
tette
topa
tr01a
tr0ia
tr0mbare
tr1ng1er
tr1ngler
tring1er
tringler
tro1a
troia
trombare
turd
twat
vaffancu10
vaffancu1o
vaffancul0
vaffanculo
vag1na
vagina
verdammt
verga
w1chsen
wank
wichsen
x0ch0ta
x0chota
xana
xoch0ta
xochota
z0cc01a
z0cc0la
z0cco1a
z0ccola
z1z1
z1zi
ziz1
zizi
zocc01a
zocc0la
zocco1a
zoccola
}
}
package provide sqids [namespace eval sqids {
variable version
set version 0.3.0
}]

1293
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/filelist-bindings.tcl

File diff suppressed because it is too large Load Diff

3604
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/What-is-New-in-TkTreeCtrl.html

File diff suppressed because it is too large Load Diff

4408
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/htmldoc/treectrl.html

File diff suppressed because it is too large Load Diff

8
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/pkgIndex.tcl

@ -1,8 +0,0 @@
if {[catch {package require Tcl 8.6-}]} return
set script ""
if {![info exists ::env(TREECTRL_LIBRARY)]
&& [file exists [file join $dir treectrl.tcl]]} {
append script "[list set ::treectrl_library $dir]\n"
}
append script [list load [file join $dir treectrl24.dll] [string totitle treectrl 0 0]]
package ifneeded treectrl 2.4.2 $script

1951
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl.tcl

File diff suppressed because it is too large Load Diff

BIN
src/vfs/punk9win.vfs/lib_tcl9/treectrl2.4.2/treectrl24.dll

Binary file not shown.
Loading…
Cancel
Save