Browse Source

ansi and raw repl modes - ansi testfiles

master
Julian Noble 10 months ago
parent
commit
cab6dc2e0c
  1. 1
      src/bootsupport/include_modules.config
  2. 1756
      src/bootsupport/modules/overtype-1.5.9.tm
  3. 1019
      src/bootsupport/modules/punk/ansi-0.1.1.tm
  4. 132
      src/bootsupport/modules/punk/args-0.1.0.tm
  5. 190
      src/bootsupport/modules/punk/cap/handlers/templates-0.1.0.tm
  6. 544
      src/bootsupport/modules/punk/char-0.1.0.tm
  7. 670
      src/bootsupport/modules/punk/console-0.1.1.tm
  8. 4
      src/bootsupport/modules/punk/encmime-0.1.0.tm
  9. 64
      src/bootsupport/modules/punk/fileline-0.1.0.tm
  10. 1847
      src/bootsupport/modules/punk/lib-0.1.1.tm
  11. 166
      src/bootsupport/modules/punk/mix/base-0.1.tm
  12. 8
      src/bootsupport/modules/punk/mix/commandset/layout-0.1.0.tm
  13. 4
      src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm
  14. 67
      src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm
  15. 20
      src/bootsupport/modules/punk/ns-0.1.0.tm
  16. 28
      src/bootsupport/modules/punk/path-0.1.0.tm
  17. 52
      src/bootsupport/modules/punk/repo-0.1.1.tm
  18. 12
      src/bootsupport/modules/punkcheck-0.1.0.tm
  19. 1
      src/bootsupport/modules/textutil/wcswidth-35.1.tm
  20. 289
      src/modules/flagfilter-0.3.tm
  21. 854
      src/modules/punk-0.1.tm
  22. 1265
      src/modules/punk/ansi-999999.0a1.0.tm
  23. 128
      src/modules/punk/args-999999.0a1.0.tm
  24. 190
      src/modules/punk/cap/handlers/templates-999999.0a1.0.tm
  25. 540
      src/modules/punk/char-999999.0a1.0.tm
  26. 3
      src/modules/punk/config-0.1.tm
  27. 174
      src/modules/punk/console-999999.0a1.0.tm
  28. 55
      src/modules/punk/fileline-999999.0a1.0.tm
  29. 508
      src/modules/punk/lib-999999.0a1.0.tm
  30. 2
      src/modules/punk/lib-buildversion.txt
  31. 166
      src/modules/punk/mix/base-0.1.tm
  32. 1041
      src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm
  33. 65
      src/modules/punk/ns-999999.0a1.0.tm
  34. 24
      src/modules/punk/path-999999.0a1.0.tm
  35. 362
      src/modules/punk/repl-0.1.tm
  36. 52
      src/modules/punk/repo-999999.0a1.0.tm
  37. 18
      src/modules/punk/timeinterval-999999.0a1.0.tm
  38. 35044
      src/modules/punk/uc-999999.0a1.0.tm
  39. 3
      src/modules/punk/uc-buildversion.txt
  40. 124
      src/modules/punkcheck-0.1.0.tm
  41. 38
      src/modules/punkcheck/cli-999999.0a1.0.tm
  42. 441
      src/modules/shellfilter-0.1.9.tm
  43. 8
      src/modules/shellrun-0.1.1.tm
  44. 435
      src/modules/textblock-999999.0a1.0.tm
  45. 76
      src/testansi/beastie.ans
  46. 72
      src/testansi/fish.ans
  47. 95
      src/testansi/flower.ans
  48. 201
      src/testansi/fruit.ans
  49. 20
      src/testansi/punk.ansi
  50. 1236
      src/vendormodules/overtype-1.5.8.tm
  51. 2194
      src/vendormodules/overtype-1.5.9.tm
  52. 1
      src/vendormodules/textutil/wcswidth-35.1.tm

1
src/bootsupport/include_modules.config

@ -10,6 +10,7 @@ set bootsupport_modules [list\
src/vendormodules fileutil\
src/vendormodules textutil::tabify\
src/vendormodules textutil::split\
src/vendormodules textutil::wcswidth\
modules punkcheck\
modules punk::ansi\
modules punk::args\

1756
src/bootsupport/modules/overtype-1.5.9.tm

File diff suppressed because it is too large Load Diff

1019
src/bootsupport/modules/punk/ansi-0.1.1.tm

File diff suppressed because it is too large Load Diff

132
src/bootsupport/modules/punk/args-0.1.0.tm

@ -81,9 +81,9 @@
#[para] packages used by punk::args
#[list_begin itemized]
package require Tcl 8.6
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
@ -174,7 +174,6 @@ namespace eval punk::args {
#} $args
set optionspecs [string map [list \r\n \n] $optionspecs]
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi]
set optspec_defaults [dict create\
-optional 1\
-allow_ansi 1\
@ -204,11 +203,8 @@ namespace eval punk::args {
foreach ln $records {
set trimln [string trim $ln]
if {$trimln eq ""} {
continue
}
if {[string index $trimln 0] eq "#"} {
continue
switch -- [string index $trimln 0] {
"" - # {continue}
}
set argname [lindex $trimln 0]
set argspecs [lrange $trimln 1 end]
@ -224,8 +220,13 @@ namespace eval punk::args {
error "punk::args::opts_values - bad optionspecs line for argument '$argname' Remaining items on line must be in paired option-value format - received '$argspecs'"
}
dict for {spec specval} $argspecs {
if {$spec ni [concat $known_argspecs -ARGTYPE]} {
error "punk::args::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
#literal-key switch - bytecompiled to jumpTable
switch -- $spec {
-default - -type - -range - -choices - -nocase - -optional - -multiple - -validate_without_ansi - -allow_ansi - -strip_ansi - -ARGTYPE {}
default {
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi]
error "punk::args::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
}
}
}
set argspecs [dict merge $optspec_defaults $argspecs]
@ -450,65 +451,90 @@ namespace eval punk::args {
if {!$is_default} {
if {[dict exists $arg_info $o -type]} {
set type [dict get $arg_info $o -type]
if {[string tolower $type] in {int integer double}} {
if {[string tolower $type] in {int integer}} {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {double}} {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
switch -- [string tolower $type] {
int -
integer -
double {
if {[string tolower $type] in {int integer}} {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {double}} {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
}
}
}
}
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
}
}
}
}
} elseif {[string tolower $type] in {bool boolean}} {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {alnum alpha ascii control digit graph lower print punct space upper wordchar xdigit}} {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
bool -
boolean {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
}
} elseif {[string tolower $type] in {file directory existingfile existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
alnum -
alpha -
ascii -
control -
digit -
graph -
lower -
print -
punct -
space -
upper -
wordchar -
xdigit {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
}
}
}
if {[string tolower $type] in {existingfile}} {
file -
directory -
existingfile -
existingdirectory {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
if {[string tolower $type] in {existingfile}} {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
}
}
}
}
} elseif {[string tolower $type] in {char character}} {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
char -
character {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
}
}
}
}

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

@ -73,110 +73,118 @@ namespace eval punk::cap::handlers::templates {
#adhoc and currentproject* paths are relative to cwd - so no projectbase information can be stored at registration time.
#not all template item types will need projectbase information - as the item data may be self-contained within the template structure -
#but project_layout will need it - or at least need to know if there is no project - because project_layout data is never stored in the template folder structure directly.
if {$pathtype eq "adhoc"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
} elseif {$pathtype eq "module"} {
set provide_statement [package ifneeded $pkg [package require $pkg]]
set tmfile [lindex $provide_statement end]
if {![file exists $tmfile]} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - unable to determine base folder for package '$pkg' which is attempting to register with punk::cap as a provider of '$capname' capability"
return 0
switch -- $pathtype {
adhoc {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
}
module {
set provide_statement [package ifneeded $pkg [package require $pkg]]
set tmfile [lindex $provide_statement end]
if {![file exists $tmfile]} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - unable to determine base folder for package '$pkg' which is attempting to register with punk::cap as a provider of '$capname' capability"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
}
set tmfolder [file dirname $tmfile]
#todo - handle wrapped or unwrapped tarjar files - in which case we have to adjust tmfolder appropriately
#set tpath [file normalize [file join $tmfile [dict get $capdict relpath]]] ;#relpath is relative to the tm *file* - not it's containing folder
set projectinfo [punk::repo::find_repos $tmfolder]
set projectbase [dict get $projectinfo closest]
#store the projectbase even if it's empty string
set extended_capdict $capdict
set resolved_path [file join $tmfolder $path]
dict set extended_capdict resolved_path $resolved_path
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "currentproject_multivendor"} {
#currently only intended for punk::mix::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
}
set tmfolder [file dirname $tmfile]
#todo - handle wrapped or unwrapped tarjar files - in which case we have to adjust tmfolder appropriately
#set tpath [file normalize [file join $tmfile [dict get $capdict relpath]]] ;#relpath is relative to the tm *file* - not it's containing folder
set projectinfo [punk::repo::find_repos $tmfolder]
set projectbase [dict get $projectinfo closest]
#store the projectbase even if it's empty string
set extended_capdict $capdict
set resolved_path [file join $tmfolder $path]
dict set extended_capdict resolved_path $resolved_path
dict set extended_capdict projectbase $projectbase
}
currentproject_multivendor {
#currently only intended for punk::mix::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor ;#vendor key still required.. controlling vendor?
} elseif {$pathtype eq "currentproject"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
set extended_capdict $capdict
dict set extended_capdict vendor $vendor ;#vendor key still required.. controlling vendor?
}
#verify that the relative path is within the relative path of a currentproject_multivendor tree
#todo - api for the _multivendor tree controlling package to validate
currentproject {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
#verify that the relative path is within the relative path of a currentproject_multivendor tree
#todo - api for the _multivendor tree controlling package to validate
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
} elseif {$pathtype eq "shellproject"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "shellproject_multivendor"} {
#currently only intended for punk::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
shellproject {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
shellproject_multivendor {
#currently only intended for punk::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "absolute"} {
if {[file pathtype $path] ne "absolute"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be absolute"
return 0
absolute {
if {[file pathtype $path] ne "absolute"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be absolute"
return 0
}
set normpath [file normalize $path]
if {!file exists $normpath} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' which doesn't seem to exist"
return 0
}
set projectinfo [punk::repo::find_repos $normpath]
set projectbase [dict get $projectinfo closest]
#todo - verify no other provider has registered same absolute path - if sharing a project-external location is needed - they need their own subfolder
set extended_capdict $capdict
dict set extended_capdict resolved_path $normpath
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
set normpath [file normalize $path]
if {!file exists $normpath} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' which doesn't seem to exist"
default {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' with unrecognised type $pathtype"
return 0
}
set projectinfo [punk::repo::find_repos $normpath]
set projectbase [dict get $projectinfo closest]
#todo - verify no other provider has registered same absolute path - if sharing a project-external location is needed - they need their own subfolder
set extended_capdict $capdict
dict set extended_capdict resolved_path $normpath
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} else {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' with unrecognised type $pathtype"
return 0
}
# -- --- --- --- --- --- --- ---- ---

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

@ -55,7 +55,9 @@
#[item] [package console]
#[para] -
package require Tcl 8.6
package require Tcl 8.6-
#dependency on tcllib not ideal for bootstrapping as punk::char is core to many features.. (textutil::wcswidth is therefore included in bootsupport/include_modules.config) review
package require textutil::wcswidth
#*** !doctools
#[list_end]
@ -71,6 +73,7 @@ package require Tcl 8.6
namespace eval punk::char {
namespace export *
variable grapheme_widths [dict create]
# -- --------------------------------------------------------------------------
variable encmimens ;#namespace of mime package providing reversemapencoding and mapencoding functions
#tcllib mime requires tcl::chan::memchan,events,core and/or Trf
@ -525,7 +528,7 @@ namespace eval punk::char {
# e.g encoding convertto dingbats <somethingpretty> will output something that doesn't look dingbatty on screen.
#-- --- --- --- --- --- --- ---
#must use Tcl instead of tcl (at least for 8.6)
if {![package vsatisfies [package present Tcl] 8.7]} {
if {![package vsatisfies [package present Tcl] 8.7-]} {
proc encodable "s {enc [encoding system]}" {
set encname [encname $enc]
if {($encname eq "ascii")} {
@ -1259,70 +1262,71 @@ namespace eval punk::char {
variable charsets
set hex_char [format %04x $dec_char]
set returninfo [dict create]
if {"dec" in $fields} {
dict set returninfo dec $dec_char
}
if {"hex" in $fields} {
dict set returninfo hex $hex_char
}
if {"desc" in $fields} {
if {[dict exists $charinfo $dec_char desc]} {
dict set returninfo desc [dict get $charinfo $dec_char desc]
} else {
dict set returninfo desc ""
}
}
if {"short" in $fields} {
if {[dict exists $charinfo $dec_char short]} {
dict set returninfo desc [dict get $charinfo $dec_char short]
} else {
dict set returninfo short ""
}
}
#todo - expectedwidth - lookup the printing width it is *supposed* to have from unicode tables
#testwidth is one of the main ones likely to be worthwhile excluding as querying the console via ansi takes time
if {"testwidth" in $fields} {
set existing_testwidth ""
if {[dict exists $charinfo $dec_char testwidth]} {
set existing_testwidth [dict get $charinfo $dec_char testwidth]
}
if {$existing_testwidth eq ""} {
#no cached data - do expensive cursor-position test (Note this might not be 100% reliable - terminals lie e.g if ansi escape sequence has set to wide printing.)
set char [format %c $dec_char]
set chwidth [char_info_testwidth $char]
dict set returninfo testwidth $chwidth
#cache it. todo - -verify flag to force recalc in case font/terminal changed in some way?
dict set charinfo $dec_char testwidth $chwidth
} else {
dict set returninfo testwidth $existing_testwidth
}
}
if {"char" in $fields} {
set char [format %c $dec_char]
dict set returninfo char $char
}
#memberof takes in the order of a few hundred microseconds if a simple scan of all ranges is taken - possibly worthwhile caching/optimising
#note that memberof is not just unicode blocks - but scripts and other non-contiguous sets consisting of multiple ranges - some of which may include ranges of a single character. (e.g WGL4)
#This means there probably isn't a really efficient means of calculating membership other than scanning all the defined ranges.
#We could potentially populate it using a side-thread - but it seems reasonable to just cache result after first use here.
#some efficiency could also be gained by pre-calculating the extents for each charset which isn't a simple unicode block. (and perhaps sorting by max char)
if {"memberof" in $fields} {
set memberof [list]
dict for {setname setinfo} $charsets {
foreach r [dict get $setinfo ranges] {
set s [dict get $r start]
set e [dict get $r end]
if {$dec_char >= $s && $dec_char <= $e} {
lappend memberof $setname
break
foreach f $fields {
switch -- $f {
dec {
dict set returninfo dec $dec_char
}
hex {
dict set returninfo hex $hex_char
}
desc {
if {[dict exists $charinfo $dec_char desc]} {
dict set returninfo desc [dict get $charinfo $dec_char desc]
} else {
dict set returninfo desc ""
}
}
short {
if {[dict exists $charinfo $dec_char short]} {
dict set returninfo desc [dict get $charinfo $dec_char short]
} else {
dict set returninfo short ""
}
}
testwidth {
#todo - expectedwidth - lookup the printing width it is *supposed* to have from unicode tables
#testwidth is one of the main ones likely to be worthwhile excluding as querying the console via ansi takes time
set existing_testwidth ""
if {[dict exists $charinfo $dec_char testwidth]} {
set existing_testwidth [dict get $charinfo $dec_char testwidth]
}
if {$existing_testwidth eq ""} {
#no cached data - do expensive cursor-position test (Note this might not be 100% reliable - terminals lie e.g if ansi escape sequence has set to wide printing.)
set char [format %c $dec_char]
set chwidth [char_info_testwidth $char]
dict set returninfo testwidth $chwidth
#cache it. todo - -verify flag to force recalc in case font/terminal changed in some way?
dict set charinfo $dec_char testwidth $chwidth
} else {
dict set returninfo testwidth $existing_testwidth
}
}
char {
set char [format %c $dec_char]
dict set returninfo char $char
}
memberof {
#memberof takes in the order of a few hundred microseconds if a simple scan of all ranges is taken - possibly worthwhile caching/optimising
#note that memberof is not just unicode blocks - but scripts and other non-contiguous sets consisting of multiple ranges - some of which may include ranges of a single character. (e.g WGL4)
#This means there probably isn't a really efficient means of calculating membership other than scanning all the defined ranges.
#We could potentially populate it using a side-thread - but it seems reasonable to just cache result after first use here.
#some efficiency could also be gained by pre-calculating the extents for each charset which isn't a simple unicode block. (and perhaps sorting by max char)
set memberof [list]
dict for {setname setinfo} $charsets {
foreach r [dict get $setinfo ranges] {
set s [dict get $r start]
set e [dict get $r end]
if {$dec_char >= $s && $dec_char <= $e} {
lappend memberof $setname
break
}
}
}
dict set returninfo memberof $memberof
}
}
dict set returninfo memberof $memberof
}
return $returninfo
@ -1512,31 +1516,75 @@ namespace eval punk::char {
#non-overlapping unicode blocks
proc char_blocks {name_or_glob} {
error "unicode block searching unimplemented"
#todo - search only charsets that have settype = block
proc char_blocks {{name_or_glob *}} {
variable charsets
#todo - more efficient datastructures?
if {![regexp {[?*]} $name_or_glob]} {
#no glob - just retrieve it
if {[dict exists $charsets $name_or_glob]} {
if {[dict get $charsets $name_or_glob settype] eq "block"} {
return [dict create $name_or_glob [dict get $charsets $name_or_glob]]
}
}
#no exact match - try case insensitive..
set name [lsearch -inline -nocase [dict keys $charsets] $name_or_glob]
if {$name ne ""} {
if {[dict get $charsets $name settype] eq "block"} {
return [dict create $name [dict get $charsets $name]]
}
}
} else {
#build a subset
set charsets_block [dict create]
dict for {k v} $charsets {
if {[string match -nocase $name_or_glob $k]} {
if {[dict get $v settype] eq "block"} {
dict set charsets_block $k $v
}
}
}
return $charsets_block
}
}
proc charset_names {{name_or_glob *}} {
variable charsets
if {![regexp {[?*]} $name_or_glob]} {
#no glob - just retrieve it
if {[dict exists $charsets $name_or_glob]} {
return [list $name_or_glob]
}
#no exact match - try case insensitive..
set name [lsearch -inline -nocase [dict keys $charsets] $name_or_glob]
if {$name ne ""} {
return [list $name]
}
} else {
if {$name_or_glob eq "*"} {
return [lsort [dict keys $charsets]]
}
return [lsort [lsearch -all -inline -nocase [dict keys $charsets] $name_or_glob]]
}
}
#deprecated
#major named sets such as unicode blocks, scripts, and other sets such as microsoft WGL4
#case insensitive search - possibly with globs
proc charset_names {{namesearch *}} {
#case insensitive search - possibly with *basic* globs (? and * only - not lb rb)
proc charset_names2 {{namesearch *}} {
variable charsets
set sortedkeys [lsort -increasing -dictionary [dict keys $charsets]] ;#NOTE must use -dictionary to use -sorted flag below
#dictionary sorting of the keys is slow! - we should obviously store it in sorted order instead of sorting entire list on retrieval - or just sort results
#set sortedkeys [lsort -increasing -dictionary [dict keys $charsets]] ;#NOTE must use -dictionary to use -sorted flag below
set sortedkeys [lsort -increasing [dict keys $charsets]]
if {$namesearch eq "*"} {
return $sortedkeys
}
if {[regexp {[?*]} $namesearch]} {
#name glob search
set matched_names [lsearch -all -inline -nocase $sortedkeys $namesearch] ;#no point using -sorted flag when -all is used
return [lsearch -all -inline -nocase $sortedkeys $namesearch] ;#no point using -sorted flag when -all is used
} else {
set matched [lsearch -sorted -inline -nocase $sortedkeys $namesearch] ;#no globs - stop on first match
if {[llength $matched]} {
return [list $matched]
} else {
return [list]
}
#return [lsearch -sorted -inline -nocase $sortedkeys $namesearch] ;#no globs - bails out earlier if -sorted?
return [lsearch -inline -nocase $sortedkeys $namesearch] ;#no globs
}
return $matched_names
}
proc charsets {{namesearch *}} {
package require textblock
@ -1585,7 +1633,7 @@ namespace eval punk::char {
}
set dict_list [list]
foreach m $matches {
lappend dict_list [dict create $m [charset_dictget $name]]
lappend dict_list [dict create $m [charset_dictget $m]]
}
#return $dict_list
return [join $dict_list \n]
@ -1651,7 +1699,8 @@ namespace eval punk::char {
set twidth [dict get $charinfo $dec testwidth]
}
if {$twidth eq ""} {
set width [string_width $ch] ;#based on unicode props
#set width [ansifreestring_width $ch] ;#based on unicode props
set width [grapheme_width_cached $ch]
} else {
set width $twidth
}
@ -1780,7 +1829,7 @@ namespace eval punk::char {
}
if {$twidth eq ""} {
#puts -nonewline stdout "." ;#this
set width [char_info_testwidth $ch] ;#based on unicode props
set width [char_info_testwidth $ch] ;#based on console test rather than unicode props
dict set charinfo $dec testwidth $width
} else {
set width $twidth
@ -1792,32 +1841,164 @@ namespace eval punk::char {
puts stdout "\ncalibration done - results cached in charinfo dictionary"
return [dict create charcount $charcount widths $width_results]
}
#prerequisites - no ansi escapes - no newlines
#review - what about \r \t \b ?
proc string_width {text} {
#review is detecting \033 enough? what about 8-bit escapes?
#maint warning - also in overtype!
#intended for single grapheme - but will work for multiple
#cannot contain ansi or newlines
#(a cache of ansifreestring_width calls - as these are quite regex heavy)
proc grapheme_width_cached {ch} {
variable grapheme_widths
if {[dict exists $grapheme_widths $ch]} {
return [dict get $grapheme_widths $ch]
}
set width [punk::char::ansifreestring_width $ch] ;#review - can we provide faster version if we know it's a single grapheme rather than a string? (grapheme is still a string as it may have combiners/diacritics)
dict set grapheme_widths $ch $width
return $width
}
#no char_width - use grapheme_width terminology to be clearer
proc grapheme_width {char} {
error "grapheme_width unimplemented - use ansifreestring_width"
}
#return N Na W etc from unicode data
#review
proc char_uc_width_prop {char} {
error "char_uc_width unimplemented try textutil::wcswidth_type"
}
#todo - provide a grapheme_width function that is optimised for speed
proc string_width {text} {
#burn approx 2uS (2024) checking for ansi codes - not just SGR
if {[punk::ansi::ta::detect $text]} {
puts stderr "string_width detected ANSI!"
}
if {[string first \n $text] >= 0} {
error "string_width accepts only a single line"
}
tailcall ansifreestring_width $text
}
#faster than textutil::wcswidth (at least for string up to a few K in length)
proc wcswidth {string} {
set codes [scan $string [string repeat %c [string length $string]]]
set width 0
foreach c $codes {
set w [textutil::wcswidth_char $c]
if {$w < 0} {
return -1
} else {
incr width $w
}
}
return $width
}
proc wcswidth2 {string} {
set codes [scan $string [string repeat %c [string length $string]]]
set widths [lmap c $codes {textutil::wcswidth_char $c}]
if {-1 in $widths} {
return -1
}
return [tcl::mathop::+ {*}$widths]
}
#prerequisites - no ansi escapes - no newlines
#review - what about \r \t \b ?
#NO processing of \b - already handled in ansi::printing_length which then calls this
#this version breaks string into sequences of ascii vs unicode
proc ansifreestring_width {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
#we can c0 control characters after or while processing ansi escapes.
#we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!)
#anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error
#if {[string first \033 $text] >= 0} {
# error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first"
#}
#todo - various combining diacritical marks.. from grave - to various complicated unicode joiners and composing chars etc
#as at 2023 - terminals generally seem to use the simplistic approach of tallying up individual character-widths, which means combinations that print short are reported too long by the terminal esc 6 n sequence.
#- {Combining Diacritical Marks} {ranges {{start 768 end 879 note {unicode block 0300..036F}}} description {} settype block}
#- {Combining Diacritical Marks Extended} {ranges {{start 6832 end 6911 note {unicode block 1AB0..1AFF}}} description {} settype block}
#- {Combining Diacritical Marks Supplement} {ranges {{start 7616 end 7679 note {unicode block 1DC0..1DFF}}} description {} settype block}
#- {Combining Diacritical Marks for Symbols} {ranges {{start 8400 end 8447 note {unicode block 20D0..20FF}}} description {} settype block}
#- {Combining Half Marks} {ranges {{start 65056 end 65071 note {unicode block FE20..FE2F}}} description {} settype block}
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
# set text " $text"
#}
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
#review
#if we strip out ZWJ \u0200d (zero width combiner) - then we don't give the chance for a grapheme combining test to merge properly e.g combining emojis
#as at 2024 - textutil::wcswidth doesn't seem to be aware of these - and counts the joiner as one wide
#ZWNJ \u0200c should also be zero length - but as a 'non' joiner - it should add zero to the length
#ZWSP \u0200b zero width space
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
#todo - check double-width chars in unicode blocks.. try to do reasonably quicky
#short-circuit basic cases
if {![regexp {[\uFF-\U10FFFF]} $text]} {
#control chars?
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
# return [string length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
return [string length $text]
}
#split just to get the standalone character widths - and then scan for other combiners (?) - or scan for clusters first?
#review
#set can_regex_high_unicode [string match [regexp -all -inline {[\U80-\U0010FFFF]} \U0001F525] \U0001F525]
#tcl pre 2023-11 - braced high unicode regexes don't work
#fixed in bug-4ed788c618 2023-11
#set uc_chars [regexp -all -inline "\[\u0100-\U10FFFF\]" $text] ;#e.g return list of chars in range only
#maintain unicode as sequences - todo - scan for grapheme clusters
#set uc_sequences [punk::ansi::ta::_perlish_split "(?:\[\u0100-\U10FFFF\])+" $text]
set uc_sequences [punk::ansi::ta::_perlish_split "(?:\[\u0000-\u00FF\])+" $text]
set len 0
foreach {uc ascii} $uc_sequences {
#puts "-ascii $ascii"
#puts "-uc $uc"
incr len [string length $ascii]
#textutil::wcswidth uses unicode data
#fall back to textutil::wcswidth (which doesn't for example handle diactricts/combiners so we can't use until these and other things such as \u200b and diacritics are already stripped/accounted for)
#todo - find something that understands grapheme clusters - needed also for grapheme_split
#use punk::char::wcswidth - faster than the string split in textutil::wcswidth but still uses textutil::wcswidth_char
incr len [wcswidth $uc]
}
#todo - work out what to do with anomalies like grave combiner which print at different lengths on different terminals (fonts?) and for which cursor-query escape sequence lies.
return $len
}
#kept as a fallback for review/test if textutil::wcswidth doesn't do what we require on all terminals.
#this version looks at console testwidths if they've been cached.
#It is relatively fast - but tests unicode widths char by char - so won't be useful going forward for grapheme clusters.
proc ansifreestring_width2 {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
#we can c0 control characters after or while processing ansi escapes.
#we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!)
#anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error
#if {[string first \033 $text] >= 0} {
# error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first"
#}
#todo - various combining diacritical marks.. from grave - to various complicated unicode joiners and composing chars etc
#as at 2023 - terminals generally seem to use the simplistic approach of tallying up individual character-widths, which means combinations that print short are reported too long by the terminal esc 6 n sequence.
@ -1831,7 +2012,7 @@ namespace eval punk::char {
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
@ -1840,7 +2021,30 @@ namespace eval punk::char {
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
#review
#if we strip out ZWJ \u0200d (zero width combiner) - then we don't give the chance for a grapheme combining test to merge properly e.g combining emojis
#as at 2024 - textutil::wcswidth doesn't seem to be aware of these - and counts the joiner as one wide
#ZWNJ \u0200c should also be zero length - but as a 'non' joiner - it should add zero to the length
#ZWSP \u0200b zero width space
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
# return [string length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
return [string length $text]
}
#review - wcswidth should detect these
set re_ascii_fullwidth {[\uFF01-\uFF5e]}
set doublewidth_char_count 0
@ -1851,20 +2055,34 @@ namespace eval punk::char {
#tcl pre 2023-11 - braced high unicode regexes don't work
#fixed in bug-4ed788c618 2023-11
#set uc_sequences [regexp -all -inline -indices {[\u0100-\U10FFFF]} $text]
set uc_sequences [regexp -all -inline -indices "\[\u0100-\U10FFFF\]" $text]
foreach uc_range $uc_sequences {
set chars [string range $text {*}$uc_range]
foreach c $chars {
if {[regexp $re_ascii_fullwidth $c]} {
incr doublewidth_char_count
} else {
#todo - replace with function that doesn't use console - just unicode data
#set uc_sequences [regexp -all -inline -indices "\[\u0100-\U10FFFF\]" $text] ;#e.g for string: \U0001f9d1abc\U001f525ab returns {0 0} {4 4}
set uc_chars [regexp -all -inline "\[\u0100-\U10FFFF\]" $text] ;#e.g return list of chars in range only
foreach c $uc_chars {
if {[regexp $re_ascii_fullwidth $c]} {
incr doublewidth_char_count
} else {
#review
# a)- terminals lie - so we could have a bad cached testwidth
# b)- textutil::wcswidth_char seems to be east-asian-width based - and not a reliable indicator of 'character cells taken by the character when printed to the terminal' despite this statement in tclllib docs.
#(character width is a complex context-dependent topic)
# c) by checking for a cached console testwidth first - we make this function less deterministic/repeatable depending on whether console tests have been run.
# d) Upstream caching of grapheme_width may also lock in a result from whatever method was first employed here
#Despite all this - the mechanism is hoped to give best effort consistency for terminals
#further work needed for combining emojis etc - which can't be done in a per character loop
#todo - see if wcswidth does any of this. It is very slow for strings that include mixed ascii/unicode - so perhaps we can use a perlish_split
# to process sequences of unicode.
#- and the user has the option to test character sets first if terminal position reporting gives better results
if {[char_info_is_testwidth_cached $c]} {
set width [char_info_testwidth_cached $c]
if {$width == 0} {
incr zerowidth_char_count
} elseif {$width == 2} {
incr doublewidth_char_count
}
} else {
#textutil::wcswidth uses unicode data
#fall back to textutil::wcswidth (which doesn't for example handle diactricts/combiners so we can't use until these and other things such as \u200b and diacritics are already stripped/accounted for)
set width [textutil::wcswidth_char [scan $c %c]]
}
if {$width == 0} {
incr zerowidth_char_count
} elseif {$width == 2} {
incr doublewidth_char_count
}
}
}
@ -1872,6 +2090,58 @@ namespace eval punk::char {
return [expr {[string length $text] + $doublewidth_char_count - $zerowidth_char_count}]
}
#slow - textutil::wcswidth is slow with mixed ascii uc
proc ansifreestring_width3 {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
#we can c0 control characters after or while processing ansi escapes.
#we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!)
#anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error
#if {[string first \033 $text] >= 0} {
# error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first"
#}
#review - work out what to do with anomalies like grave combiner which print at different lengths on different terminals (fonts?) and for which cursor-query escape sequence lies.
# - various combining diacritical marks.. from grave - to various complicated unicode joiners and composing chars etc
#as at 2023 - terminals generally seem to use the simplistic approach of tallying up individual character-widths, which means combinations that print short are reported too long by the terminal esc 6 n sequence.
#- {Combining Diacritical Marks} {ranges {{start 768 end 879 note {unicode block 0300..036F}}} description {} settype block}
#- {Combining Diacritical Marks Extended} {ranges {{start 6832 end 6911 note {unicode block 1AB0..1AFF}}} description {} settype block}
#- {Combining Diacritical Marks Supplement} {ranges {{start 7616 end 7679 note {unicode block 1DC0..1DFF}}} description {} settype block}
#- {Combining Diacritical Marks for Symbols} {ranges {{start 8400 end 8447 note {unicode block 20D0..20FF}}} description {} settype block}
#- {Combining Half Marks} {ranges {{start 65056 end 65071 note {unicode block FE20..FE2F}}} description {} settype block}
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
# set text " $text"
#}
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
# return [string length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
return [string length $text]
}
#slow when ascii mixed with unicode (but why?)
return [punk::wcswidth $text]
}
#This shouldn't be called on text containing ansi codes!
proc strip_nonprinting_ascii {str} {
#review - some single-byte 'control' chars have visual representations e.g ETX as heart depending on font/codepage
@ -1885,22 +2155,14 @@ namespace eval punk::char {
return [string map $map $str]
}
proc char_width {char} {
error "char_width unimplemented"
}
#return N Na W etc from unicode data
proc char_uc_width_prop {char} {
error "char_uc_width unimplemented"
}
#split into plaintext and runs of combiners
#split into plaintext and runs of combiners (combining diacritical marks - not ZWJ or ZWJNJ)
proc combiner_split {text} {
#split into odd numbered list (or zero) in a similar way to punk::ansi::ta::_perlish_split
#
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
#set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set graphemes [list]
set g ""
if {[string length $text] == 0} {
return {}
}
@ -1909,7 +2171,7 @@ namespace eval punk::char {
set strlen [string length $text]
#make sure our regexes aren't non-greedy - or we may not have exit condition for loop
#review
while {$start < $strlen && [regexp -start $start -indices -- $re_diacritics $text match]} {
while {$start < $strlen && [regexp -start $start -indices -- {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} $text match]} {
lassign $match matchStart matchEnd
#puts "->start $start ->match $matchStart $matchEnd"
lappend list [string range $text $start $matchStart-1] [string range $text $matchStart $matchEnd]
@ -1920,13 +2182,19 @@ namespace eval punk::char {
#}
}
lappend list [string range $text $start end]
return $list
}
#ZWJ ZWNJ ?
#1st shot - basic diacritics
#todo - become aware of unicode grapheme cluster boundaries
#
#This is difficult in Tcl without unicode property based Character Classes in the regex engine
#review - this needs to be performant - it is used a lot by punk terminal/ansi features
#todo - trie data structures for unicode?
#for now we can at least combine diacritics
#should also handle the ZWJ (and the variation selectors? eg \uFE0F) character which should account for emoji clusters
#Note - emoji cluster could be for example 11 code points/41 bytes (family emoji with skin tone modifiers for each member, 3 ZWJs)
#This still leaves a whole class of clusters.. korean etc unhandled :/
proc grapheme_split {text} {
set graphemes [list]
set csplits [combiner_split $text]
@ -1941,9 +2209,51 @@ namespace eval punk::char {
}
return $graphemes
}
proc grapheme_split_dec {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} [lrange $csplits 0 end-1] {
set pt_decs [scan $pt [string repeat %c [string length $pt]]]
set combiner_decs [scan $combiners [string repeat %c [string length $combiners]]]
lset pt_decs end [concat [lindex $pt_decs end] $combiner_decs]
lappend graphemes {*}$pt_decs
}
#last csplit never has a combiner (_perlish_split style) - and may be empty - in which case we don't append it as a grapheme
if {[lindex $csplits end] ne ""} {
lappend graphemes {*}[scan [lindex $csplits end] [string repeat %c [string length [lindex $csplits end]]]]
}
return $graphemes
}
proc grapheme_split_dec2 {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} $csplits {
set pt_decs [scan $pt [string repeat %c [string length $pt]]]
if {$combiners ne ""} {
set combiner_decs [scan $combiners [string repeat %c [string length $combiners]]]
lset pt_decs end [concat [lindex $pt_decs end] $combiner_decs]
}
lappend graphemes {*}$pt_decs
}
return $graphemes
}
proc grapheme_split2 {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} [lrange $csplits 0 end-1] {
set clist [split $pt ""]
lappend graphemes {*}[lrange $clist 0 end-1] [string cat [lindex $clist end] $combiners]
}
#last csplit never has a combiner (_perlish_split style) - and may be empty - in which case we don't append it as a grapheme
if {[lindex $csplits end] ne ""} {
lappend graphemes {*}[split [lindex $csplits end] ""]
}
return $graphemes
}
# -- --- --- --- ---
#will accept a single char or a string - test using console cursor position reporting
#unreliable
proc char_info_testwidth {ch {emit 0}} {
package require punk::console
#uses cursor movement and relies on console to report position.. which it may do properly for single chars - but may misreport for combinations that combine into a single glyph
@ -1964,6 +2274,10 @@ namespace eval punk::char {
return $twidth
}
}
proc char_info_is_testwidth_cached {char} {
variable charinfo
return [dict exists $charinfo [scan $char %c] testwidth]
}
# -- --- --- --- ---

670
src/bootsupport/modules/punk/console-0.1.1.tm

@ -34,8 +34,20 @@ namespace eval punk::console {
variable previous_stty_state_stdin ""
variable previous_stty_state_stdout ""
variable previous_stty_state_stderr ""
variable is_raw 0
variable input_chunks_waiting
if {![info exists input_chunks_waiting(stdin)]} {
set input_chunks_waiting(stdin) [list]
}
# --
variable ansi_available -1 ;#default -1 for unknown. Leave it this way so test for ansi support is run.
#-1 still evaluates to true - as the modern assumption for ansi availability is true
#only false if ansi_available has been set 0 by test_can_ansi
#support stripansi for legacy windows terminals
# --
variable ansi_wanted 2 ;#2 for default assumed yes, will be set to -1 for automatically unwanted when ansi unavailable values of 0 or 1 won't be autoset
#punk::console namespace - contains *directly* acting functions - some based on ansi escapes from the 'ansi' sub namespace, some on local system calls or executable calls wrapped in the 'local' sub namespace
#directly acting means they write to stdout to cause the console to peform the action, or they perform the action immediately via other means.
@ -55,41 +67,61 @@ namespace eval punk::console {
}
if {"windows" eq $::tcl_platform(platform)} {
proc enableAnsi {} {
#accept args for all dummy/load functions so we don't have to match/update argument signatures here
proc enableAnsi {args} {
#loopavoidancetoken (don't remove)
internal::define_windows_procs
internal::abort_if_loop
tailcall enableAnsi
tailcall enableAnsi {*}$args
}
#review what raw mode means with regard to a specific channel vs terminal as a whole
proc enableRaw {{channel stdin}} {
proc enableRaw {args} {
#loopavoidancetoken (don't remove)
internal::define_windows_procs
internal::abort_if_loop
tailcall enableRaw $channel
tailcall enableRaw {*}$args
}
proc disableRaw {{channel stdin}} {
proc disableRaw {args} {
#loopavoidancetoken (don't remove)
internal::define_windows_procs
internal::abort_if_loop
tailcall disableRaw $channel
tailcall disableRaw {*}$args
}
proc enableVirtualTerminal {} {
proc enableVirtualTerminal {args} {
#loopavoidancetoken (don't remove)
internal::define_windows_procs
internal::abort_if_loop
tailcall enableVirtualTerminal
tailcall enableVirtualTerminal {*}$args
}
proc disableVirtualTerminal {} {
proc disableVirtualTerminal {args} {
#loopavoidancetoken (don't remove)
internal::define_windows_procs
internal::abort_if_loop
tailcall disableVirtualTerminal
tailcall disableVirtualTerminal {*}$args
}
set funcs [list disableAnsi enableProcessedInput disableProcessedInput]
foreach f $funcs {
proc $f {args} [string map [list %f% $f] {
set mybody [info body %f%]
internal::define_windows_procs
set newbody [info body %f%]
if {$newbody ne $mybody} {
tailcall %f% {*}$args
} else {
#error vs noop?
puts stderr "Unable to set implementation for %f% - check twapi?"
}
}]
}
} else {
proc enableAnsi {} {
#todo?
}
proc disableAnsi {} {
}
#todo - something better - the 'channel' concept may not really apply on unix, as raw mode is for input and output modes
proc enableRaw {{channel stdin}} {
@ -118,14 +150,15 @@ namespace eval punk::console {
set is_raw 0
return done
}
proc enableVirtualTerminal {} {
proc enableVirtualTerminal {{channels {input output}}} {
}
proc disableVirtualTerminal {} {
proc disableVirtualTerminal {args} {
}
}
#review - document and decide granularity required. should we enable/disable more than one at once?
proc enable_mouse {} {
puts -nonewline stdout \x1b\[?1000h
puts -nonewline stdout \x1b\[?1003h
@ -157,6 +190,7 @@ namespace eval punk::console {
}
proc mode {{raw_or_line query}} {
variable is_raw
variable ansi_available
set raw_or_line [string tolower $raw_or_line]
if {$raw_or_line eq "query"} {
if {$is_raw} {
@ -166,14 +200,18 @@ namespace eval punk::console {
}
} elseif {$raw_or_line eq "raw"} {
punk::console::enableRaw
punk::console::enableVirtualTerminal both
if {[can_ansi]} {
punk::console::enableVirtualTerminal both
}
} elseif {$raw_or_line eq "line"} {
#review -order. disableRaw has memory from enableRaw.. but but for line mode we want vt disabled - so call it after disableRaw (?)
punk::console::disableRaw
punk::console::disableVirtualTerminal input ;#default readline arrow behaviour etc
punk::console::enableVirtualTerminal output ;#display/use ansi codes
if {[can_ansi]} {
punk::console::disableVirtualTerminal input ;#default readline arrow behaviour etc
punk::console::enableVirtualTerminal output ;#display/use ansi codes
}
} else {
error "punk::console::mode expected 'raw' or 'line' or default value 'query'
error "punk::console::mode expected 'raw' or 'line' or default value 'query'"
}
}
@ -211,6 +249,7 @@ namespace eval punk::console {
#enableAnsi seems like it should be directly under punk::console .. but then it seems inconsistent if other local console-mode setting functions aren't.
#Find a compromise to organise things somewhat sensibly..
#this is really enableAnsi *processing*
proc [namespace parent]::enableAnsi {} {
#output handle modes
#Enable virtual terminal processing (sometimes off in older windows terminals)
@ -220,12 +259,13 @@ namespace eval punk::console {
#DISABLE_NEWLINE_AUTO_RETURN = 0x0008
set h_out [twapi::get_console_handle stdout]
set oldmode_out [twapi::GetConsoleMode $h_out]
set newmode_out [expr {$oldmode_out | 5}] ;#5?
set newmode_out [expr {$oldmode_out | 4}] ;#don't enable processed output too, even though it's required. keep symmetrical with disableAnsi?
twapi::SetConsoleMode $h_out $newmode_out
#what does window_input have to do with it??
#input handle modes
#ENABLE_PROCESSED_INPUT 0x0001
#ENABLE_PROCESSED_INPUT 0x0001 ;#set to zero will allow ctrl-c to be reported as keyboard input rather than as a signal
#ENABLE_LINE_INPUT 0x0002
#ENABLE_ECHO_INPUT 0x0004
#ENABLE_WINDOW_INPUT 0x0008 (default off when a terminal created)
@ -245,10 +285,10 @@ namespace eval punk::console {
proc [namespace parent]::disableAnsi {} {
set h_out [twapi::get_console_handle stdout]
set oldmode_out [twapi::GetConsoleMode $h_out]
set newmode_out [expr {$oldmode_out & ~5}]
set newmode_out [expr {$oldmode_out & ~4}]
twapi::SetConsoleMode $h_out $newmode_out
#??? review
set h_in [twapi::get_console_handle stdin]
set oldmode_in [twapi::GetConsoleMode $h_in]
set newmode_in [expr {$oldmode_in & ~8}]
@ -372,6 +412,12 @@ namespace eval punk::console {
}
proc [namespace parent]::disableVirtualTerminal {{channels {input output}}} {
}
proc [namespace parent]::enableProcessedInput {args} {
}
proc [namespace parent]::disableProcessedInput {args} {
}
}
@ -442,39 +488,200 @@ namespace eval punk::console {
}
#review - 1 byte at a time seems inefficient... but we don't have a way to peek or put back chars (?)
#todo - timeout - what if terminal doesn't put data on stdin?
#review - what if we slurp in data meant for main loop? Main loop probably needs to detect these responses and store them for lookup *instead* of this handler
#capturingendregex should capture ANY prefix, whole escape match - and a subcapture of the data we're interested in and match at end of string.
#ie {(.*)(ESC(info)end)$}
#e.g {(.*)(\x1bP44!~([:alnum:])\x1b\\)$}
#we expect 4 results from regexp -indices -inline (overallmatch, prefix, wholeescape,info)
#todo - check capturingendregex value supplied has appropriate captures and tail-anchor
proc get_ansi_response_payload {query capturingendregex {inoutchannels {stdin stdout}}} {
lassign $inoutchannels input output
#chunks from input that need to be handled by readers
upvar ::punk::console::input_chunks_waiting input_chunks_waiting
#we need to cooperate with other stdin/$input readers and put data here if we overconsume.
#Main repl reader may be currently active - or may be inactive.
#This call could come from within code called by the main reader - or from user code running while main read-loop is temporarily disabled
#In other contexts there may not even be another input reader
#REVIEW - what if there is existing data in input_chunks_waiting - is it for us?
#temp - let's keep alert to it until we decide if it's legit/required..
if {[info exists input_chunks_waiting($input)] && [llength $input_chunks_waiting($input)]} {
#puts stderr "[punk::ansi::a+ cyan bold]get_ansi_response_payload called while input_chunks_waiting($input) contained data: $input_chunks_waiting($input)[punk::ansi::a]"
}
if {!$::punk::console::ansi_available} {
return ""
}
set callid [info cmdcount] ;#info cmdcount is almost as fast as clock clicks - and whilst not unique in a long-running app(will wrap?) - fine for this context
#
upvar ::punk::console::ansi_response_chunk accumulator
upvar ::punk::console::ansi_response_wait waitvar
set accumulator($callid) ""
set waitvar($callid) ""
#todo - use a linked array and an accumulatorid and waitvar id? When can there be more than one terminal query in-flight?
set existing_handler [fileevent $input readable] ;#review!
fileevent $input readable {}
set input_state [fconfigure $input]
#todo - test and save rawstate so we don't disableRaw if console was already raw
if {!$::punk::console::is_raw} {
set was_raw 0
punk::console::enableRaw
} else {
set was_raw 1
}
fconfigure $input -blocking 0
#
set this_handler ::punk::console::internal::ansi_response_handler_regex
if {[lindex $existing_handler 0] eq $this_handler} {
puts stderr "[punk::ansi::a+ red]Warning get_ansi_response_payload called while existing ansi response handler in place[a]: $this_handler"
}
#in handler - its used for a boolean match (capturing aspect not used)
fileevent $input readable [list $this_handler $input $callid $capturingendregex]
# - stderr vs stdout
#It has to be same channel as used by functions such as test_char_width or erroneous results returned for those functions
#(presumably race conditions as to when data hits console?)
#review - experiment changing this and calling functions to stderr and see if it works
#review - Are there disadvantages to using stdout vs stderr?
#puts stdout "sending console request [ansistring VIEW $query]"
puts -nonewline $output $query;flush $output
#response from terminal
#e.g for cursor position \033\[46;1R
#todo - make timeout configurable?
set waitvarname "::punk::console::ansi_response_wait($callid)"
set cancel_timeout_id [after 500 [list set $waitvarname timedout]]
if {[set waitvar($callid)] eq ""} {
vwait ::punk::console::ansi_response_wait($callid)
}
#response handler automatically removes it's own fileevent
fileevent $input readable {} ;#explicit remove anyway - review
if {$waitvar($callid) ne "timedout"} {
after cancel $cancel_timeout_id
} else {
puts stderr "timeout in get_ansi_response_payload"
}
if {$was_raw == 0} {
punk::console::disableRaw
}
#restore $input state
fconfigure $input -blocking [dict get $input_state -blocking]
set response [set accumulator($callid)]
if {$response ne ""} {
set got_match [regexp -indices $capturingendregex $response _match_indices prefix_indices response_indices payload_indices]
if {$got_match} {
set responsedata [string range $response {*}$response_indices]
set payload [string range $response {*}$payload_indices]
set prefixdata [string range $response {*}$prefix_indices]
if {$prefixdata ne ""} {
#puts stderr "Warning - get_ansi_response_payload read extra data at start - '[ansistring VIEW -lf 1 $prefixdata]' (response=[ansistring VIEW -lf 1 $responsedata])"
lappend input_chunks_waiting($input) $prefixdata
}
} else {
#timedout - or eof?
puts stderr "get_ansi_response_payload regex match '$capturingendregex' to data '[ansistring VIEW $response]' not found"
lappend input_chunks_waiting($input) $response
set payload ""
}
} else {
#timedout or eof? and nothing read
set payload ""
}
#is there a way to know if existing_handler is input_chunks_waiting aware?
if {[string length $existing_handler] && [lindex $existing_handler 0] ne $this_handler} {
#puts "get_ansi_response_paylaod reinstalling ------>$existing_handler<------"
fileevent $input readable $existing_handler
#we may have consumed all pending input on $input - so there may be no trigger for the readable fileevent
if {[llength $input_chunks_waiting($input)]} {
#This is experimental If a handler is aware of input_chunks_waiting - there should be no need to schedule a trigger
#If it isn't, but the handler can accept an existing chunk of data as an argument - we could trigger and pass it the waiting chunks - but there's no way to know its API.
#we could look at info args - but that's not likely to tell us much in a robust way.
#we could create a reflected channel for stdin? That is potentially an overreach..?
#triggering it manually... as it was already listening - this should generally do no harm as it was the active reader anyway, but won't help with the missing data if it's input_chunks_waiting-unaware.
puts stderr "[punk::ansi::a+ yellow bold]-->punk::console::get_ansi_response_payload triggering existing handler while over-read data is in punk::console::input_chunks_waiting($input) instead of channel [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
after idle [list after 0 $existing_handler]
}
#Note - we still may be in_repl_handler here (which disables its own reader while executing commandlines)
#The input_chunks_waiting may really belong to the existing_handler we found - but if it doesn't consume them they will end up being read by the repl_handler when it eventually re-enables.
#todo - some better structure than just a list of waiting chunks indexed by input channel, so repl/other handlers can determine the context in which these waiting chunks were generated?
} elseif {[llength $::repl::in_repl_handler]} {
if {[llength $input_chunks_waiting($input)]} {
#don't trigger the repl handler manually - we will inevitably get things out of order - as it knows when to enable/disable itself based on whether chunks are waiting.
#triggering it by putting it on the eventloop will potentially result in re-entrancy
#The cooperating reader must be designed to consume waiting chunks and only reschedule it's channel read handler once all waiting chunks have been consumed.
#puts stderr "[punk::ansi::a+ green bold]--> repl_handler has chunks to consume [ansistring VIEW $input_chunks_waiting($input)][punk::ansi::a]"
}
}
catch {
unset accumulator($callid)
unset waitvar($callid)
}
#set punk::console::chunk ""
return $payload
}
#review - reading 1 byte at a time and repeatedly running the small capturing/completion regex seems a little inefficient... but we don't have a way to peek or put back chars (?)
#review (we do have the punk::console::input_chunks_waiting($chan) array to cooperatively put back data - but this won't work for user scripts not aware of this)
#review - timeout - what if terminal doesn't put data on stdin? error vs stderr report vs empty results
#review - Main loop may need to detect some terminal responses and store them for lookup instead-of or as-well-as this handler?
#e.g what happens to mouse-events while user code is executing?
#we may still need this handler if such a loop doesn't exist.
proc ansi_response_handler {chan accumulatorvar waitvar} {
proc ansi_response_handler_regex {chan callid endregex} {
upvar ::punk::console::ansi_response_chunk chunks
upvar ::punk::console::ansi_response_wait waits
#endregex should explicitly have a trailing $
set status [catch {read $chan 1} bytes]
if { $status != 0 } {
# Error on the channel
fileevent stdin readable {}
puts "error reading $chan: $bytes"
set $waitvar [list error_read status $status bytes $bytes]
fileevent $chan readable {}
puts "ansi_response_handler_regex error reading $chan: $bytes"
set waits($callid) [list error_read status $status bytes $bytes]
} elseif {$bytes ne ""} {
# Successfully read the channel
#puts "got: [string length $bytes]"
upvar $accumulatorvar chunk
append chunk $bytes
if {$bytes eq "R"} {
fileevent stdin readable {}
set $waitvar ok
#puts "got: [string length $bytes]bytes"
append chunks($callid) $bytes
#puts stderr [ansistring VIEW $chunks($callid)]
if {[regexp $endregex $chunks($callid)]} {
fileevent $chan readable {}
#puts stderr "matched - setting ansi_response_wait($callid) ok"
set waits($callid) ok
}
} elseif { [eof $chan] } {
fileevent stdin readable {}
} elseif {[eof $chan]} {
fileevent $chan readable {}
# End of file on the channel
#review
puts "ansi_response_handler end of file"
set $waitvar eof
} elseif { [fblocked $chan] } {
puts stderr "ansi_response_handler_regex end of file on channel $chan"
set waits($callid) eof
} elseif {[fblocked $chan]} {
# Read blocked. Just return
# Caller should be using timeout on the wait variable
} else {
fileevent stdin readable {}
fileevent $chan readable {}
# Something else
puts "ansi_response_handler can't happen"
set $waitvar error_unknown
puts stderr "ansi_response_handler_regex Situation shouldn't be possible. No error and no bytes read on channel $chan but chan is not fblocked or EOF"
set waits($callid) error_unknown_zerobytes_while_not_blocked_or_eof
}
}
} ;#end namespace eval internal
@ -487,67 +694,99 @@ namespace eval punk::console {
}
}
namespace eval ansi {
proc a+ {args} {
puts -nonewline [::punk::ansi::a+ {*}$args]
}
}
proc ansi+ {args} {
variable colour_disabled
if {$colour_disabled == 1} {
return
}
#a and a+ functions are not very useful when emitting directly to console
#e.g puts [punk::console::a red]test[punk::console::a cyan] would produce a cyan coloured test as the commands are evaluated first
#proc a {args} {
# variable colour_disabled
# variable ansi_wanted
# if {$colour_disabled || $ansi_wanted <= 0} {
# return
# }
# #stdout
# tailcall ansi::a {*}$args
#}
#proc a+ {args} {
# variable colour_disabled
# variable ansi_wanted
# if {$colour_disabled || $ansi_wanted <= 0} {
# return
# }
# #stdout
# tailcall ansi::a+ {*}$args
#}
proc a? {args} {
#stdout
tailcall ansi::a+ {*}$args
}
proc get_ansi+ {args} {
variable colour_disabled
if {$colour_disabled == 1} {
return
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
puts -nonewline [punk::ansi::stripansi [::punk::ansi::a?]]
} else {
tailcall ansi::a? {*}$args
}
tailcall punk::ansi::a+ {*}$args
}
namespace eval ansi {
proc a {args} {
puts -nonewline [::punk::ansi::a {*}$args]
proc code_a {args} {
variable colour_disabled
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
return
}
tailcall punk::ansi::a {*}$args
}
proc ansi {args} {
proc code_a? {args} {
variable colour_disabled
if {$colour_disabled == 1} {
return
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
return [punk::ansi::stripansi [::punk::ansi::a? {*}$args]]
} else {
tailcall ::punk::ansi::a? {*}$args
}
#stdout
tailcall ansi::a {*}$args
}
proc get_ansi {args} {
proc code_a+ {args} {
variable colour_disabled
if {$colour_disabled == 1} {
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
return
}
tailcall punk::ansi::a {*}$args
tailcall punk::ansi::a+ {*}$args
}
namespace eval ansi {
proc a? {args} {
puts -nonewline stdout [::punk::ansi::a? {*}$args]
proc ansi {{onoff {}}} {
variable ansi_wanted
if {[string length $onoff]} {
set onoff [string tolower $onoff]
switch -- $onoff {
1 -
on -
true -
yes {
set ansi_wanted 1
}
0 -
off -
false -
no {
set ansi_wanted 0
}
default {
set ansi_wanted 2
}
default {
error "punk::console::ansi expected 0|1|on|off|true|false|yes|no|default"
}
}
}
catch {repl::reset_prompt}
return [expr {$ansi_wanted}]
}
proc ansi? {args} {
#stdout
tailcall ansi::a? {*}$args
}
proc get_ansi? {args} {
tailcall ::punk::ansi::a? {*}$args
}
proc colour {{onoff {}}} {
variable colour_disabled
if {[string length $onoff]} {
set onoff [string tolower $onoff]
#an experiment with complete disabling vs test of state for each call
if {$onoff in [list 1 on true yes]} {
interp alias "" a+ "" punk::console::ansi+
interp alias "" a+ "" punk::console::code_a+
set colour_disabled 0
} elseif {$onoff in [list 0 off false no]} {
interp alias "" a+ "" control::no-op
@ -560,14 +799,17 @@ namespace eval punk::console {
return [expr {!$colour_disabled}]
}
namespace eval ansi {
proc reset {} {
puts -nonewline stdout [punk::ansi::reset]
}
}
namespace import ansi::reset
namespace eval ansi {
proc a {args} {
puts -nonewline [::punk::ansi::a {*}$args]
}
proc a? {args} {
puts -nonewline stdout [::punk::ansi::a? {*}$args]
}
proc a+ {args} {
puts -nonewline [::punk::ansi::a+ {*}$args]
}
proc clear {} {
puts -nonewline stdout [punk::ansi::clear]
}
@ -580,11 +822,15 @@ namespace eval punk::console {
proc clear_all {} {
puts -nonewline stdout [punk::ansi::clear_all]
}
proc reset {} {
puts -nonewline stdout [punk::ansi::reset]
}
}
namespace import ansi::clear
namespace import ansi::clear_above
namespace import ansi::clear_below
namespace import ansi::clear_all
namespace import ansi::reset
namespace eval local {
proc set_codepage_output {cpname} {
@ -607,91 +853,80 @@ namespace eval punk::console {
namespace import local::set_codepage_output
namespace import local::set_codepage_input
proc get_cursor_pos {} {
set ::punk::console::chunk ""
set accumulator ::punk::console::chunk
set waitvar ::punk::console::chunkdone
set existing_handler [fileevent stdin readable] ;#review!
set $waitvar ""
set stdin_state [fconfigure stdin]
#todo - only use own handler if an existing stdin handler not present.. (or console is in line mode)
#todo - test and save rawstate so we don't disableRaw if console was already raw
if {!$::punk::console::is_raw} {
set was_raw 0
enableRaw
} else {
set was_raw 1
}
fconfigure stdin -blocking 0
#
fileevent stdin readable [list ::punk::console::internal::ansi_response_handler stdin $accumulator $waitvar]
# - stderr vs stdout
#It has to be same channel as used by functions such as test_char_width or erroneous results returned for those functions
#(presumably race conditions as to when data hits console?)
#review - experiment changing this and calling functions to stderr and see if it works
#review - Are there disadvantages to using stdout vs stderr?
puts -nonewline stdout \033\[6n ;flush stdout
# -- --- --- --- --- --- ---
#get_ansi_response functions
#review - can these functions sensibly be used on channels not attached to the local console?
#ie can we default to {stdin stdout} but allow other channel pairs?
# -- --- --- --- --- --- ---
proc get_cursor_pos {{inoutchannels {stdin stdout}}} {
#response from terminal
#e.g \033\[46;1R
set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload
#todo - make timeout configurable?
set cancel_timeout_id [after 2000 [list set $waitvar timedout]]
after 0 {update idletasks}
set info ""
if {[set $waitvar] eq ""} {
vwait $waitvar
}
if {$waitvar ne "timedout"} {
after cancel $cancel_timeout_id
} else {
return ""
}
if {$was_raw == 0} {
disableRaw
}
#restore stdin state
fconfigure stdin -blocking [dict get $stdin_state -blocking]
if {[string length $existing_handler]} {
fileevent stdin readable $existing_handler
}
#response handler automatically removes it's own fileevent
set info [set $accumulator]
set start [string first \x1b $info]
if {$start > 0} {
set other [string range $info 0 $start-1]
#!!!!! TODO
# Log this somewhere? Work out how to stop it happening?
#puts stderr "Warning - get_cursor_pos read extra data at start - '$other'"
set info [string range $info $start end]
}
set request "\033\[6n"
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
return $payload
}
proc get_checksum_rect {id page t l b r {inoutchannels {stdin stdout}}} {
#e.g \x1b\[P44!~E797\x1b\\
#re e.g {(.*)(\x1b\[P44!~([[:alnum:]])\x1b\[\\)$}
set capturingregex [string map [list %id% $id] {(.*)(\x1bP%id%!~([[:alnum:]]+)\x1b\\)$}]
set request "\x1b\[${id}\;${page}\;$t\;$l\;$b\;$r*y"
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
return $payload
}
proc get_device_status {{inoutchannels {stdin stdout}}} {
set capturingregex {(.*)(\x1b\[([0-9]+)n)$} ;#must capture prefix,entire-response,response-payload
set request "\x1b\[5n"
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
return $payload
}
#set punk::console::chunk ""
set data [string range $info 2 end-1]
return $data
}
proc get_cursor_pos_list {} {
return [split [get_cursor_pos] ";"]
}
proc get_size {} {
if {[catch {
puts -nonewline [punk::ansi::cursor_off][punk::ansi::cursor_save][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list] lines cols
puts -nonewline [punk::ansi::cursor_restore][punk::console::cursor_on];flush stdout
set result [list columns $cols rows $lines]
} errM]} {
puts -nonewline [punk::ansi::cursor_restore]
puts -nonewline [punk::ansi::cursor_on]
error "$errM"
} else {
return $result
}
}
proc get_dimensions {} {
lassign [get_size] _c cols _l lines
return "${cols}x${lines}"
}
#the (xterm?) CSI 18t query is supported by *some* terminals
proc get_xterm_size {{inoutchannels {stdin stdout}}} {
set capturingregex {(.*)(\x1b\[8;([0-9]+;[0-9]+)t)$} ;#must capture prefix,entire-response,response-payload
set request "\x1b\[18t"
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
lassign [split $payload {;}] rows cols
return [list columns $cols rows $rows]
}
#terminals lie. This should be a reasonable (albeit relatively slow) test of actual width - but some terminals seem to miscalculate.
#todo - a visual interactive test/questionnaire to ask user if things are lining up or if the terminal is telling fibs about cursor position.
#todo - determine if these anomalies are independent of font
#punk::ansi should be able to glean widths from unicode data files - but this may be incomplete - todo - compare with what terminal actually does.
proc test_char_width {char_or_string {emit 0}} {
#return 1
#JMN
#puts stderr "cwtest"
variable ansi_available
if {!$ansi_available} {
puts stderr "No ansi - cannot test char_width of '$char_or_string' returning [string length $char_or_string]"
return [string length $char_or_string]
}
if {!$emit} {
puts -nonewline stdout \033\[2K\033\[1G ;#2K erase line 1G cursor at col1
}
@ -725,6 +960,50 @@ namespace eval punk::console {
return [expr {$col2 - $col1}]
}
#todo! - improve
proc test_can_ansi {} {
#don't set ansi_avaliable here - we want to be able to change things, retest etc.
if {"windows" eq "$::tcl_platform(platform)"} {
if {[package provide twapi] ne ""} {
set h_out [twapi::get_console_handle stdout]
set existing_mode [twapi::GetConsoleMode $h_out]
if {[expr {$existing_mode & 4}]} {
#virtual terminal processing happens to be enabled - so it's supported
return 1
}
#output mode
#ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
#try temporarily setting it - if we get an error - ansi not supported
if {[catch {
twapi::SetConsoleMode $h_out [expr {$existing_mode | 4}]
} errM]} {
return 0
}
#restore
twapi::SetConsoleMode $h_out [expr {$existing_mode & ~4}]
return 1
} else {
#todo - try a cursorpos query and read stdin to see if we got a response?
puts stderr "Unable to verify terminal ansi support - assuming modern default of true"
puts stderr "to force disable, use command: ansi off"
return 1
}
} else {
return 1
}
}
#review
proc can_ansi {} {
variable ansi_available
if {!$ansi_available} {
return 0
}
set ansi_available [test_can_ansi]
return [expr {$ansi_available}]
}
namespace eval ansi {
proc cursor_on {} {
puts -nonewline stdout [punk::ansi::cursor_on]
@ -768,7 +1047,15 @@ namespace eval punk::console {
puts -nonewline stdout [punk::ansi::titleset $windowtitle]
}
}
namespace import ansi::titleset
#namespace import ansi::titleset
proc titleset {windowtitle} {
variable ansi_wanted
if { $ansi_wanted <= 0} {
punk::console::local::titleset $windowtitle
} else {
tailcall ansi::titleset $windowtitle
}
}
#no known pure-ansi solution
proc titleget {} {
return [local::titleget]
@ -852,14 +1139,14 @@ namespace eval punk::console {
#review - worth the extra microseconds to inline? might be if used in for example prompt on every keypress.
#caller should build as much as possible using the punk::ansi versions to avoid extra puts calls
proc save_cursor {} {
proc cursor_save {} {
#*** !doctools
#[call [fun save_cursor]]
#[call [fun cursor_save]]
puts -nonewline \x1b\[s
}
proc restore_cursor {} {
proc cursor_restore {} {
#*** !doctools
#[call [fun restore_cursor]]
#[call [fun cursor_restore]]
puts -nonewline \x1b\[u
}
proc insert_spaces {count} {
@ -886,8 +1173,8 @@ namespace eval punk::console {
namespace import ansi::move_down
namespace import ansi::move_column
namespace import ansi::move_row
namespace import ansi::save_cursor
namespace import ansi::restore_cursor
namespace import ansi::cursor_save
namespace import ansi::cursor_restore
namespace import ansi::scroll_down
namespace import ansi::scroll_up
namespace import ansi::insert_spaces
@ -906,27 +1193,64 @@ namespace eval punk::console {
#set blanks [string repeat " " [expr {$col + $tw}]]
#puts -nonewline [punk::ansi::erase_eol]$blanks;move_emit_return this $col $text
#puts -nonewline [move_emit_return this $col [punk::ansi::insert_spaces 150]$text]
save_cursor
cursor_save
#move_emit_return this $col [punk::ansi::move_forward 50][punk::ansi::insert_spaces 150][punk::ansi::move_back 50][punk::ansi::move_forward $col]$text
puts -nonewline [punk::ansi::insert_spaces 150][punk::ansi::move_column $col]$text
restore_cursor
#puts -nonewline [punk::ansi::insert_spaces 150][punk::ansi::move_column $col]$text
puts -nonewline [punk::ansi::erase_eol][punk::ansi::move_column $col]$text
cursor_restore
}
proc move_emit_return {row col data args} {
#todo detect if in raw mode or not?
set is_in_raw 0
lassign [punk::console::get_cursor_pos_list] orig_row orig_col
move_emit $row $col $data
set commands ""
append commands [punk::ansi::move_emit $row $col $data]
foreach {row col data} $args {
move_emit $row $col $data
append commands [punk::ansi::move_emit $row $col $data]
}
if {!$is_in_raw} {
incr orig_row -1
}
move $orig_row $orig_col
append commands [punk::ansi::move $orig_row $orig_col]
puts -nonewline stdout $commands
return ""
}
#we can be faster and more efficient if we use the consoles cursor_save command - but each savecursor overrides any previous one.
#leave cursor_off/cursor_on to caller who can wrap more efficiently..
proc cursorsave_move_emit_return {row col data args} {
set commands ""
append commands [punk::ansi::cursor_save]
append commands [punk::ansi::move_emit $row $col $data]
foreach {row col data} $args {
append commands [punk::ansi::move_emit $row $col $data]
}
append commands [punk::ansi::cursor_restore]
puts -nonewline stdout $commands; flush stdout
}
proc move_emitblock_return {row col textblock} {
lassign [punk::console::get_cursor_pos_list] orig_row orig_col
set commands ""
foreach ln [split $textblock \n] {
append commands [punk::ansi::move_emit $row $col $ln]
incr row
}
append commands [punk::ansi::move $orig_row $orig_col]
puts -nonewline $commands
return
}
proc cursorsave_move_emitblock_return {row col textblock} {
set commands ""
append commands [punk::ansi::cursor_save]
foreach ln [split $textblock \n] {
append commands [punk::ansi::move_emit $row $col $ln]
incr row
}
append commands [punk::ansi::cursor_restore]
puts -nonewline stdout $commands;flush stdout
return
}
proc move_call_return {row col script} {
lassign [punk::console::get_cursor_pos_list] orig_row orig_col
move $row $col
@ -934,7 +1258,7 @@ namespace eval punk::console {
move $orig_row $orig_col
}
#this doesn't work - we would need an internal virtual screen structure to pick up cursor attributes from arbitrary locations
#this doesn't work - we would need an internal virtual screen structure to pick up cursor attributes from arbitrary locations?
# ncurses and its ilk may have something like that - but we specifically want to avoid curses libraries
proc pick {row col} {
lassign [punk::console::get_cursor_pos_list] orig_row orig_col

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

@ -50,9 +50,9 @@
#[para] packages used by punk::encmime
#[list_begin itemized]
package require Tcl 8.6
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools

64
src/bootsupport/modules/punk/fileline-0.1.0.tm

@ -60,10 +60,10 @@
#[para] packages needed by punk::fileline
#[list_begin itemized]
package require Tcl 8.6
package require Tcl 8.6-
package require punk::args
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {Tcl 8.6-}]
#[item] [package {punk::args}]
@ -368,6 +368,7 @@ namespace eval punk::fileline::class {
} else {
set tail [string trimleft $opt_linebase +];#ignore +
}
#todo - switch -glob -- $tail
if {[string match eof* $tail]} {
set endmath [string range $tail 3 end]
#todo endmath?
@ -1066,32 +1067,37 @@ namespace eval punk::fileline::class {
foreach whichvar [list start end] {
upvar 0 ${whichvar}idx index
if {![string is digit -strict $index]} {
if {"end" eq $index} {
set index $max
} elseif {[string match "*-*" $index]} {
#end-int or int-int - like lrange etc we don't accept arbitrarily complex expressions
lassign [split $index -] A B
if {$A eq "end"} {
set index [expr {$max - $B}]
} else {
set index [expr {$A - $B}]
switch -glob -- $index {
end {
set index $max
}
} elseif {[string match "*+*" $index]} {
lassign [split $index +] A B
if {$A eq "end"} {
#review - this will just result in out of bounds error in final test - as desired
#By calculating here - we will see the result in the error message - but it's probably not particularly useful - as we don't really need end+ support at all.
set index [expr {$max + $B}]
} else {
set index [expr {$A + $B}]
"*-*" {
#end-int or int-int - like lrange etc we don't accept arbitrarily complex expressions
lassign [split $index -] A B
if {$A eq "end"} {
set index [expr {$max - $B}]
} else {
set index [expr {$A - $B}]
}
}
} else {
#May be something like +2 or -0 which braced expr can hanle
#we would like to avoid unbraced expr here - as we're potentially dealing with ranges that may come from external sources.
if {[catch {expr {$index}} index]} {
#could be end+x - but we don't want out of bounds to be valid
#set it to something that the final bounds expr test can deal with
set index Inf
"*+*" {
lassign [split $index +] A B
if {$A eq "end"} {
#review - this will just result in out of bounds error in final test - as desired
#By calculating here - we will see the result in the error message - but it's probably not particularly useful - as we don't really need end+ support at all.
set index [expr {$max + $B}]
} else {
set index [expr {$A + $B}]
}
}
default {
#May be something like +2 or -0 which braced expr can hanle
#we would like to avoid unbraced expr here - as we're potentially dealing with ranges that may come from external sources.
if {[catch {expr {$index}} index]} {
#could be end+x - but we don't want out of bounds to be valid
#set it to something that the final bounds expr test can deal with
set index Inf
}
}
}
}
@ -1308,6 +1314,7 @@ namespace eval punk::fileline {
set bomenc ""
set is_reliabletxt 0 ;#see http://reliabletxt.com - only utf-8 with bom, utf-16be, utf-16le, utf-32be supported as at 2024
set startdata 0
#todo switch -glob
if {[string match "efbbbf*" $maybe_bom]} {
set bomid utf-8
set bomenc utf-8
@ -1424,6 +1431,7 @@ namespace eval punk::fileline {
set encoding_selected $bomenc
}
} else {
#!?
if {$bomenc eq "binary"} {
set datachunk [string range $rawchunk $startdata end]
set encoding_selected binary
@ -1523,7 +1531,7 @@ namespace eval punk::fileline::lib {
# is_span 1 boundaries {514 1026 1538}
#[example_end]
#[para] This function automatically uses lseq (if Tcl >= 8.7) when number of boundaries spanned is approximately greater than 75
if {[catch {package require Tcl 8.7}]} {
if {[catch {package require Tcl 8.7-}]} {
#only one implementation available for older Tcl
tailcall punk::fileline::system::_range_spans_chunk_boundaries_tcl $start $end $chunksize {*}$args
}
@ -1675,7 +1683,7 @@ namespace eval punk::fileline::system {
proc _range_spans_chunk_boundaries_TIMEIT {start end chunksize {repeat 1}} {
puts "main : [time {punk::fileline::lib::range_spans_chunk_boundaries $start $end $chunksize} $repeat]"
puts "tcl : [time {punk::fileline::system::_range_spans_chunk_boundaries_tcl $start $end $chunksize} $repeat]"
if {![catch {package require Tcl 8.7}]} {
if {![catch {package require Tcl 8.7-}]} {
puts "lseq : [time {punk::fileline::system::_range_spans_chunk_boundaries_lseq $start $end $chunksize} $repeat]"
}
}

1847
src/bootsupport/modules/punk/lib-0.1.1.tm

File diff suppressed because it is too large Load Diff

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

@ -494,9 +494,13 @@ namespace eval punk::mix::base {
if {[catch {file type $path} ftype]} {
return [list cksum "<PATHNOTFOUND>" opts $opts]
}
if {$ftype ni [list file directory]} {
#review - links?
error "cksum_path error file type '$ftype' not supported"
#review - links?
switch -- $ftype {
file - directory {}
default {
error "cksum_path error file type '$ftype' not supported"
}
}
@ -512,54 +516,65 @@ namespace eval punk::mix::base {
set opt_cksum_meta [dict get $opts -cksum_meta]
set opt_use_tar [dict get $opts -cksum_usetar]
if {$ftype eq "file"} {
if {$opt_use_tar eq "auto"} {
if {$opt_cksum_meta eq "1"} {
set opt_use_tar 1
} else {
#prefer no tar if meta not required - faster/simpler
#meta == auto or 0
set opt_cksum_meta 0
set opt_use_tar 0
}
} elseif {$opt_use_tar eq "0"} {
if {$opt_cksum_meta eq "1"} {
puts stderr "cksum_path doesn't yet support a non-tar cksum with metadata for a file"
return [list error unsupported_meta_without_tar cksum "<ERR>" opts $opts]
} else {
#meta == auto or 0
set opt_cksum_meta 0
}
} else {
#tar == 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar cksum without metadata for a file"
return [list error unsupported_tar_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
switch -- $ftype {
file {
switch -- $opt_use_tar {
auto {
if {$opt_cksum_meta eq "1"} {
set opt_use_tar 1
} else {
#prefer no tar if meta not required - faster/simpler
#meta == auto or 0
set opt_cksum_meta 0
set opt_use_tar 0
}
}
0 {
if {$opt_cksum_meta eq "1"} {
puts stderr "cksum_path doesn't yet support a non-tar cksum with metadata for a file"
return [list error unsupported_meta_without_tar cksum "<ERR>" opts $opts]
} else {
#meta == auto or 0
set opt_cksum_meta 0
}
}
default {
#tar == 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar cksum without metadata for a file"
return [list error unsupported_tar_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
}
}
}
}
} elseif {$ftype eq "directory"} {
if {$opt_use_tar eq "auto"} {
if {$opt_cksum_meta in [list "auto" "1"]} {
set opt_use_tar 1
set opt_cksum_meta 1
} else {
puts stderr "cksum_path doesn't yet support a content-only cksum of a folder structure. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto"
return [list error unsupported_directory_cksum_without_meta cksum "<ERR>" opts $opts]
}
} elseif {$opt_use_tar eq "0"} {
puts stderr "cksum_path doesn't yet support a cksum of a folder structure without tar. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_directory_cksum_without_tar cksum "<ERR>" opts $opts]
} else {
#tar 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar checksum of a folder structure without metadat. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
directory {
switch -- $opt_use_tar {
auto {
if {$opt_cksum_meta in [list "auto" "1"]} {
set opt_use_tar 1
set opt_cksum_meta 1
} else {
puts stderr "cksum_path doesn't yet support a content-only cksum of a folder structure. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto"
return [list error unsupported_directory_cksum_without_meta cksum "<ERR>" opts $opts]
}
}
0 {
puts stderr "cksum_path doesn't yet support a cksum of a folder structure without tar. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_directory_cksum_without_tar cksum "<ERR>" opts $opts]
}
default {
#tar 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar checksum of a folder structure without metadat. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
}
}
}
}
}
@ -578,29 +593,36 @@ namespace eval punk::mix::base {
puts stderr "cksum_path doesn't yet support cksum of entire volume. (todo)"
return [list error unsupported_path opts $opts]
}
if {$opt_cksum_algorithm eq "sha1"} {
package require sha1
set cksum_command [list sha1::sha1 -hex -file]
} elseif {$opt_cksum_algorithm in [list "sha2" "sha256"]} {
package require sha256
set cksum_command [list sha2::sha256 -hex -file]
} elseif {$opt_cksum_algorithm eq "md5"} {
package require md5
set cksum_command [list md5::md5 -hex -file]
} elseif {$opt_cksum_algorithm eq "cksum"} {
package require cksum ;#tcllib
set cksum_command [list crc::cksum -format 0x%X -file]
} elseif {$opt_cksum_algorithm eq "adler32"} {
set cksum_command [list cksum_adler32_file]
} elseif {$opt_cksum_algorithm in [list "sha3" "sha3-256"]} {
#todo - replace with something that doesn't call another process
#set cksum_command [list apply {{file} {lindex [exec fossil sha3sum -256 $file] 0}}]
set cksum_command [list $sha3_implementation 256]
} elseif {$opt_cksum_algorithm in [list "sha3-224" "sha3-384" "sha3-512"]} {
set bits [lindex [split $opt_cksum_algorithm -] 1]
#set cksum_command [list apply {{bits file} {lindex [exec fossil sha3sum -$bits $file] 0}} $bits]
set cksum_command [list $sha3_implementation $bits]
switch -- $opt_cksum_algorithm {
sha1 {
package require sha1
set cksum_command [list sha1::sha1 -hex -file]
}
sha2 - sha256 {
package require sha256
set cksum_command [list sha2::sha256 -hex -file]
}
md5 {
package require md5
set cksum_command [list md5::md5 -hex -file]
}
cksum {
package require cksum ;#tcllib
set cksum_command [list crc::cksum -format 0x%X -file]
}
adler32 {
set cksum_command [list cksum_adler32_file]
}
sha3 - sha3-256 {
#todo - replace with something that doesn't call another process
#set cksum_command [list apply {{file} {lindex [exec fossil sha3sum -256 $file] 0}}]
set cksum_command [list $sha3_implementation 256]
}
sha3-224 - sha3-384 - sah3-512 {
set bits [lindex [split $opt_cksum_algorithm -] 1]
#set cksum_command [list apply {{bits file} {lindex [exec fossil sha3sum -$bits $file] 0}} $bits]
set cksum_command [list $sha3_implementation $bits]
}
}
set cksum ""

8
src/bootsupport/modules/punk/mix/commandset/layout-0.1.0.tm

@ -114,11 +114,11 @@ namespace eval punk::mix::commandset::layout {
set tablewidth [expr {$widest(name) + $vsep_w + $widest(pathtype) + $vsep_w + $widest(path)}]
set table ""
append table [string repeat - $tablewidth] \n
append table "[textblock::join [overtype::left $col(name) $title(name)] $vsep [overtype::left $col(pathtype) $title(pathtype)] $vsep [overtype::left $col(path) $title(path)]]" \n
append table "[textblock::join -- [overtype::left $col(name) $title(name)] $vsep [overtype::left $col(pathtype) $title(pathtype)] $vsep [overtype::left $col(path) $title(path)]]" \n
append table [string repeat - $tablewidth] \n
foreach n $names pt $pathtypes p $paths {
append table "[textblock::join [overtype::left $col(name) $n] $vsep [overtype::left $col(pathtype) $pt] $vsep [overtype::left $col(path) $p]]" \n
append table "[textblock::join -- [overtype::left $col(name) $n] $vsep [overtype::left $col(pathtype) $pt] $vsep [overtype::left $col(path) $p]]" \n
}
return $table
@ -161,11 +161,11 @@ namespace eval punk::mix::commandset::layout {
set tablewidth [expr {$widest(name) + $vsep_w + $widest(pathtype) + $vsep_w + $widest(path)}]
set table ""
append table [string repeat - $tablewidth] \n
append table "[textblock::join [overtype::left $col(name) $title(name)] $vsep [overtype::left $col(pathtype) $title(pathtype)] $vsep [overtype::left $col(path) $title(path)]]" \n
append table "[textblock::join -- [overtype::left $col(name) $title(name)] $vsep [overtype::left $col(pathtype) $title(pathtype)] $vsep [overtype::left $col(path) $title(path)]]" \n
append table [string repeat - $tablewidth] \n
foreach n $names pt $pathtypes p $paths {
append table "[textblock::join [overtype::left $col(name) $n] $vsep [overtype::left $col(pathtype) $pt] $vsep [overtype::left $col(path) $p]]" \n
append table "[textblock::join -- [overtype::left $col(name) $n] $vsep [overtype::left $col(pathtype) $pt] $vsep [overtype::left $col(path) $p]]" \n
}
return $table

4
src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm

@ -59,9 +59,9 @@
#[para] packages used by punk::mix::commandset::project
#[list_begin itemized]
package require Tcl 8.6
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {Tcl 8.6-}]
#[item] [package punk::ns]
#[item] [package sqlite3] (binary)
#[item] [package overtype]

67
src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm

@ -209,9 +209,13 @@ namespace eval punk::mix::commandset::scriptwrap {
set callposn -1
set trimln [string trim $callingline_payload]
if {[string match "rem *" $trimln] || [string match "@rem *" $trimln] || [string match "REM *" $trimln] || [string match "@REM *" $trimln]} {
#ignore things that look like a call that are beind a REM
} else {
#if {[string match "rem *" $trimln] || [string match "@rem *" $trimln] || [string match "REM *" $trimln] || [string match "@REM *" $trimln]} {}
#ignore things that look like a call that are beind a REM
switch -glob -nocase -- $trimln {
"rem *" -
"@rem *" {}
default {
#todo - better callsite analysis. There can be data between @GOTO or @CALL and : other than just whitespace!
@ -695,7 +699,8 @@ namespace eval punk::mix::commandset::scriptwrap {
}
}
}
}
} ;# end default switch case on trimln
} ;# end switch
incr file_offset $callingline_len ;#including per-line stored line-ending
}
if {[dict size $possible_target_labels_found] > 0} {
@ -1381,34 +1386,38 @@ namespace eval punk::mix::commandset::scriptwrap {
set inputconsumed 0
foreach c $inputchars {
if {!$invar} {
if {$c eq "%"} {
set caretseq 0
set lookahead [lrange $inputchars $inputconsumed+1 end]
if {"%" in $lookahead} {
set invar 1
incr percentrun
} else {
incr percentrun
}
} elseif {$c eq "^"} {
if {$caretseq} {
switch -- $c {
"%" {
set caretseq 0
append labelout "^" ;#for every pair encountered in direct sequence - second gets included in label
} else {
set caretseq 1
set lookahead [lrange $inputchars $inputconsumed+1 end]
if {"%" in $lookahead} {
set invar 1
incr percentrun
} else {
incr percentrun
}
}
} else {
set caretseq 0
if {$percentrun && ($c in [list 0 1 2 3 4 5 6 7 8 9])} {
#subst %i with value - here we have no way of getting that - so use blank - user will get warning that target not found
set percentrun 0
} else {
append labelout [string repeat % [expr {$percentrun / 2}]]
set percentrun 0
if {$c in $labelterminals} {
break
"^" {
if {$caretseq} {
set caretseq 0
append labelout "^" ;#for every pair encountered in direct sequence - second gets included in label
} else {
set caretseq 1
}
}
default {
set caretseq 0
if {$percentrun && ($c in [list 0 1 2 3 4 5 6 7 8 9])} {
#subst %i with value - here we have no way of getting that - so use blank - user will get warning that target not found
set percentrun 0
} else {
append labelout [string repeat % [expr {$percentrun / 2}]]
set percentrun 0
if {$c in $labelterminals} {
break
}
append labelout $c
}
append labelout $c
}
}
} else {

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

@ -766,8 +766,9 @@ namespace eval punk::ns {
set e [a+ yellow bold]
set o [a+ cyan bold]
set p [a+ white bold]
set a1 [a][a+ cyan]
foreach ch1 $children1 ch2 $children2 cmd1 $elements1 cmd2 $elements2 cmd3 $elements3 cmd4 $elements4 {
set a1 [a+ cyan]
set c1 [a+ white]
set c2 [a+ white]
set c3 [a+ white]
@ -1355,8 +1356,9 @@ namespace eval punk::ns {
proc corp {path} {
#thanks to Richard Suchenwirth for the original - wiki.tcl-lang.org/page/corp
#Note: modified here to support aliases and relative/absolute name (with respect to namespace .ie ::name vs name)
set indent " " ;#review
if {[info exists ::auto_index($path)]} {
set body "# $::auto_index($path)\n"
set body "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set body ""
}
@ -1404,10 +1406,20 @@ namespace eval punk::ns {
}
}
if {[nsprefix $origin] ne [nsprefix [nsjoin ${targetns} $name]]} {
append body "# namespace origin $origin" \n
append body \n "${indent}#corp# namespace origin $origin"
}
append body [info body $origin]
if {$body ne "" && [string index $body end] ne "\n"} {
append body \n
}
if {![catch {package require textutil::tabify} errpkg]} {
set bodytext [info body $origin]
#punk::lib::indent preserves trailing empty lines - unlike textutil version
set bodytext [punk::lib::undent [textutil::untabify2 $bodytext]]
append body [punk::lib::indent $bodytext $indent]
} else {
append body [info body $origin]
}
set argl {}
foreach a [info args $origin] {
if {[info default $origin $a def]} {

28
src/bootsupport/modules/punk/path-0.1.0.tm

@ -44,9 +44,9 @@
#[para] packages used by punk::path
#[list_begin itemized]
package require Tcl 8.6
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
@ -126,18 +126,18 @@ namespace eval punk::path {
if {[string range $seg end end] eq "/"} {
set seg [string range $seg 0 end-1] ;# e.g c:/ -> c: / -> "" so that join at end doesn't double up
}
if {$seg eq "*"} {
lappend pats {[^/]*}
} elseif {$seg eq "**"} {
lappend pats {.*}
} else {
set seg [string map [list {^ {\^} $ {\$} [} {\[} ( {\(} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters in the input as literals
set seg [string map [list . {[.]}] $seg]
if {[regexp {[*?]} $seg]} {
set pat [string map [list ** {.*} * {[^/]*} ? {[^/]}] $seg]
lappend pats "$pat"
} else {
lappend pats "$seg"
switch -- $seg {
* {lappend pats {[^/]*}}
** {lappend pats {.*}}
default {
set seg [string map [list {^ {\^} $ {\$} [} {\[} ( {\(} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters in the input as literals
set seg [string map [list . {[.]}] $seg]
if {[regexp {[*?]} $seg]} {
set pat [string map [list ** {.*} * {[^/]*} ? {[^/]}] $seg]
lappend pats "$pat"
} else {
lappend pats "$seg"
}
}
}
}

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

@ -418,28 +418,36 @@ namespace eval punk::repo {
continue
}
}
if {[string match "EDITED *" $ln]} {
set path [string trim [string range $ln [string length "EDITED "] end]] ;#should handle spaced paths
dict set pathdict $path "changed"
} elseif {[string match "ADDED *" $ln]} {
set path [string trim [string range $ln [string length "ADDED "] end]]
dict set pathdict $path "new"
} elseif {[string match "DELETED *" $ln]} {
set path [string trim [string range $ln [string length "DELETED "] end]]
dict set pathdict $path "missing"
} elseif {[string match "MISSING *" $ln]} {
set path [string trim [string range $ln [string length "MISSING "] end]]
dict set pathdict $path "missing"
} elseif {[string match "EXTRA *" $ln]} {
#fossil will explicitly list files in a new folder - as opposed to git which shows just the folder
set path [string trim [string range $ln [string length "EXTRA "] end]]
dict set pathdict $path "extra"
} elseif {[string match "UNCHANGED *" $ln]} {
set path [string trim [string range $ln [string length "UNCHANGED "] end]]
dict set pathdict $path "unchanged"
} else {
#emit for now
puts stderr "unprocessed fossilstate line: $ln"
switch -glob -- $ln {
"EDITED *" {
set path [string trim [string range $ln [string length "EDITED "] end]] ;#should handle spaced paths
dict set pathdict $path "changed"
}
"ADDED *" {
set path [string trim [string range $ln [string length "ADDED "] end]]
dict set pathdict $path "new"
}
"DELETED *" {
set path [string trim [string range $ln [string length "DELETED "] end]]
dict set pathdict $path "missing"
}
"MISSING *" {
set path [string trim [string range $ln [string length "MISSING "] end]]
dict set pathdict $path "missing"
}
"EXTRA * " {
#fossil will explicitly list files in a new folder - as opposed to git which shows just the folder
set path [string trim [string range $ln [string length "EXTRA "] end]]
dict set pathdict $path "extra"
}
"UNCHANGED *" {
set path [string trim [string range $ln [string length "UNCHANGED "] end]]
dict set pathdict $path "unchanged"
}
default {
#emit for now
puts stderr "unprocessed fossilstate line: $ln"
}
}
#other entries??
}

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

@ -73,7 +73,11 @@ namespace eval punkcheck {
set record_list [list]
if {[file exists $punkcheck_file]} {
set tdlscript [punk::mix::util::fcat $punkcheck_file]
set record_list [punk::tdl::prettyparse $tdlscript]
if {[catch {
set record_list [punk::tdl::prettyparse $tdlscript]
} errparse]} {
error "punkcheck::load_records_from_file failed to parse '$punkcheck_file'\n error:$errparse"
}
}
return $record_list
}
@ -131,8 +135,10 @@ namespace eval punkcheck {
#get last previous that is tagged INSTALL-RECORD,MODIFY-RECORD,DELETE-RECORD
set revlist [lreverse $previous_records]
foreach rec $revlist {
if {[dict get $rec tag] in [list "INSTALL-RECORD" "MODIFY-RECORD" "DELETE-RECORD" "VIRTUAL-RECORD"]} {
return $rec
switch -- [dict get $rec tag] {
INSTALL-RECORD - MODIFY-RECORD - DELETE-RECORD - VIRTUAL-RECORD {
return $rec
}
}
}
return [list]

1
src/bootsupport/modules/textutil/wcswidth-35.1.tm

@ -8,6 +8,7 @@
# Author: Sean Woods <yoda@etoyoc.com>
###
package provide textutil::wcswidth 35.1
namespace eval ::textutil {}
proc ::textutil::wcswidth_type char {
if {$char == 161} { return A }
if {$char == 164} { return A }

289
src/modules/flagfilter-0.3.tm

@ -731,38 +731,42 @@ namespace eval flagfilter {
lassign $vinfo class type val
if {[string match $classmatch $class]} {
set a [llength $all_flagged] ;#index into all_flagged list we are building
if {$type eq "soloflag"} {
if {[dict exists $seenflag $val]} {
set seenindex [dict get $seenflag $val]
set seenindexplus [expr {$seenindex+1}]
set existingvals [lindex $all_flagged $seenindexplus]
lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead?
lset all_flagged $seenindexplus $existingvals
} else {
dict set seenflag $val $a
lappend all_flagged $val 1
switch -- $type {
soloflag {
if {[dict exists $seenflag $val]} {
set seenindex [dict get $seenflag $val]
set seenindexplus [expr {$seenindex+1}]
set existingvals [lindex $all_flagged $seenindexplus]
lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead?
lset all_flagged $seenindexplus $existingvals
} else {
dict set seenflag $val $a
lappend all_flagged $val 1
}
}
} elseif {$type eq "flag"} {
if {![dict exists $seenflag $val]} {
dict set seenflag $val $a
lappend all_flagged $val
flag {
if {![dict exists $seenflag $val]} {
dict set seenflag $val $a
lappend all_flagged $val
}
#no need to do anything if already seen - flagvalue must be next, and it will work out where to go.
}
#no need to do anything if already seen - flagvalue must be next, and it will work out where to go.
} elseif {$type eq "flagvalue"} {
set idxflagfor [expr {$k -1}]
set flagforinfo [dict get $o_map $idxflagfor]
lassign $flagforinfo ffclass fftype ffval
#jn "--" following a flag could result in us getting here accidentaly.. review
set seenindex [dict get $seenflag $ffval]
if {$seenindex == [expr {$a-1}]} {
#usual case - this is a flagvalue following the first instance of the flag
lappend all_flagged $val
} else {
#write the value back to the seenindex+1
set seenindexplus [expr {$seenindex+1}]
set existingvals [lindex $all_flagged $seenindexplus]
lappend existingvals $val ;#we keep multiples as a list
lset all_flagged $seenindexplus $existingvals
flagvalue {
set idxflagfor [expr {$k -1}]
set flagforinfo [dict get $o_map $idxflagfor]
lassign $flagforinfo ffclass fftype ffval
#jn "--" following a flag could result in us getting here accidentaly.. review
set seenindex [dict get $seenflag $ffval]
if {$seenindex == [expr {$a-1}]} {
#usual case - this is a flagvalue following the first instance of the flag
lappend all_flagged $val
} else {
#write the value back to the seenindex+1
set seenindexplus [expr {$seenindex+1}]
set existingvals [lindex $all_flagged $seenindexplus]
lappend existingvals $val ;#we keep multiples as a list
lset all_flagged $seenindexplus $existingvals
}
}
}
}
@ -2230,23 +2234,28 @@ namespace eval flagfilter {
#jn concat allows $command to itself be a list
##tcl dispatchtype
dict set dispatchrecord dispatchtype $dispatchtype
if {$dispatchtype eq "tcl"} {
do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags"
#set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags]
set commandline [concat $command $matched_operands $matched_opts $extraflags]
} elseif {$dispatchtype eq "raw"} {
do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]"
#set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags]
set commandline [concat $command [dict get $dispatchrecord raw] $extraflags]
} elseif {$dispatchtype eq "shell"} {
do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]"
#assume the shell arguments are in one quoted string?
set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags]
} else {
#non quoted shell? raw + defaults?
do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags"
#set commandline [list $command {*}$matched_in_order {*}$extraflags]
set commandline [concat $command $matched_in_order $extraflags]
switch -- $dispatchtype {
tcl {
do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags"
#set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags]
set commandline [concat $command $matched_operands $matched_opts $extraflags]
}
raw {
do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]"
#set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags]
set commandline [concat $command [dict get $dispatchrecord raw] $extraflags]
}
shell {
do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]"
#assume the shell arguments are in one quoted string?
set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags]
}
default {
#non quoted shell? raw + defaults?
do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags"
#set commandline [list $command {*}$matched_in_order {*}$extraflags]
set commandline [concat $command $matched_in_order $extraflags]
}
}
dict set dispatchrecord asdispatched $commandline
@ -2378,97 +2387,102 @@ namespace eval flagfilter {
do_debug 1 $debugc "[string repeat = 40]"
foreach {k v} $combined {
set dlev [dict get $debugdict $k]
if {$k eq "dispatch"} {
set col1 [string repeat " " 12]
#process as paired list rather than dict (support repeated commands)
set i 0
foreach {cmdname cmdinfo} $v {
set field1 [string repeat " " [expr {[string length $cmdname]}]]
set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]]
set j 0
foreach {ckey cval} $cmdinfo {
switch -- $k {
dispatch {
set col1 [string repeat " " 12]
#process as paired list rather than dict (support repeated commands)
set i 0
foreach {cmdname cmdinfo} $v {
set field1 [string repeat " " [expr {[string length $cmdname]}]]
set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]]
set j 0
foreach {ckey cval} $cmdinfo {
if {$i == 0 && $j == 0} {
set c1 [overtype::left $col1 "dispatch"]
} else {
set c1 [overtype::left $col1 { ... }]
}
if {$j == 0} {
set f1 [overtype::left $field1 $cmdname]
set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
} else {
set f1 [overtype::left $field1 ...]
set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
}
#leave at debug level 1 - because dispatch is generally important
do_debug $dlev $debugc "${c1}${c2} $cval"
if {$i == 0 && $j == 0} {
set c1 [overtype::left $col1 "dispatch"]
} else {
set c1 [overtype::left $col1 { ... }]
incr j
}
if {$j == 0} {
set f1 [overtype::left $field1 $cmdname]
set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
incr i
}
#do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]"
#foreach {nm rem} [lrange $v 2 end] {
# do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]"
#}
}
dispatchresultlist {
set col1 [string repeat " " 25]
set i 0
foreach dresult $v {
if {$i == 0} {
set c1 [overtype::left $col1 $k]
} else {
set f1 [overtype::left $field1 ...]
set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
set c1 [overtype::left $col1 { ... }]
}
#leave at debug level 1 - because dispatch is generally important
do_debug $dlev $debugc "${c1}${c2} $cval"
incr j
do_debug $dlev $debugc "$c1 $dresult"
incr i
}
incr i
}
#do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]"
#foreach {nm rem} [lrange $v 2 end] {
# do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]"
#}
} elseif {$k eq "dispatchresultlist"} {
set col1 [string repeat " " 25]
set i 0
foreach dresult $v {
if {$i == 0} {
set c1 [overtype::left $col1 $k]
} else {
set c1 [overtype::left $col1 { ... }]
classifications {
set col1 [string repeat " " 25]
set len [dict size $v]
if {$len == 0} {
do_debug $dlev $debugc "[overtype::left $col1 $k]"
continue
}
do_debug $dlev $debugc "$c1 $dresult"
incr i
}
} elseif {$k eq "classifications"} {
set col1 [string repeat " " 25]
set len [dict size $v]
if {$len == 0} {
do_debug $dlev $debugc "[overtype::left $col1 $k]"
continue
}
set max [expr {$len -1}]
set numlines [expr $len / 3 + 1]
if {($len % 3) == 0} {
incr numlines -1
}
set j 0
for {set ln 0} {$ln < $numlines} {incr ln} {
if {$ln == 0} {
set c1 "[overtype::left $col1 $k]"
} else {
set c1 "[overtype::left $col1 { ... }]"
set max [expr {$len -1}]
set numlines [expr $len / 3 + 1]
if {($len % 3) == 0} {
incr numlines -1
}
set line ""
for {set col 0} {$col < 3} {incr col} {
if {$j <= $max} {
append line "$j [list [dict get $v $j]] "
set j 0
for {set ln 0} {$ln < $numlines} {incr ln} {
if {$ln == 0} {
set c1 "[overtype::left $col1 $k]"
} else {
set c1 "[overtype::left $col1 { ... }]"
}
set line ""
for {set col 0} {$col < 3} {incr col} {
if {$j <= $max} {
append line "$j [list [dict get $v $j]] "
}
incr j
}
incr j
do_debug $dlev $debugc "$c1 [string trim $line]"
}
do_debug $dlev $debugc "$c1 [string trim $line]"
}
} elseif {$k eq "gridstring"} {
set col1 [string repeat " " 25]
set i 0
foreach ln [split $v \n] {
if {$i == 0} {
set c1 [overtype::left $col1 $k]
} else {
set c1 [overtype::left $col1 { ... }]
gridstring {
set col1 [string repeat " " 25]
set i 0
foreach ln [split $v \n] {
if {$i == 0} {
set c1 [overtype::left $col1 $k]
} else {
set c1 [overtype::left $col1 { ... }]
}
do_debug $dlev $debugc "$c1 $ln"
incr i
}
do_debug $dlev $debugc "$c1 $ln"
incr i
}
} else {
set col1 [string repeat " " 25]
do_debug $dlev $debugc "[overtype::left $col1 $k] $v"
default {
set col1 [string repeat " " 25]
do_debug $dlev $debugc "[overtype::left $col1 $k] $v"
}
}
}
do_debug 1 $debugc "[string repeat = 40]"
@ -2495,20 +2509,19 @@ namespace eval flagfilter {
for {set i $a} {$i <=$b} {incr i} {
set arginfo [dict get $classifications $i]
lassign $arginfo class ftype v
if {$ftype eq "flag"} {
lappend extraflags $v
}
if {$ftype eq "soloflag"} {
lappend extraflags $v
if {[dict exists $defaults $v]} {
lappend extraflags [dict get $defaults $v]
} else {
lappend extraflags 1
switch -- $ftype {
flag - flagvalue {
lappend extraflags $v
}
}
if {$ftype eq "flagvalue"} {
lappend extraflags $v
}
soloflag {
lappend extraflags $v
if {[dict exists $defaults $v]} {
lappend extraflags [dict get $defaults $v]
} else {
lappend extraflags 1
}
}
}
}
foreach {k v} [dict get $defaults] {
if {$k ni $extraflags} {

854
src/modules/punk-0.1.tm

File diff suppressed because it is too large Load Diff

1265
src/modules/punk/ansi-999999.0a1.0.tm

File diff suppressed because it is too large Load Diff

128
src/modules/punk/args-999999.0a1.0.tm

@ -174,7 +174,6 @@ namespace eval punk::args {
#} $args
set optionspecs [string map [list \r\n \n] $optionspecs]
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi]
set optspec_defaults [dict create\
-optional 1\
-allow_ansi 1\
@ -204,11 +203,8 @@ namespace eval punk::args {
foreach ln $records {
set trimln [string trim $ln]
if {$trimln eq ""} {
continue
}
if {[string index $trimln 0] eq "#"} {
continue
switch -- [string index $trimln 0] {
"" - # {continue}
}
set argname [lindex $trimln 0]
set argspecs [lrange $trimln 1 end]
@ -224,8 +220,13 @@ namespace eval punk::args {
error "punk::args::opts_values - bad optionspecs line for argument '$argname' Remaining items on line must be in paired option-value format - received '$argspecs'"
}
dict for {spec specval} $argspecs {
if {$spec ni [concat $known_argspecs -ARGTYPE]} {
error "punk::args::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
#literal-key switch - bytecompiled to jumpTable
switch -- $spec {
-default - -type - -range - -choices - -nocase - -optional - -multiple - -validate_without_ansi - -allow_ansi - -strip_ansi - -ARGTYPE {}
default {
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi]
error "punk::args::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
}
}
}
set argspecs [dict merge $optspec_defaults $argspecs]
@ -450,65 +451,90 @@ namespace eval punk::args {
if {!$is_default} {
if {[dict exists $arg_info $o -type]} {
set type [dict get $arg_info $o -type]
if {[string tolower $type] in {int integer double}} {
if {[string tolower $type] in {int integer}} {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {double}} {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
switch -- [string tolower $type] {
int -
integer -
double {
if {[string tolower $type] in {int integer}} {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {double}} {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
}
}
}
}
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
}
}
}
}
} elseif {[string tolower $type] in {bool boolean}} {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {alnum alpha ascii control digit graph lower print punct space upper wordchar xdigit}} {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
bool -
boolean {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
}
} elseif {[string tolower $type] in {file directory existingfile existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
alnum -
alpha -
ascii -
control -
digit -
graph -
lower -
print -
punct -
space -
upper -
wordchar -
xdigit {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
}
}
}
if {[string tolower $type] in {existingfile}} {
file -
directory -
existingfile -
existingdirectory {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
if {[string tolower $type] in {existingfile}} {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
}
}
}
}
} elseif {[string tolower $type] in {char character}} {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
char -
character {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
}
}
}
}

190
src/modules/punk/cap/handlers/templates-999999.0a1.0.tm

@ -73,110 +73,118 @@ namespace eval punk::cap::handlers::templates {
#adhoc and currentproject* paths are relative to cwd - so no projectbase information can be stored at registration time.
#not all template item types will need projectbase information - as the item data may be self-contained within the template structure -
#but project_layout will need it - or at least need to know if there is no project - because project_layout data is never stored in the template folder structure directly.
if {$pathtype eq "adhoc"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
} elseif {$pathtype eq "module"} {
set provide_statement [package ifneeded $pkg [package require $pkg]]
set tmfile [lindex $provide_statement end]
if {![file exists $tmfile]} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - unable to determine base folder for package '$pkg' which is attempting to register with punk::cap as a provider of '$capname' capability"
return 0
switch -- $pathtype {
adhoc {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
}
module {
set provide_statement [package ifneeded $pkg [package require $pkg]]
set tmfile [lindex $provide_statement end]
if {![file exists $tmfile]} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - unable to determine base folder for package '$pkg' which is attempting to register with punk::cap as a provider of '$capname' capability"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
}
set tmfolder [file dirname $tmfile]
#todo - handle wrapped or unwrapped tarjar files - in which case we have to adjust tmfolder appropriately
#set tpath [file normalize [file join $tmfile [dict get $capdict relpath]]] ;#relpath is relative to the tm *file* - not it's containing folder
set projectinfo [punk::repo::find_repos $tmfolder]
set projectbase [dict get $projectinfo closest]
#store the projectbase even if it's empty string
set extended_capdict $capdict
set resolved_path [file join $tmfolder $path]
dict set extended_capdict resolved_path $resolved_path
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "currentproject_multivendor"} {
#currently only intended for punk::mix::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
}
set tmfolder [file dirname $tmfile]
#todo - handle wrapped or unwrapped tarjar files - in which case we have to adjust tmfolder appropriately
#set tpath [file normalize [file join $tmfile [dict get $capdict relpath]]] ;#relpath is relative to the tm *file* - not it's containing folder
set projectinfo [punk::repo::find_repos $tmfolder]
set projectbase [dict get $projectinfo closest]
#store the projectbase even if it's empty string
set extended_capdict $capdict
set resolved_path [file join $tmfolder $path]
dict set extended_capdict resolved_path $resolved_path
dict set extended_capdict projectbase $projectbase
}
currentproject_multivendor {
#currently only intended for punk::mix::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set extended_capdict $capdict
dict set extended_capdict vendor $vendor ;#vendor key still required.. controlling vendor?
} elseif {$pathtype eq "currentproject"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
set extended_capdict $capdict
dict set extended_capdict vendor $vendor ;#vendor key still required.. controlling vendor?
}
#verify that the relative path is within the relative path of a currentproject_multivendor tree
#todo - api for the _multivendor tree controlling package to validate
currentproject {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
#verify that the relative path is within the relative path of a currentproject_multivendor tree
#todo - api for the _multivendor tree controlling package to validate
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
} elseif {$pathtype eq "shellproject"} {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "shellproject_multivendor"} {
#currently only intended for punk::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
shellproject {
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
shellproject_multivendor {
#currently only intended for punk::templates - review if 3rd party _multivendor trees even make sense
if {$pkg ni $multivendor_package_whitelist} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but package is not in whitelist $multivendor_package_whitelist - 3rd party _multivendor tree not supported"
return 0
}
if {[file pathtype $path] ne "relative"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be a relative path"
return 0
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
set shellbase [file dirname [file dirname [file normalize [set ::argv0]/__]]] ;#review
set projectinfo [punk::repo::find_repos $shellbase]
set projectbase [dict get $projectinfo closest]
set extended_capdict $capdict
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} elseif {$pathtype eq "absolute"} {
if {[file pathtype $path] ne "absolute"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be absolute"
return 0
absolute {
if {[file pathtype $path] ne "absolute"} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' of type $pathtype which doesn't seem to be absolute"
return 0
}
set normpath [file normalize $path]
if {!file exists $normpath} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' which doesn't seem to exist"
return 0
}
set projectinfo [punk::repo::find_repos $normpath]
set projectbase [dict get $projectinfo closest]
#todo - verify no other provider has registered same absolute path - if sharing a project-external location is needed - they need their own subfolder
set extended_capdict $capdict
dict set extended_capdict resolved_path $normpath
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
}
set normpath [file normalize $path]
if {!file exists $normpath} {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' which doesn't seem to exist"
default {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' with unrecognised type $pathtype"
return 0
}
set projectinfo [punk::repo::find_repos $normpath]
set projectbase [dict get $projectinfo closest]
#todo - verify no other provider has registered same absolute path - if sharing a project-external location is needed - they need their own subfolder
set extended_capdict $capdict
dict set extended_capdict resolved_path $normpath
dict set extended_capdict vendor $vendor
dict set extended_capdict projectbase $projectbase
} else {
puts stderr "punk::cap::handlers::templates::capsystem::pkg_register WARNING - package '$pkg' is attempting to register with punk::cap as a provider of '$capname' capability but provided a path '$path' with unrecognised type $pathtype"
return 0
}
# -- --- --- --- --- --- --- ---- ---

540
src/modules/punk/char-999999.0a1.0.tm

@ -56,6 +56,8 @@
#[para] -
package require Tcl 8.6-
#dependency on tcllib not ideal for bootstrapping as punk::char is core to many features.. (textutil::wcswidth is therefore included in bootsupport/include_modules.config) review
package require textutil::wcswidth
#*** !doctools
#[list_end]
@ -71,6 +73,7 @@ package require Tcl 8.6-
namespace eval punk::char {
namespace export *
variable grapheme_widths [dict create]
# -- --------------------------------------------------------------------------
variable encmimens ;#namespace of mime package providing reversemapencoding and mapencoding functions
#tcllib mime requires tcl::chan::memchan,events,core and/or Trf
@ -1259,70 +1262,71 @@ namespace eval punk::char {
variable charsets
set hex_char [format %04x $dec_char]
set returninfo [dict create]
if {"dec" in $fields} {
dict set returninfo dec $dec_char
}
if {"hex" in $fields} {
dict set returninfo hex $hex_char
}
if {"desc" in $fields} {
if {[dict exists $charinfo $dec_char desc]} {
dict set returninfo desc [dict get $charinfo $dec_char desc]
} else {
dict set returninfo desc ""
}
}
if {"short" in $fields} {
if {[dict exists $charinfo $dec_char short]} {
dict set returninfo desc [dict get $charinfo $dec_char short]
} else {
dict set returninfo short ""
}
}
#todo - expectedwidth - lookup the printing width it is *supposed* to have from unicode tables
#testwidth is one of the main ones likely to be worthwhile excluding as querying the console via ansi takes time
if {"testwidth" in $fields} {
set existing_testwidth ""
if {[dict exists $charinfo $dec_char testwidth]} {
set existing_testwidth [dict get $charinfo $dec_char testwidth]
}
if {$existing_testwidth eq ""} {
#no cached data - do expensive cursor-position test (Note this might not be 100% reliable - terminals lie e.g if ansi escape sequence has set to wide printing.)
set char [format %c $dec_char]
set chwidth [char_info_testwidth $char]
dict set returninfo testwidth $chwidth
#cache it. todo - -verify flag to force recalc in case font/terminal changed in some way?
dict set charinfo $dec_char testwidth $chwidth
} else {
dict set returninfo testwidth $existing_testwidth
}
}
if {"char" in $fields} {
set char [format %c $dec_char]
dict set returninfo char $char
}
#memberof takes in the order of a few hundred microseconds if a simple scan of all ranges is taken - possibly worthwhile caching/optimising
#note that memberof is not just unicode blocks - but scripts and other non-contiguous sets consisting of multiple ranges - some of which may include ranges of a single character. (e.g WGL4)
#This means there probably isn't a really efficient means of calculating membership other than scanning all the defined ranges.
#We could potentially populate it using a side-thread - but it seems reasonable to just cache result after first use here.
#some efficiency could also be gained by pre-calculating the extents for each charset which isn't a simple unicode block. (and perhaps sorting by max char)
if {"memberof" in $fields} {
set memberof [list]
dict for {setname setinfo} $charsets {
foreach r [dict get $setinfo ranges] {
set s [dict get $r start]
set e [dict get $r end]
if {$dec_char >= $s && $dec_char <= $e} {
lappend memberof $setname
break
foreach f $fields {
switch -- $f {
dec {
dict set returninfo dec $dec_char
}
hex {
dict set returninfo hex $hex_char
}
desc {
if {[dict exists $charinfo $dec_char desc]} {
dict set returninfo desc [dict get $charinfo $dec_char desc]
} else {
dict set returninfo desc ""
}
}
short {
if {[dict exists $charinfo $dec_char short]} {
dict set returninfo desc [dict get $charinfo $dec_char short]
} else {
dict set returninfo short ""
}
}
testwidth {
#todo - expectedwidth - lookup the printing width it is *supposed* to have from unicode tables
#testwidth is one of the main ones likely to be worthwhile excluding as querying the console via ansi takes time
set existing_testwidth ""
if {[dict exists $charinfo $dec_char testwidth]} {
set existing_testwidth [dict get $charinfo $dec_char testwidth]
}
if {$existing_testwidth eq ""} {
#no cached data - do expensive cursor-position test (Note this might not be 100% reliable - terminals lie e.g if ansi escape sequence has set to wide printing.)
set char [format %c $dec_char]
set chwidth [char_info_testwidth $char]
dict set returninfo testwidth $chwidth
#cache it. todo - -verify flag to force recalc in case font/terminal changed in some way?
dict set charinfo $dec_char testwidth $chwidth
} else {
dict set returninfo testwidth $existing_testwidth
}
}
char {
set char [format %c $dec_char]
dict set returninfo char $char
}
memberof {
#memberof takes in the order of a few hundred microseconds if a simple scan of all ranges is taken - possibly worthwhile caching/optimising
#note that memberof is not just unicode blocks - but scripts and other non-contiguous sets consisting of multiple ranges - some of which may include ranges of a single character. (e.g WGL4)
#This means there probably isn't a really efficient means of calculating membership other than scanning all the defined ranges.
#We could potentially populate it using a side-thread - but it seems reasonable to just cache result after first use here.
#some efficiency could also be gained by pre-calculating the extents for each charset which isn't a simple unicode block. (and perhaps sorting by max char)
set memberof [list]
dict for {setname setinfo} $charsets {
foreach r [dict get $setinfo ranges] {
set s [dict get $r start]
set e [dict get $r end]
if {$dec_char >= $s && $dec_char <= $e} {
lappend memberof $setname
break
}
}
}
dict set returninfo memberof $memberof
}
}
dict set returninfo memberof $memberof
}
return $returninfo
@ -1512,31 +1516,75 @@ namespace eval punk::char {
#non-overlapping unicode blocks
proc char_blocks {name_or_glob} {
error "unicode block searching unimplemented"
#todo - search only charsets that have settype = block
proc char_blocks {{name_or_glob *}} {
variable charsets
#todo - more efficient datastructures?
if {![regexp {[?*]} $name_or_glob]} {
#no glob - just retrieve it
if {[dict exists $charsets $name_or_glob]} {
if {[dict get $charsets $name_or_glob settype] eq "block"} {
return [dict create $name_or_glob [dict get $charsets $name_or_glob]]
}
}
#no exact match - try case insensitive..
set name [lsearch -inline -nocase [dict keys $charsets] $name_or_glob]
if {$name ne ""} {
if {[dict get $charsets $name settype] eq "block"} {
return [dict create $name [dict get $charsets $name]]
}
}
} else {
#build a subset
set charsets_block [dict create]
dict for {k v} $charsets {
if {[string match -nocase $name_or_glob $k]} {
if {[dict get $v settype] eq "block"} {
dict set charsets_block $k $v
}
}
}
return $charsets_block
}
}
proc charset_names {{name_or_glob *}} {
variable charsets
if {![regexp {[?*]} $name_or_glob]} {
#no glob - just retrieve it
if {[dict exists $charsets $name_or_glob]} {
return [list $name_or_glob]
}
#no exact match - try case insensitive..
set name [lsearch -inline -nocase [dict keys $charsets] $name_or_glob]
if {$name ne ""} {
return [list $name]
}
} else {
if {$name_or_glob eq "*"} {
return [lsort [dict keys $charsets]]
}
return [lsort [lsearch -all -inline -nocase [dict keys $charsets] $name_or_glob]]
}
}
#deprecated
#major named sets such as unicode blocks, scripts, and other sets such as microsoft WGL4
#case insensitive search - possibly with globs
proc charset_names {{namesearch *}} {
#case insensitive search - possibly with *basic* globs (? and * only - not lb rb)
proc charset_names2 {{namesearch *}} {
variable charsets
set sortedkeys [lsort -increasing -dictionary [dict keys $charsets]] ;#NOTE must use -dictionary to use -sorted flag below
#dictionary sorting of the keys is slow! - we should obviously store it in sorted order instead of sorting entire list on retrieval - or just sort results
#set sortedkeys [lsort -increasing -dictionary [dict keys $charsets]] ;#NOTE must use -dictionary to use -sorted flag below
set sortedkeys [lsort -increasing [dict keys $charsets]]
if {$namesearch eq "*"} {
return $sortedkeys
}
if {[regexp {[?*]} $namesearch]} {
#name glob search
set matched_names [lsearch -all -inline -nocase $sortedkeys $namesearch] ;#no point using -sorted flag when -all is used
return [lsearch -all -inline -nocase $sortedkeys $namesearch] ;#no point using -sorted flag when -all is used
} else {
set matched [lsearch -sorted -inline -nocase $sortedkeys $namesearch] ;#no globs - stop on first match
if {[llength $matched]} {
return [list $matched]
} else {
return [list]
}
#return [lsearch -sorted -inline -nocase $sortedkeys $namesearch] ;#no globs - bails out earlier if -sorted?
return [lsearch -inline -nocase $sortedkeys $namesearch] ;#no globs
}
return $matched_names
}
proc charsets {{namesearch *}} {
package require textblock
@ -1585,7 +1633,7 @@ namespace eval punk::char {
}
set dict_list [list]
foreach m $matches {
lappend dict_list [dict create $m [charset_dictget $name]]
lappend dict_list [dict create $m [charset_dictget $m]]
}
#return $dict_list
return [join $dict_list \n]
@ -1651,7 +1699,8 @@ namespace eval punk::char {
set twidth [dict get $charinfo $dec testwidth]
}
if {$twidth eq ""} {
set width [ansifreestring_width $ch] ;#based on unicode props
#set width [ansifreestring_width $ch] ;#based on unicode props
set width [grapheme_width_cached $ch]
} else {
set width $twidth
}
@ -1780,7 +1829,7 @@ namespace eval punk::char {
}
if {$twidth eq ""} {
#puts -nonewline stdout "." ;#this
set width [char_info_testwidth $ch] ;#based on unicode props
set width [char_info_testwidth $ch] ;#based on console test rather than unicode props
dict set charinfo $dec testwidth $width
} else {
set width $twidth
@ -1792,22 +1841,172 @@ namespace eval punk::char {
puts stdout "\ncalibration done - results cached in charinfo dictionary"
return [dict create charcount $charcount widths $width_results]
}
#todo - provide a char_width equivalent that is optimised for speed
#maint warning - also in overtype!
#intended for single grapheme - but will work for multiple
#cannot contain ansi or newlines
#(a cache of ansifreestring_width calls - as these are quite regex heavy)
proc grapheme_width_cached {ch} {
variable grapheme_widths
if {[dict exists $grapheme_widths $ch]} {
return [dict get $grapheme_widths $ch]
}
set width [punk::char::ansifreestring_width $ch] ;#review - can we provide faster version if we know it's a single grapheme rather than a string? (grapheme is still a string as it may have combiners/diacritics)
dict set grapheme_widths $ch $width
return $width
}
#no char_width - use grapheme_width terminology to be clearer
proc grapheme_width {char} {
error "grapheme_width unimplemented - use ansifreestring_width"
}
#return N Na W etc from unicode data
#review
proc char_uc_width_prop {char} {
error "char_uc_width unimplemented try textutil::wcswidth_type"
}
#todo - provide a grapheme_width function that is optimised for speed
proc string_width {text} {
#burn approx 2uS (2024) checking for ansi codes - not just SGR
if {[punk::ansi::ta::detect $text]} {
puts stderr "string_width detected ANSI!"
}
if {[string first \n $text] >= 0} {
if {[string last \n $text] >= 0} {
error "string_width accepts only a single line"
}
tailcall ansifreestring_width $text
}
#prerequisites - no ansi escapes - no newlines
#faster than textutil::wcswidth (at least for string up to a few K in length)
proc wcswidth {string} {
set codes [scan $string [string repeat %c [string length $string]]]
set width 0
foreach c $codes {
set w [textutil::wcswidth_char $c]
if {$w < 0} {
return -1
} else {
incr width $w
}
}
return $width
}
proc wcswidth2 {string} {
set codes [scan $string [string repeat %c [string length $string]]]
set widths [lmap c $codes {textutil::wcswidth_char $c}]
if {-1 in $widths} {
return -1
}
return [tcl::mathop::+ {*}$widths]
}
#prerequisites - no ansi escapes - no newlines - utf8 encoding assumed
#review - what about \r \t \b ?
#NO processing of \b - already handled in ansi::printing_length which then calls this
#this version breaks string into sequences of ascii vs unicode
proc ansifreestring_width {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
#we can c0 control characters after or while processing ansi escapes.
#we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!)
#anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error
#if {[string first \033 $text] >= 0} {
# error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first"
#}
#todo - various combining diacritical marks.. from grave - to various complicated unicode joiners and composing chars etc
#as at 2023 - terminals generally seem to use the simplistic approach of tallying up individual character-widths, which means combinations that print short are reported too long by the terminal esc 6 n sequence.
#- {Combining Diacritical Marks} {ranges {{start 768 end 879 note {unicode block 0300..036F}}} description {} settype block}
#- {Combining Diacritical Marks Extended} {ranges {{start 6832 end 6911 note {unicode block 1AB0..1AFF}}} description {} settype block}
#- {Combining Diacritical Marks Supplement} {ranges {{start 7616 end 7679 note {unicode block 1DC0..1DFF}}} description {} settype block}
#- {Combining Diacritical Marks for Symbols} {ranges {{start 8400 end 8447 note {unicode block 20D0..20FF}}} description {} settype block}
#- {Combining Half Marks} {ranges {{start 65056 end 65071 note {unicode block FE20..FE2F}}} description {} settype block}
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
# set text " $text"
#}
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
# -- --- --- --- --- --- ---
#review
#if we strip out ZWJ \u200d (zero width combiner) - then we don't give the chance for a grapheme combining test to merge properly e.g combining emojis
#as at 2024 - textutil::wcswidth just uses the unicode east-asian width property data and doesn't seem to handle these specially - it counts this joiner and others as one wide (also BOM \uFFEF)
#TODO - once we have proper grapheme cluster splitting - work out which of these characters should be left in and/or when exactly their length-effects apply
#
#for now - strip them out
#ZWNJ \u200c should also be zero length - but as a 'non' joiner - it should add zero to the length
#ZWSP \u200b zero width space
#\uFFEFBOM/ ZWNBSP and others that should be zero width
#todo - work out proper way to mark/group zero width.
set text [string map [list \u200b "" \u200c "" \u200d "" \uFFEF ""] $text]
# -- --- --- --- --- --- ---
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
#c0 controls
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
#c1 controls - first section of the Latin-1 Supplement block - all non-printable from a utf-8 perspective
#some or all of these may have a visual representation in other encodings e.g cp855 seems to have 1 width for them all
#we are presuming that the text supplied has been converted from any other encoding to utf8 - so we don't need to handle that here
#they should also be unlikely to be present in an ansi-free string (as is a precondition for use of this function)
set text [regsub -all {[\u0080-\u009f]+} $text ""]
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
# return [string length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
return [string length $text]
}
#split just to get the standalone character widths - and then scan for other combiners (?) - or scan for clusters first?
#review
#set can_regex_high_unicode [string match [regexp -all -inline {[\U80-\U0010FFFF]} \U0001F525] \U0001F525]
#tcl pre 2023-11 - braced high unicode regexes don't work
#fixed in bug-4ed788c618 2023-11
#set uc_chars [regexp -all -inline "\[\u0100-\U10FFFF\]" $text] ;#e.g return list of chars in range only
#maintain unicode as sequences - todo - scan for grapheme clusters
#set uc_sequences [punk::ansi::ta::_perlish_split "(?:\[\u0100-\U10FFFF\])+" $text]
set uc_sequences [punk::ansi::ta::_perlish_split "(?:\[\u0000-\u00FF\])+" $text]
set len 0
foreach {uc ascii} $uc_sequences {
#puts "-ascii $ascii"
#puts "-uc $uc"
incr len [string length $ascii]
#textutil::wcswidth uses unicode data
#fall back to textutil::wcswidth (which doesn't for example handle diactricts/combiners so we can't use until these and other things such as \u200b and diacritics are already stripped/accounted for)
#todo - find something that understands grapheme clusters - needed also for grapheme_split
#use punk::char::wcswidth - faster than the string split in textutil::wcswidth but still uses textutil::wcswidth_char
incr len [wcswidth $uc]
}
#todo - work out what to do with anomalies like grave combiner which print at different lengths on different terminals (fonts?) and for which cursor-query escape sequence lies.
return $len
}
#kept as a fallback for review/test if textutil::wcswidth doesn't do what we require on all terminals.
#this version looks at console testwidths if they've been cached.
#It is relatively fast - but tests unicode widths char by char - so won't be useful going forward for grapheme clusters.
proc ansifreestring_width2 {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
@ -1831,7 +2030,7 @@ namespace eval punk::char {
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
@ -1840,10 +2039,17 @@ namespace eval punk::char {
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
#review
#if we strip out ZWJ \u0200d (zero width combiner) - then we don't give the chance for a grapheme combining test to merge properly e.g combining emojis
#as at 2024 - textutil::wcswidth doesn't seem to be aware of these - and counts the joiner as one wide
#ZWNJ \u0200c should also be zero length - but as a 'non' joiner - it should add zero to the length
#only map control sequences to nothing after processing ones with special effects, such as \b (\x07f)
#Note DEL \x1f will only
#ZWSP \u0200b zero width space
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
@ -1856,7 +2062,7 @@ namespace eval punk::char {
return [string length $text]
}
#todo - check double-width chars in unicode blocks.. try to do reasonably quicky
#review - wcswidth should detect these
set re_ascii_fullwidth {[\uFF01-\uFF5e]}
set doublewidth_char_count 0
@ -1867,20 +2073,34 @@ namespace eval punk::char {
#tcl pre 2023-11 - braced high unicode regexes don't work
#fixed in bug-4ed788c618 2023-11
#set uc_sequences [regexp -all -inline -indices {[\u0100-\U10FFFF]} $text]
set uc_sequences [regexp -all -inline -indices "\[\u0100-\U10FFFF\]" $text]
foreach uc_range $uc_sequences {
set chars [string range $text {*}$uc_range]
foreach c $chars {
if {[regexp $re_ascii_fullwidth $c]} {
incr doublewidth_char_count
} else {
#todo - replace with function that doesn't use console - just unicode data
#set uc_sequences [regexp -all -inline -indices "\[\u0100-\U10FFFF\]" $text] ;#e.g for string: \U0001f9d1abc\U001f525ab returns {0 0} {4 4}
set uc_chars [regexp -all -inline "\[\u0100-\U10FFFF\]" $text] ;#e.g return list of chars in range only
foreach c $uc_chars {
if {[regexp $re_ascii_fullwidth $c]} {
incr doublewidth_char_count
} else {
#review
# a)- terminals lie - so we could have a bad cached testwidth
# b)- textutil::wcswidth_char seems to be east-asian-width based - and not a reliable indicator of 'character cells taken by the character when printed to the terminal' despite this statement in tclllib docs.
#(character width is a complex context-dependent topic)
# c) by checking for a cached console testwidth first - we make this function less deterministic/repeatable depending on whether console tests have been run.
# d) Upstream caching of grapheme_width may also lock in a result from whatever method was first employed here
#Despite all this - the mechanism is hoped to give best effort consistency for terminals
#further work needed for combining emojis etc - which can't be done in a per character loop
#todo - see if wcswidth does any of this. It is very slow for strings that include mixed ascii/unicode - so perhaps we can use a perlish_split
# to process sequences of unicode.
#- and the user has the option to test character sets first if terminal position reporting gives better results
if {[char_info_is_testwidth_cached $c]} {
set width [char_info_testwidth_cached $c]
if {$width == 0} {
incr zerowidth_char_count
} elseif {$width == 2} {
incr doublewidth_char_count
}
} else {
#textutil::wcswidth uses unicode data
#fall back to textutil::wcswidth (which doesn't for example handle diactricts/combiners so we can't use until these and other things such as \u200b and diacritics are already stripped/accounted for)
set width [textutil::wcswidth_char [scan $c %c]]
}
if {$width == 0} {
incr zerowidth_char_count
} elseif {$width == 2} {
incr doublewidth_char_count
}
}
}
@ -1888,6 +2108,58 @@ namespace eval punk::char {
return [expr {[string length $text] + $doublewidth_char_count - $zerowidth_char_count}]
}
#slow - textutil::wcswidth is slow with mixed ascii uc
proc ansifreestring_width3 {text} {
#caller responsible for calling ansistrip first if text may have ansi codes - and for ensuring no newlines
#we can c0 control characters after or while processing ansi escapes.
#we need to map remaining control characters to zero-width (under assumption we're not using a font/codepage that displays them - review!)
#anyway - we must allow things like raw ESC,DEL, NUL etc to pass through without error
#if {[string first \033 $text] >= 0} {
# error "string_width doesn't accept ansi escape sequences. Use punk::ansi::stripansi first"
#}
#review - work out what to do with anomalies like grave combiner which print at different lengths on different terminals (fonts?) and for which cursor-query escape sequence lies.
# - various combining diacritical marks.. from grave - to various complicated unicode joiners and composing chars etc
#as at 2023 - terminals generally seem to use the simplistic approach of tallying up individual character-widths, which means combinations that print short are reported too long by the terminal esc 6 n sequence.
#- {Combining Diacritical Marks} {ranges {{start 768 end 879 note {unicode block 0300..036F}}} description {} settype block}
#- {Combining Diacritical Marks Extended} {ranges {{start 6832 end 6911 note {unicode block 1AB0..1AFF}}} description {} settype block}
#- {Combining Diacritical Marks Supplement} {ranges {{start 7616 end 7679 note {unicode block 1DC0..1DFF}}} description {} settype block}
#- {Combining Diacritical Marks for Symbols} {ranges {{start 8400 end 8447 note {unicode block 20D0..20FF}}} description {} settype block}
#- {Combining Half Marks} {ranges {{start 65056 end 65071 note {unicode block FE20..FE2F}}} description {} settype block}
#
# initial simplistic approach is just to strip these ... todo REVIEW
#experiment to detect leading diacritics - but this isn't necessary - it's still zero-width - and if the user is splitting properly we shouldn't be getting a string with leading diacritics anyway
#(leading combiners may display in terminal as mark on rightmost prompt char which is usually a space - but won't add width even then)
#set re_leading_diacritic {^(?:[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+)}
#if {[regexp $re_leading_diacritic $text]} {
# set text " $text"
#}
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set text [regsub -all $re_diacritics $text ""]
#we should only map control sequences to nothing after processing ones with length effects, such as \b (\x07f) or DEL \x1f
#todo - document that these shouldn't be present in input rather than explicitly checking here
set re_ascii_c0 {[\U0000-\U001F]}
set text [regsub -all $re_ascii_c0 $text ""]
#short-circuit basic cases
#support tcl pre 2023-11 - see regexp bug below
#if {![regexp {[\uFF-\U10FFFF]} $text]} {
# return [string length $text]
#}
if {![regexp "\[\uFF-\U10FFFF\]" $text]} {
return [string length $text]
}
#slow when ascii mixed with unicode (but why?)
return [punk::wcswidth $text]
}
#This shouldn't be called on text containing ansi codes!
proc strip_nonprinting_ascii {str} {
#review - some single-byte 'control' chars have visual representations e.g ETX as heart depending on font/codepage
@ -1901,22 +2173,14 @@ namespace eval punk::char {
return [string map $map $str]
}
proc char_width {char} {
error "char_width unimplemented"
}
#return N Na W etc from unicode data
proc char_uc_width_prop {char} {
error "char_uc_width unimplemented"
}
#split into plaintext and runs of combiners
#split into plaintext and runs of combiners (combining diacritical marks - not ZWJ or ZWJNJ)
proc combiner_split {text} {
#split into odd numbered list (or zero) in a similar way to punk::ansi::ta::_perlish_split
#
set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
#set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
set graphemes [list]
set g ""
if {[string length $text] == 0} {
return {}
}
@ -1925,7 +2189,7 @@ namespace eval punk::char {
set strlen [string length $text]
#make sure our regexes aren't non-greedy - or we may not have exit condition for loop
#review
while {$start < $strlen && [regexp -start $start -indices -- $re_diacritics $text match]} {
while {$start < $strlen && [regexp -start $start -indices -- {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} $text match]} {
lassign $match matchStart matchEnd
#puts "->start $start ->match $matchStart $matchEnd"
lappend list [string range $text $start $matchStart-1] [string range $text $matchStart $matchEnd]
@ -1936,13 +2200,19 @@ namespace eval punk::char {
#}
}
lappend list [string range $text $start end]
return $list
}
#ZWJ ZWNJ ?
#1st shot - basic diacritics
#todo - become aware of unicode grapheme cluster boundaries
#
#This is difficult in Tcl without unicode property based Character Classes in the regex engine
#review - this needs to be performant - it is used a lot by punk terminal/ansi features
#todo - trie data structures for unicode?
#for now we can at least combine diacritics
#should also handle the ZWJ (and the variation selectors? eg \uFE0F) character which should account for emoji clusters
#Note - emoji cluster could be for example 11 code points/41 bytes (family emoji with skin tone modifiers for each member, 3 ZWJs)
#This still leaves a whole class of clusters.. korean etc unhandled :/
proc grapheme_split {text} {
set graphemes [list]
set csplits [combiner_split $text]
@ -1957,9 +2227,51 @@ namespace eval punk::char {
}
return $graphemes
}
proc grapheme_split_dec {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} [lrange $csplits 0 end-1] {
set pt_decs [scan $pt [string repeat %c [string length $pt]]]
set combiner_decs [scan $combiners [string repeat %c [string length $combiners]]]
lset pt_decs end [concat [lindex $pt_decs end] $combiner_decs]
lappend graphemes {*}$pt_decs
}
#last csplit never has a combiner (_perlish_split style) - and may be empty - in which case we don't append it as a grapheme
if {[lindex $csplits end] ne ""} {
lappend graphemes {*}[scan [lindex $csplits end] [string repeat %c [string length [lindex $csplits end]]]]
}
return $graphemes
}
proc grapheme_split_dec2 {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} $csplits {
set pt_decs [scan $pt [string repeat %c [string length $pt]]]
if {$combiners ne ""} {
set combiner_decs [scan $combiners [string repeat %c [string length $combiners]]]
lset pt_decs end [concat [lindex $pt_decs end] $combiner_decs]
}
lappend graphemes {*}$pt_decs
}
return $graphemes
}
proc grapheme_split2 {text} {
set graphemes [list]
set csplits [combiner_split $text]
foreach {pt combiners} [lrange $csplits 0 end-1] {
set clist [split $pt ""]
lappend graphemes {*}[lrange $clist 0 end-1] [string cat [lindex $clist end] $combiners]
}
#last csplit never has a combiner (_perlish_split style) - and may be empty - in which case we don't append it as a grapheme
if {[lindex $csplits end] ne ""} {
lappend graphemes {*}[split [lindex $csplits end] ""]
}
return $graphemes
}
# -- --- --- --- ---
#will accept a single char or a string - test using console cursor position reporting
#unreliable
proc char_info_testwidth {ch {emit 0}} {
package require punk::console
#uses cursor movement and relies on console to report position.. which it may do properly for single chars - but may misreport for combinations that combine into a single glyph
@ -1980,6 +2292,10 @@ namespace eval punk::char {
return $twidth
}
}
proc char_info_is_testwidth_cached {char} {
variable charinfo
return [dict exists $charinfo [scan $char %c] testwidth]
}
# -- --- --- --- ---

3
src/modules/punk/config-0.1.tm

@ -37,7 +37,10 @@ namespace eval punk::config {
dict set startup exec_unknown true ;#whether to use exec instead of experimental shellfilter::run
#dict set startup color_stdout [list cyan bold] ;#not a good idea to default
dict set startup color_stdout [list]
#This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful.
dict set startup color_stderr [list red bold]
dict set startup syslog_stdout "127.0.0.1:514"
dict set startup syslog_stderr "127.0.0.1:514"
dict set startup syslog_active 0

174
src/modules/punk/console-999999.0a1.0.tm

@ -30,6 +30,9 @@ if {"windows" eq $::tcl_platform(platform)} {
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::console {
variable tabwidth 8 ;#default only - will attempt to detect and set to that configured in terminal
#Note that windows terminal cooked mode seems to use 8 for interactive use even if set differently
#e.g typing tab characters may still be echoed 8-spaced while writing to stdout my obey the terminal's tab stops.
variable has_twapi 0
variable previous_stty_state_stdin ""
variable previous_stty_state_stdout ""
@ -572,7 +575,7 @@ namespace eval punk::console {
if {$waitvar($callid) ne "timedout"} {
after cancel $cancel_timeout_id
} else {
puts stderr "timeout in get_ansi_response_payload"
puts stderr "timeout in get_ansi_response_payload. Ansi request was:[ansistring VIEW $query]"
}
if {$was_raw == 0} {
@ -694,25 +697,27 @@ namespace eval punk::console {
}
}
proc a {args} {
variable colour_disabled
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
return
}
#stdout
tailcall ansi::a {*}$args
}
proc a+ {args} {
variable colour_disabled
variable ansi_wanted
if {$colour_disabled || $ansi_wanted <= 0} {
return
}
#stdout
tailcall ansi::a+ {*}$args
}
#a and a+ functions are not very useful when emitting directly to console
#e.g puts [punk::console::a red]test[punk::console::a cyan] would produce a cyan coloured test as the commands are evaluated first
#proc a {args} {
# variable colour_disabled
# variable ansi_wanted
# if {$colour_disabled || $ansi_wanted <= 0} {
# return
# }
# #stdout
# tailcall ansi::a {*}$args
#}
#proc a+ {args} {
# variable colour_disabled
# variable ansi_wanted
# if {$colour_disabled || $ansi_wanted <= 0} {
# return
# }
# #stdout
# tailcall ansi::a+ {*}$args
#}
proc a? {args} {
#stdout
variable colour_disabled
@ -754,14 +759,25 @@ namespace eval punk::console {
variable ansi_wanted
if {[string length $onoff]} {
set onoff [string tolower $onoff]
if {$onoff in [list 1 on true yes]} {
set ansi_wanted 1
} elseif {$onoff in [list 0 off false no]} {
set ansi_wanted 0
} elseif {$onoff in [list default]} {
set ansi_wanted 2
} else {
error "punk::console::ansi expected 0|1|on|off|true|false|yes|no|default"
switch -- $onoff {
1 -
on -
true -
yes {
set ansi_wanted 1
}
0 -
off -
false -
no {
set ansi_wanted 0
}
default {
set ansi_wanted 2
}
default {
error "punk::console::ansi expected 0|1|on|off|true|false|yes|no|default"
}
}
}
catch {repl::reset_prompt}
@ -868,6 +884,70 @@ namespace eval punk::console {
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
return $payload
}
proc get_tabstops {{inoutchannels {stdin stdout}}} {
#DECTABSR \x1b\[2\$w
#response example " ^[P2$u9/17/25/33/41/49/57/65/73/81^[\ " (where ^[ is \x1b)
#set capturingregex {(.*)(\x1b\[P2$u()\x1b\[\\)}
#set capturingregex {(.*)(\x1bP2$u((?:[0-9]+)*(?:\/[0-9]+)*)\x1b\\)$}
set capturingregex {(.*)(\x1bP2\$u(.*)\x1b\\)$}
set request "\x1b\[2\$w"
set payload [punk::console::internal::get_ansi_response_payload $request $capturingregex $inoutchannels]
set tabstops [split $payload "/"]
return $tabstops
}
#a simple estimation of tab-width under assumption console is set with even spacing.
#It's known this isn't always the case - but things like textutil::untabify2 take only a single value
#on some systems test_char_width is a similar speed to get_tabstop_apparent_width - but on some test_char_width is much slower
#we will use test_char_width as a fallback
proc get_tabstop_apparent_width {} {
set tslist [get_tabstops]
if {![llength $tslist]} {
#either terminal failed to report - or none set.
set testw [test_char_width \t]
if {[string is integer -strict $testw]} {
return $testw
}
#We don't support none - default to 8
return 8
}
#we generally expect to see a tabstop at column 1 - but it may not be set.
if {[lindex $tslist 0] eq "1"} {
if {[llength $tslist] == 1} {
set testw [test_char_width \t]
if {[string is integer -strict $testw]} {
return $testw
}
return 8
} else {
set next [lindex $tslist 1]
return [expr {$next - 1}]
}
} else {
#simplistic guess at width - review - do we need to consider leftmost tabstops as more likely to be non-representative and look further into the list?
if {[llength $tslist] == 1} {
return [lindex $tslist 0]
} else {
return [expr {[lindex $tslist 1] - [lindex $tslist 0]}]
}
}
}
#default to 8 just because it seems to be most common default in terminals
proc set_tabstop_width {{w 8}} {
set tsize [get_size]
set width [dict get $tsize columns]
set mod [expr {$width % $w}]
set max [expr {$width - $mod}]
set tstops ""
set c 1
while {$c <= $max} {
append tstops [string repeat " " $w][punk::ansi::set_tabstop]
incr c $w
}
set punk::console::tabwidth $w ;#we also attempt to read terminal's tabstops and set tabwidth to the apparent spacing of first non-1 value in tabstops list.
catch {textutil::tabify::untabify2 "" $w} ;#textutil tabify can end up uninitialised and raise errors like "can't read Spaces(<n>).." after a tabstop change This call seems to keep tabify happy - review.
puts -nonewline "[punk::ansi::clear_all_tabstops]\n[punk::ansi::set_tabstop]$tstops"
}
proc get_cursor_pos_list {} {
@ -875,12 +955,14 @@ namespace eval punk::console {
}
proc get_size {} {
if {[catch {
puts -nonewline [punk::ansi::cursor_off][punk::ansi::cursor_save][punk::ansi::move 2000 2000]
#some terminals (conemu on windows) scroll the viewport when we make a big move down like this - a move to 1 1 immediately after cursor_save doesn't seem to fix that.
#This issue also occurs when switching back from the alternate screen buffer - so perhaps that needs to be addressed elsewhere.
puts -nonewline [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list] lines cols
puts -nonewline [punk::ansi::cursor_restore][punk::console::cursor_on];flush stdout
set result [list columns $cols rows $lines]
} errM]} {
puts -nonewline [punk::ansi::cursor_restore]
puts -nonewline [punk::ansi::cursor_restore_dec]
puts -nonewline [punk::ansi::cursor_on]
error "$errM"
} else {
@ -905,7 +987,7 @@ namespace eval punk::console {
#todo - determine if these anomalies are independent of font
#punk::ansi should be able to glean widths from unicode data files - but this may be incomplete - todo - compare with what terminal actually does.
proc test_char_width {char_or_string {emit 0}} {
return 1
#return 1
#JMN
#puts stderr "cwtest"
variable ansi_available
@ -1118,10 +1200,10 @@ namespace eval punk::console {
move $orig_row $orig_col
}
proc scroll_up {n} {
puts -nonewline stdout [punk::ansi::scroll_up]
puts -nonewline stdout [punk::ansi::scroll_up $n]
}
proc scroll_down {n} {
puts -nonewline stdout [punk::ansi::scroll_down]
puts -nonewline stdout [punk::ansi::scroll_down $n]
}
#review - worth the extra microseconds to inline? might be if used in for example prompt on every keypress.
@ -1136,6 +1218,18 @@ namespace eval punk::console {
#[call [fun cursor_restore]]
puts -nonewline \x1b\[u
}
#DEC equivalents of cursor_save/cursor_restore - perhaps more widely supported?
proc cursor_save_dec {} {
#*** !doctools
#[call [fun cursor_save_dec]]
puts -nonewline \x1b7
}
proc cursor_restore_dec {} {
#*** !doctools
#[call [fun cursor_restore_dec]]
puts -nonewline \x1b8
}
proc insert_spaces {count} {
puts -nonewline stdout \x1b\[${count}@
}
@ -1162,6 +1256,8 @@ namespace eval punk::console {
namespace import ansi::move_row
namespace import ansi::cursor_save
namespace import ansi::cursor_restore
namespace import ansi::cursor_save_dec
namespace import ansi::cursor_restore_dec
namespace import ansi::scroll_down
namespace import ansi::scroll_up
namespace import ansi::insert_spaces
@ -1180,7 +1276,7 @@ namespace eval punk::console {
#set blanks [string repeat " " [expr {$col + $tw}]]
#puts -nonewline [punk::ansi::erase_eol]$blanks;move_emit_return this $col $text
#puts -nonewline [move_emit_return this $col [punk::ansi::insert_spaces 150]$text]
cursor_save
cursor_save_dec
#move_emit_return this $col [punk::ansi::move_forward 50][punk::ansi::insert_spaces 150][punk::ansi::move_back 50][punk::ansi::move_forward $col]$text
#puts -nonewline [punk::ansi::insert_spaces 150][punk::ansi::move_column $col]$text
puts -nonewline [punk::ansi::erase_eol][punk::ansi::move_column $col]$text
@ -1203,16 +1299,16 @@ namespace eval punk::console {
puts -nonewline stdout $commands
return ""
}
#we can be faster and more efficient if we use the consoles cursor_save command - but each savecursor overrides any previous one.
#we can be faster and more efficient if we use the consoles cursor_save_dec command - but each savecursor overrides any previous one.
#leave cursor_off/cursor_on to caller who can wrap more efficiently..
proc cursorsave_move_emit_return {row col data args} {
set commands ""
append commands [punk::ansi::cursor_save]
append commands [punk::ansi::cursor_save_dec]
append commands [punk::ansi::move_emit $row $col $data]
foreach {row col data} $args {
append commands [punk::ansi::move_emit $row $col $data]
}
append commands [punk::ansi::cursor_restore]
append commands [punk::ansi::cursor_restore_dec]
puts -nonewline stdout $commands; flush stdout
}
proc move_emitblock_return {row col textblock} {
@ -1229,12 +1325,12 @@ namespace eval punk::console {
}
proc cursorsave_move_emitblock_return {row col textblock} {
set commands ""
append commands [punk::ansi::cursor_save]
append commands [punk::ansi::cursor_save_dec]
foreach ln [split $textblock \n] {
append commands [punk::ansi::move_emit $row $col $ln]
incr row
}
append commands [punk::ansi::cursor_restore]
append commands [punk::ansi::cursor_restore_dec]
puts -nonewline stdout $commands;flush stdout
return
}

55
src/modules/punk/fileline-999999.0a1.0.tm

@ -368,6 +368,7 @@ namespace eval punk::fileline::class {
} else {
set tail [string trimleft $opt_linebase +];#ignore +
}
#todo - switch -glob -- $tail
if {[string match eof* $tail]} {
set endmath [string range $tail 3 end]
#todo endmath?
@ -1066,32 +1067,37 @@ namespace eval punk::fileline::class {
foreach whichvar [list start end] {
upvar 0 ${whichvar}idx index
if {![string is digit -strict $index]} {
if {"end" eq $index} {
set index $max
} elseif {[string match "*-*" $index]} {
#end-int or int-int - like lrange etc we don't accept arbitrarily complex expressions
lassign [split $index -] A B
if {$A eq "end"} {
set index [expr {$max - $B}]
} else {
set index [expr {$A - $B}]
switch -glob -- $index {
end {
set index $max
}
} elseif {[string match "*+*" $index]} {
lassign [split $index +] A B
if {$A eq "end"} {
#review - this will just result in out of bounds error in final test - as desired
#By calculating here - we will see the result in the error message - but it's probably not particularly useful - as we don't really need end+ support at all.
set index [expr {$max + $B}]
} else {
set index [expr {$A + $B}]
"*-*" {
#end-int or int-int - like lrange etc we don't accept arbitrarily complex expressions
lassign [split $index -] A B
if {$A eq "end"} {
set index [expr {$max - $B}]
} else {
set index [expr {$A - $B}]
}
}
} else {
#May be something like +2 or -0 which braced expr can hanle
#we would like to avoid unbraced expr here - as we're potentially dealing with ranges that may come from external sources.
if {[catch {expr {$index}} index]} {
#could be end+x - but we don't want out of bounds to be valid
#set it to something that the final bounds expr test can deal with
set index Inf
"*+*" {
lassign [split $index +] A B
if {$A eq "end"} {
#review - this will just result in out of bounds error in final test - as desired
#By calculating here - we will see the result in the error message - but it's probably not particularly useful - as we don't really need end+ support at all.
set index [expr {$max + $B}]
} else {
set index [expr {$A + $B}]
}
}
default {
#May be something like +2 or -0 which braced expr can hanle
#we would like to avoid unbraced expr here - as we're potentially dealing with ranges that may come from external sources.
if {[catch {expr {$index}} index]} {
#could be end+x - but we don't want out of bounds to be valid
#set it to something that the final bounds expr test can deal with
set index Inf
}
}
}
}
@ -1308,6 +1314,7 @@ namespace eval punk::fileline {
set bomenc ""
set is_reliabletxt 0 ;#see http://reliabletxt.com - only utf-8 with bom, utf-16be, utf-16le, utf-32be supported as at 2024
set startdata 0
#todo switch -glob
if {[string match "efbbbf*" $maybe_bom]} {
set bomid utf-8
set bomenc utf-8

508
src/modules/punk/lib-999999.0a1.0.tm

@ -213,12 +213,16 @@ namespace eval punk::lib {
set resultlist [list]
if {[string tolower $opt_case] eq "upper"} {
set spec X
} elseif {[string tolower $opt_case] eq "lower"} {
set spec x
} else {
error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower"
switch -- [string tolower $opt_case] {
upper {
set spec X
}
lower {
set spec x
}
default {
error "[namespace current]::dec2hex unknown value '$opt_case' for -case expected upper|lower"
}
}
set fmt "%${opt_width}.${opt_width}ll${spec}"
@ -529,6 +533,81 @@ namespace eval punk::lib {
return $answer
}
#like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter.
proc indent {text {prefix " "}} {
set result [list]
foreach line [split $text \n] {
if {[string trim $line] eq ""} {
lappend result ""
} else {
lappend result $prefix[string trimright $line]
}
}
return [join $result \n]
}
proc undent {text} {
if {$text eq ""} {
return ""
}
set lines [split $text \n]
set nonblank [list]
foreach ln $lines {
if {[string trim $ln] eq ""} {
continue
}
lappend nonblank $ln
}
set lcp [longestCommonPrefix $nonblank]
if {$lcp eq ""} {
return $text
}
regexp {^([\t ]*)} $lcp _m lcp
if {$lcp eq ""} {
return $text
}
set len [string length $lcp]
set result [list]
foreach ln $lines {
if {[string trim $ln] eq ""} {
lappend result ""
} else {
lappend result [string range $ln $len end]
}
}
return [join $result \n]
}
#A version of textutil::string::longestCommonPrefixList
proc longestCommonPrefix {items} {
if {[llength $items] <= 1} {
return [lindex $items 0]
}
set items [lsort $items[unset items]]
set min [lindex $items 0]
set max [lindex $items end]
#if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list)
#(sort order nothing to do with length - e.g min may be longer than max)
if {[string length $min] > [string length $max]} {
set temp $min
set min $max
set max $temp
}
set n [string length $min]
set prefix ""
set i -1
while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} {
append prefix $c
}
return $prefix
}
#test example of the technique - not necessarily particularly useful as a function, except maybe for brevity/clarity. todo - test if inlined version gives any perf advantage compared to a temp var
proc swapnumvars {namea nameb} {
upvar $namea a $nameb b
set a [expr {$a ^ $b}]
set b [expr {$a ^ $b}]
set a [expr {$a ^ $b}]
}
#e.g linesort -decreasing $data
proc linesort {args} {
#*** !doctools
@ -561,7 +640,6 @@ namespace eval punk::lib {
} elseif {[llength $args] == 1} {
set joinchar "\n"
set lines [lindex $args 0]
} else {
error "list_as_lines usage: list_as_lines ?-joinchar <char>? <linelist>"
}
@ -632,43 +710,56 @@ namespace eval punk::lib {
-ansiresets 0\
]
dict for {o v} $arglist {
if {$o ni {-block -line -commandprefix -ansiresets}} {
error "linelist: Unrecognized option '$o' usage:$usage"
switch -- $o {
-block - -line - -commandprefix - -ansiresets {}
default {
error "linelist: Unrecognized option '$o' usage:$usage"
}
}
}
set opts [dict merge $defaults $arglist]
# -- --- --- --- --- ---
set opt_block [dict get $opts -block]
set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty]
foreach bo $opt_block {
if {$bo ni $known_blockopts} {
error "linelist: unknown -block option value: $bo known values: $known_blockopts"
if {[llength $opt_block]} {
foreach bo $opt_block {
switch -- $bo {
trimhead - trimtail - triminner - trimall - trimhead1 - trimtail1 - collateempty {}
default {
set known_blockopts [list trimhead trimtail triminner trimall trimhead1 trimtail1 collateempty]
error "linelist: unknown -block option value: $bo known values: $known_blockopts"
}
}
}
}
#normalize certain combos
if {[set posn [lsearch $opt_block trimhead1]] >=0 && "trimhead" in $opt_block} {
set opt_block [lreplace $opt_block $posn $posn]
}
if {[set posn [lsearch $opt_block trimtail1]] >=0 && "trimtail" in $opt_block} {
set opt_block [lreplace $opt_block $posn $posn]
}
if {"trimall" in $opt_block} {
#no other block options make sense in combination with this
set opt_block [list "trimall"]
#normalize certain combos
if {[set posn [lsearch $opt_block trimhead1]] >=0 && "trimhead" in $opt_block} {
set opt_block [lreplace $opt_block $posn $posn]
}
if {[set posn [lsearch $opt_block trimtail1]] >=0 && "trimtail" in $opt_block} {
set opt_block [lreplace $opt_block $posn $posn]
}
if {"trimall" in $opt_block} {
#no other block options make sense in combination with this
set opt_block [list "trimall"]
}
#TODO
if {"triminner" in $opt_block } {
error "linelist -block triminner not implemented - sorry"
}
}
#TODO
if {"triminner" in $opt_block } {
error "linelist -block triminner not implemented - sorry"
}
# -- --- --- --- --- ---
set opt_line [dict get $opts -line]
set known_lineopts [list trimline trimleft trimright]
foreach lo $opt_line {
if {$lo ni $known_lineopts} {
error "linelist: unknown -line option value: $lo known values: $known_lineopts"
}
switch -- $lo {
trimline - trimleft - trimright {}
default {
set known_lineopts [list trimline trimleft trimright]
error "linelist: unknown -line option value: $lo known values: $known_lineopts"
}
}
}
#normalize trimleft trimright combo
if {"trimleft" in $opt_line && "trimright" in $opt_line} {
@ -777,9 +868,15 @@ namespace eval punk::lib {
set linelist $transformed
} else {
#INLINE punk::ansi::codetype::is_sgr_reset
#regexp {\x1b\[0*m$} $code
set re_is_sgr_reset {\x1b\[0*m$}
#INLINE punk::ansi::codetype::is_sgr
#regexp {\033\[[0-9;:]*m$} $code
set re_is_sgr {\x1b\[[0-9;:]*m$}
foreach ln $linelist {
set is_replay_pure_reset [punk::ansi::codetype::is_sgr_reset $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable
#set is_replay_pure_reset [regexp {\x1b\[0*m$} $replaycodes] ;#only looks at tail code - but if tail is pure reset - any prefix is ignorable
set ansisplits [punk::ansi::ta::split_codes_single $ln]
if {[llength $ansisplits]<= 1} {
@ -819,6 +916,11 @@ namespace eval punk::lib {
set codestack [list $code]
} else {
if {[punk::ansi::codetype::is_sgr $code]} {
#todo - proper test of each code - so we only take latest background/foreground etc.
#requires handling codes with varying numbers of parameters.
#basic simplification - remove straight dupes.
set dup_posns [lsearch -all -exact $codestack $code] ;#!must use -exact as codes have square brackets which are interpreted as glob chars.
set codestack [lremove $codestack {*}$dup_posns]
lappend codestack $code
} ;#else gx0 or other code - we don't want to stack it with SGR codes
}
@ -834,7 +936,9 @@ namespace eval punk::lib {
}
}
set newreplay [join $codestack ""]
#set newreplay [join $codestack ""]
set newreplay [punk::ansi::codetype::sgr_merge {*}$codestack]
if {$line_has_sgr && $newreplay ne $replaycodes} {
#adjust if it doesn't already does a reset at start
if {[punk::ansi::codetype::has_sgr_leadingreset $newreplay]} {
@ -930,7 +1034,6 @@ namespace eval punk::lib {
set rawargs [lindex $ov_vals 1]
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi -ARGTYPE]
set optspec_defaults [dict create\
-optional 1\
-allow_ansi 1\
@ -960,8 +1063,8 @@ namespace eval punk::lib {
foreach ln $records {
set trimln [string trim $ln]
if {$trimln eq "" || [string index $trimln 0] eq "#"} {
continue
switch -- [string index $trimln 0] {
"" - # {continue}
}
set argname [lindex $trimln 0]
set argspecs [lrange $trimln 1 end]
@ -977,8 +1080,12 @@ namespace eval punk::lib {
set is_opt 0
}
dict for {spec specval} $argspecs {
if {$spec ni $known_argspecs} {
error "punk::lib::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
switch -- $spec {
-default - -type - -range - -choices - -nocase - -optional - -multiple - -validate_without_ansi - -allow_ansi - -strip_ansi - -ARGTYPE {}
default {
set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi -ARGTYPE]
error "punk::lib::opts_values - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argspecs"
}
}
}
set argspecs [dict merge $optspec_defaults $argspecs]
@ -1194,15 +1301,20 @@ namespace eval punk::lib {
set allow_ansi 0
}
if {!$allow_ansi} {
foreach e $vlist {
if {[punk::ansi::ta::detect $e]} {
error "Option $o for $caller contains ansi - but -allow_ansi is false. Received: '$e'"
}
#detect should work fine directly on whole list
if {[punk::ansi::ta::detect $vlist]} {
error "Option $o for $caller contains ansi - but -allow_ansi is false. Received: [ansistring VIEW $vlist]"
}
#foreach e $vlist {
# if {[punk::ansi::ta::detect $e]} {
# error "Option $o for $caller contains ansi - but -allow_ansi is false. Received: '$e'"
# }
#}
}
set vlist_check [list]
foreach e $vlist {
#could probably stripansi entire list safely in one go? - review
if {$validate_without_ansi} {
lappend vlist_check [punk::ansi::stripansi $e]
} else {
@ -1224,65 +1336,94 @@ namespace eval punk::lib {
if {!$is_default} {
if {[dict exists $arg_info $o -type]} {
set type [dict get $arg_info $o -type]
if {[string tolower $type] in {int integer double}} {
if {[string tolower $type] in {int integer}} {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
set ltype [string tolower $type]
switch -- $type {
int -
integer -
double {
switch -- $ltype {
int -
integer {
foreach e $vlist e_check $vlist_check {
if {![string is integer -strict $e_check]} {
error "Option $o for $caller requires type 'integer'. Received: '$e'"
}
}
}
}
} elseif {[string tolower $type] in {double}} {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
double {
foreach e $vlist e_check $vlist_check {
if {![string is double -strict $e_check]} {
error "Option $o for $caller requires type 'double'. Received: '$e'"
}
}
}
}
}
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
#todo - small-value double comparisons with error-margin? review
if {[dict exists $arg_info $o -range]} {
lassign [dict get $arg_info $o -range] low high
foreach e $vlist e_check $vlist_check {
if {$e_check < $low || $e_check > $high} {
error "Option $o for $caller must be between $low and $high. Received: '$e'"
}
}
}
}
} elseif {[string tolower $type] in {bool boolean}} {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
} elseif {[string tolower $type] in {alnum alpha ascii control digit graph lower print punct space upper wordchar xdigit}} {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
bool -
boolean {
foreach e $vlist e_check $vlist_check {
if {![string is boolean -strict $e_check]} {
error "Option $o for $caller requires type 'boolean'. Received: '$e'"
}
}
}
} elseif {[string tolower $type] in {file directory existingfile existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
alnum -
alpha -
ascii -
control -
digit -
graph -
lower -
print -
punct -
space -
upper -
wordchar -
xdigit {
foreach e $vlist e_check $vlist_check {
if {![string is [string tolower $type] $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e'"
}
}
}
if {[string tolower $type] in {existingfile}} {
file -
directory -
existingfile -
existingdirectory {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
if {!([string length $e_check]>0 && ![regexp {[\"*?<>\;]} $e_check])} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which doesn't look like it could be a file or directory"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
if {[string tolower $type] in {existingfile}} {
foreach e $vlist e_check $vlist_check {
if {![file exists $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing file"
}
}
} elseif {[string tolower $type] in {existingdirectory}} {
foreach e $vlist e_check $vlist_check {
if {![file isdirectory $e_check]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not an existing directory"
}
}
}
}
} elseif {[string tolower $type] in {char character}} {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
char -
character {
foreach e $vlist e_check $vlist_check {
if {[string length != 1]} {
error "Option $o for $caller requires type '[string tolower $type]'. Received: '$e' which is not a single character"
}
}
}
}
@ -1372,6 +1513,102 @@ namespace eval punk::lib {
insert ::tcl::string::insert]
}
interp alias {} errortime {} punk::lib::errortime
proc errortime {script groupsize {iters 2}} {
#by use MAK from https://wiki.tcl-lang.org/page/How+to+Measure+Performance
set i 0
set times {}
if {$iters < 2} {set iters 2}
for {set i 0} {$i < $iters} {incr i} {
set result [uplevel [list time $script $groupsize]]
lappend times [lindex $result 0]
}
set average 0.0
set s2 0.0
foreach time $times {
set average [expr {$average + double($time)/$iters}]
}
foreach time $times {
set s2 [expr {$s2 + (($time-$average)*($time-$average) / ($iters-1))}]
}
set sigma [expr {int(sqrt($s2))}]
set average [expr int($average)]
return "$average +/- $sigma microseconds per iteration"
}
#test function to use with show_jump_tables
#todo - check if switch compilation to jump tables differs by Tcl version
proc switch_char_test {c} {
set dec [scan $c %c]
foreach t [list 1 2 3] {
switch -- $c {
x {
return [list $dec x $t]
}
y {
return [list $dec y $t]
}
z {
return [list $dec z $t]
}
}
}
#tcl 8.6/8.7 (at least)
#curlies must be unescaped and unbraced to work as literals in switch and enable it to compile to jumpTable
switch -- $c {
a {
return [list $dec a]
}
{"} {
return [list $dec dquote]
}
{[} {return [list $dec lb]}
{]} {return [list $dec rb]}
"{" {
return [list $dec lbrace]
}
"}" {
return [list $dec rbrace]
}
default {
return [list $dec $c]
}
}
}
#we are interested in seeing jumpTable line and following lines up until next line starting with "Command" or bracketed number e.g (164)
proc show_jump_tables {procname} {
set data [tcl::unsupported::disassemble proc $procname]
set result ""
set in_jt 0
foreach ln [split $data \n] {
set tln [string trim $ln]
if {!$in_jt} {
if {[string match *jumpTable* $ln]} {
append result $ln \n
set in_jt 1
}
} else {
if {[string match Command* $tln] || [string match "(*) *" $tln]} {
set in_jt 0
} else {
append result $ln \n
}
}
}
return $result
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::lib ---}]
}
@ -1480,49 +1717,56 @@ namespace eval punk::lib::system {
} ;# set escaped 0 at end
set p [lindex $innerpartials end]
if {$escaped == 0} {
if {$c eq {"}} {
if {![info complete ${p}]} {
lappend waiting {"}
lappend innerpartials ""
} else {
if {[lindex $waiting end] eq {"}} {
#this quote is endquote
set waiting [lrange $waiting 0 end-1]
set innerpartials [lrange $innerpartials 0 end-1]
#NOTE - curly braces as switch arm keys must be unescaped and balanced. (escapes stop byte-compilation to jumpTable for switch statements for tcl8.6/8.7 at least)
switch -- $c {
{"} {
if {![info complete ${p}]} {
lappend waiting {"}
lappend innerpartials ""
} else {
if {![info complete ${p}$c]} {
lappend waiting {"}
lappend innerpartials ""
if {[lindex $waiting end] eq {"}} {
#this quote is endquote
set waiting [lrange $waiting 0 end-1]
set innerpartials [lrange $innerpartials 0 end-1]
} else {
set p ${p}${c}
lset innerpartials end $p
if {![info complete ${p}$c]} {
lappend waiting {"}
lappend innerpartials ""
} else {
set p ${p}${c}
lset innerpartials end $p
}
}
}
}
} elseif {$c eq "\["} {
if {![info complete ${p}$c]} {
lappend waiting "\]"
lappend innerpartials ""
} else {
set p ${p}${c}
lset innerpartials end $p
{[} {
if {![info complete ${p}$c]} {
lappend waiting "\]"
lappend innerpartials ""
} else {
set p ${p}${c}
lset innerpartials end $p
}
}
} elseif {$c eq "\{"} {
if {![info complete ${p}$c]} {
lappend waiting "\}"
lappend innerpartials ""
} else {
set p ${p}${c}
lset innerpartials end $p
"{" {
if {![info complete ${p}$c]} {
lappend waiting "\}"
lappend innerpartials ""
} else {
set p ${p}${c}
lset innerpartials end $p
}
}
} else {
set waitingfor [lindex $waiting end]
if {$c eq "$waitingfor"} {
set waiting [lrange $waiting 0 end-1]
set innerpartials [lrange $innerpartials 0 end-1]
} else {
set p ${p}${c}
lset innerpartials end $p
"}" -
default {
set waitingfor [lindex $waiting end]
if {$c eq "$waitingfor"} {
set waiting [lrange $waiting 0 end-1]
set innerpartials [lrange $innerpartials 0 end-1]
} else {
set p ${p}${c}
lset innerpartials end $p
}
}
}
} else {
@ -1534,12 +1778,18 @@ namespace eval punk::lib::system {
}
set incomplete [list]
foreach w $waiting {
if {$w eq {"}} {
lappend incomplete $w
} elseif {$w eq "\]"} {
lappend incomplete "\["
} elseif {$w eq "\}"} {
lappend incomplete "\{"
#to be treated as literals - curly braces must be unescaped here - and balanced - hence the left-curly empty arm.
switch -- $w {
{"} {
lappend incomplete $w
}
{]} {
lappend incomplete "\["
}
"{" {}
"}" {
lappend incomplete "\{"
}
}
}
set debug 0
@ -1595,7 +1845,7 @@ namespace eval punk::lib::system {
}
return $incomplete
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::lib::system ---}]
}

2
src/modules/punk/lib-buildversion.txt

@ -1,3 +1,3 @@
0.1.0
0.1.1
#First line must be a semantic version number
#all other lines are ignored.

166
src/modules/punk/mix/base-0.1.tm

@ -494,9 +494,13 @@ namespace eval punk::mix::base {
if {[catch {file type $path} ftype]} {
return [list cksum "<PATHNOTFOUND>" opts $opts]
}
if {$ftype ni [list file directory]} {
#review - links?
error "cksum_path error file type '$ftype' not supported"
#review - links?
switch -- $ftype {
file - directory {}
default {
error "cksum_path error file type '$ftype' not supported"
}
}
@ -512,54 +516,65 @@ namespace eval punk::mix::base {
set opt_cksum_meta [dict get $opts -cksum_meta]
set opt_use_tar [dict get $opts -cksum_usetar]
if {$ftype eq "file"} {
if {$opt_use_tar eq "auto"} {
if {$opt_cksum_meta eq "1"} {
set opt_use_tar 1
} else {
#prefer no tar if meta not required - faster/simpler
#meta == auto or 0
set opt_cksum_meta 0
set opt_use_tar 0
}
} elseif {$opt_use_tar eq "0"} {
if {$opt_cksum_meta eq "1"} {
puts stderr "cksum_path doesn't yet support a non-tar cksum with metadata for a file"
return [list error unsupported_meta_without_tar cksum "<ERR>" opts $opts]
} else {
#meta == auto or 0
set opt_cksum_meta 0
}
} else {
#tar == 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar cksum without metadata for a file"
return [list error unsupported_tar_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
switch -- $ftype {
file {
switch -- $opt_use_tar {
auto {
if {$opt_cksum_meta eq "1"} {
set opt_use_tar 1
} else {
#prefer no tar if meta not required - faster/simpler
#meta == auto or 0
set opt_cksum_meta 0
set opt_use_tar 0
}
}
0 {
if {$opt_cksum_meta eq "1"} {
puts stderr "cksum_path doesn't yet support a non-tar cksum with metadata for a file"
return [list error unsupported_meta_without_tar cksum "<ERR>" opts $opts]
} else {
#meta == auto or 0
set opt_cksum_meta 0
}
}
default {
#tar == 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar cksum without metadata for a file"
return [list error unsupported_tar_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
}
}
}
}
} elseif {$ftype eq "directory"} {
if {$opt_use_tar eq "auto"} {
if {$opt_cksum_meta in [list "auto" "1"]} {
set opt_use_tar 1
set opt_cksum_meta 1
} else {
puts stderr "cksum_path doesn't yet support a content-only cksum of a folder structure. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto"
return [list error unsupported_directory_cksum_without_meta cksum "<ERR>" opts $opts]
}
} elseif {$opt_use_tar eq "0"} {
puts stderr "cksum_path doesn't yet support a cksum of a folder structure without tar. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_directory_cksum_without_tar cksum "<ERR>" opts $opts]
} else {
#tar 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar checksum of a folder structure without metadat. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
directory {
switch -- $opt_use_tar {
auto {
if {$opt_cksum_meta in [list "auto" "1"]} {
set opt_use_tar 1
set opt_cksum_meta 1
} else {
puts stderr "cksum_path doesn't yet support a content-only cksum of a folder structure. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto"
return [list error unsupported_directory_cksum_without_meta cksum "<ERR>" opts $opts]
}
}
0 {
puts stderr "cksum_path doesn't yet support a cksum of a folder structure without tar. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_directory_cksum_without_tar cksum "<ERR>" opts $opts]
}
default {
#tar 1
if {$opt_cksum_meta eq "0"} {
puts stderr "cksum_path doesn't yet support a tar checksum of a folder structure without metadat. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1 or auto with -cksum_usetar 1 or auto"
return [list error unsupported_without_meta cksum "<ERR>" opts $opts]
} else {
#meta == auto or 1
set opt_cksum_meta 1
}
}
}
}
}
@ -578,29 +593,36 @@ namespace eval punk::mix::base {
puts stderr "cksum_path doesn't yet support cksum of entire volume. (todo)"
return [list error unsupported_path opts $opts]
}
if {$opt_cksum_algorithm eq "sha1"} {
package require sha1
set cksum_command [list sha1::sha1 -hex -file]
} elseif {$opt_cksum_algorithm in [list "sha2" "sha256"]} {
package require sha256
set cksum_command [list sha2::sha256 -hex -file]
} elseif {$opt_cksum_algorithm eq "md5"} {
package require md5
set cksum_command [list md5::md5 -hex -file]
} elseif {$opt_cksum_algorithm eq "cksum"} {
package require cksum ;#tcllib
set cksum_command [list crc::cksum -format 0x%X -file]
} elseif {$opt_cksum_algorithm eq "adler32"} {
set cksum_command [list cksum_adler32_file]
} elseif {$opt_cksum_algorithm in [list "sha3" "sha3-256"]} {
#todo - replace with something that doesn't call another process
#set cksum_command [list apply {{file} {lindex [exec fossil sha3sum -256 $file] 0}}]
set cksum_command [list $sha3_implementation 256]
} elseif {$opt_cksum_algorithm in [list "sha3-224" "sha3-384" "sha3-512"]} {
set bits [lindex [split $opt_cksum_algorithm -] 1]
#set cksum_command [list apply {{bits file} {lindex [exec fossil sha3sum -$bits $file] 0}} $bits]
set cksum_command [list $sha3_implementation $bits]
switch -- $opt_cksum_algorithm {
sha1 {
package require sha1
set cksum_command [list sha1::sha1 -hex -file]
}
sha2 - sha256 {
package require sha256
set cksum_command [list sha2::sha256 -hex -file]
}
md5 {
package require md5
set cksum_command [list md5::md5 -hex -file]
}
cksum {
package require cksum ;#tcllib
set cksum_command [list crc::cksum -format 0x%X -file]
}
adler32 {
set cksum_command [list cksum_adler32_file]
}
sha3 - sha3-256 {
#todo - replace with something that doesn't call another process
#set cksum_command [list apply {{file} {lindex [exec fossil sha3sum -256 $file] 0}}]
set cksum_command [list $sha3_implementation 256]
}
sha3-224 - sha3-384 - sah3-512 {
set bits [lindex [split $opt_cksum_algorithm -] 1]
#set cksum_command [list apply {{bits file} {lindex [exec fossil sha3sum -$bits $file] 0}} $bits]
set cksum_command [list $sha3_implementation $bits]
}
}
set cksum ""

1041
src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm

File diff suppressed because it is too large Load Diff

65
src/modules/punk/ns-999999.0a1.0.tm

@ -880,12 +880,14 @@ namespace eval punk::ns {
foreach nsdict $with_results {
dict set opts -nsdict $nsdict
set block [get_nslist {*}$opts]
if {[string first \n $block] < 0} {
#single line
set width [textblock::width [list $block]]
} else {
set width [textblock::width $block]
}
#if {[string first \n $block] < 0} {
# #single line
# set width [textblock::width [list $block]]
#} else {
# set width [textblock::width $block]
#}
set width [textblock::width $block]
#if multiple results or if there is only 1 result - but we are path-globbing - then we need to show location
if {$count_with_results > 1 || [regexp {[*?]} [nsprefix $ns_absolute]]} {
append output \n [dict get $nsdict location]
@ -1356,8 +1358,16 @@ namespace eval punk::ns {
proc corp {path} {
#thanks to Richard Suchenwirth for the original - wiki.tcl-lang.org/page/corp
#Note: modified here to support aliases and relative/absolute name (with respect to namespace .ie ::name vs name)
if {[info exists punk::console::tabwidth]} {
set tw $::punk::console::tabwidth
} else {
set tw 8
}
set indent [string repeat " " $tw] ;#match
#set indent [string repeat " " $tw] ;#A more sensible default for code - review
if {[info exists ::auto_index($path)]} {
set body "# $::auto_index($path)\n"
set body "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set body ""
}
@ -1405,10 +1415,20 @@ namespace eval punk::ns {
}
}
if {[nsprefix $origin] ne [nsprefix [nsjoin ${targetns} $name]]} {
append body "# namespace origin $origin" \n
append body \n "${indent}#corp# namespace origin $origin"
}
append body [info body $origin]
if {$body ne "" && [string index $body end] ne "\n"} {
append body \n
}
if {![catch {package require textutil::tabify} errpkg]} {
set bodytext [info body $origin]
#punk::lib::indent preserves trailing empty lines - unlike textutil version
set bodytext [punk::lib::undent [textutil::untabify2 $bodytext $tw]]
append body [punk::lib::indent $bodytext $indent]
} else {
append body [info body $origin]
}
set argl {}
foreach a [info args $origin] {
if {[info default $origin $a def]} {
@ -1511,20 +1531,23 @@ namespace eval punk::ns {
#todo support leading solo flags such as -capture to control whether we do a static capture of local vars in the ns
if {[string tolower $pkg_or_existing_ns] in [list :: global]} {
set ns ::
set ver "";# tcl version?
} else {
if {[string match ::* $pkg_or_existing_ns]} {
if {![namespace exists $pkg_or_existing_ns]} {
set ver [package require [string range $pkg_or_existing_ns 2 end]]
switch -- [string tolower $pkg_or_existing_ns] {
"::" - global {
set ns ::
set ver "";# tcl version?
}
default {
if {[string match ::* $pkg_or_existing_ns]} {
if {![namespace exists $pkg_or_existing_ns]} {
set ver [package require [string range $pkg_or_existing_ns 2 end]]
} else {
set ver ""
}
set ns $pkg_or_existing_ns
} else {
set ver ""
set ver [package require $pkg_or_existing_ns]
set ns ::$pkg_or_existing_ns
}
set ns $pkg_or_existing_ns
} else {
set ver [package require $pkg_or_existing_ns]
set ns ::$pkg_or_existing_ns
}
}
if {[namespace exists $ns]} {

24
src/modules/punk/path-999999.0a1.0.tm

@ -126,18 +126,18 @@ namespace eval punk::path {
if {[string range $seg end end] eq "/"} {
set seg [string range $seg 0 end-1] ;# e.g c:/ -> c: / -> "" so that join at end doesn't double up
}
if {$seg eq "*"} {
lappend pats {[^/]*}
} elseif {$seg eq "**"} {
lappend pats {.*}
} else {
set seg [string map [list {^ {\^} $ {\$} [} {\[} ( {\(} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters in the input as literals
set seg [string map [list . {[.]}] $seg]
if {[regexp {[*?]} $seg]} {
set pat [string map [list ** {.*} * {[^/]*} ? {[^/]}] $seg]
lappend pats "$pat"
} else {
lappend pats "$seg"
switch -- $seg {
* {lappend pats {[^/]*}}
** {lappend pats {.*}}
default {
set seg [string map [list {^ {\^} $ {\$} [} {\[} ( {\(} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters in the input as literals
set seg [string map [list . {[.]}] $seg]
if {[regexp {[*?]} $seg]} {
set pat [string map [list ** {.*} * {[^/]*} ? {[^/]}] $seg]
lappend pats "$pat"
} else {
lappend pats "$seg"
}
}
}
}

362
src/modules/punk/repl-0.1.tm

@ -475,6 +475,7 @@ proc ::unknown args {
puts stderr ">>>scriptrun_commandlist: $commandlist"
#ansiwrap for testing
#set id_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}]
uplevel #0 [list ::catch [list ::shellfilter::run $commandlist -teehandle punk -inbuffering line -outbuffering none ] ::tcl::UnknownResult ::tcl::UnknownOptions]
@ -655,8 +656,11 @@ proc repl::start {inchan args} {
# ---
variable editbuf
variable editbuf_list ;#command history
variable editbuf_lineindex_submitted
variable editbuf_linenum_submitted
# ---
catch {
set punk::console::tabwidth [punk::console::get_tabstop_apparent_width]
}
variable running
variable reading
@ -677,7 +681,7 @@ proc repl::start {inchan args} {
# ---
set editbuf [punk::repl::class::class_editbuf new {}]
lappend editbuf_list $editbuf ;#current editbuf is always in the history
set editbuf_lineindex_submitted -1
set editbuf_linenum_submitted 0
# ---
if {$::punk::console::ansi_wanted == 2} {
@ -1025,16 +1029,12 @@ proc repl::screen_needs_clearance {} {
return 1
}
lassign $last_char_info c what why
if {$what in [list "stdout" "stderr" "stdout/stderr"]} {
return 1
}
if {$c eq "\n"} {
return 0
} else {
return 1
switch -- $what {
stdout - stderr - stdout/stderr {
return 1
}
}
return [expr {$c ne "\n"}]
}
namespace eval repl {
@ -1115,6 +1115,10 @@ namespace eval punk::repl::class {
#we should merge first row of newparts differently in case our chunks split a grapheme-combination?
#
if {$o_cursor_row < 1} {
puts stderr "add_chunk warning cursor_row < 1 - changing to minimum value 1"
set o_cursor_row 1
}
set cursor_row_idx [expr {$o_cursor_row -1}]
set activeline [lindex $o_rendered_lines $cursor_row_idx]
set new0 [lindex $newparts 0]
@ -1124,7 +1128,7 @@ namespace eval punk::repl::class {
#append combined \n
append new0 \n
}
set underlay $activeline
set underlay [punk::ansi::stripansi $activeline]
set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}]
if {$o_cursor_col > $line_nextchar_col} {
set o_cursor_col $line_nextchar_col
@ -1156,33 +1160,28 @@ namespace eval punk::repl::class {
set cursor_row_idx [expr {$o_cursor_row-1}]
lset o_rendered_lines $cursor_row_idx $result
if {[string is integer -strict $cmove]} {
#cmove - positive,negative or zero
if {$cmove == 0} {
#set nextrow [expr {$o_cursor_row + 1}]
#set o_cursor_col 1
} elseif {$cmove == 1} {
#check for overflow_right and unapplied
#leave cursor_column
} elseif {$cmove >= 1} {
}
} else {
# =<int> - absolute
set nextrow [string range $cmove 1 end]
}
if {$insert_lines_below > 0} {
for {set i 0} {$i < $insert_lines_below} {incr i} {
set nextrow $cmove
#if {$insert_lines_below > 0} {
# for {set i 0} {$i < $insert_lines_below} {incr i} {
# lappend o_rendered_lines ""
# }
# set o_cursor_col 1
#}
if {$insert_lines_below == 1} {
if {[string length $overflow_right]} {
lappend o_rendered_lines $overflow_right
set o_cursor_col [expr {[punk::ansi::printing_length $overflow_right] +1}]
} else {
lappend o_rendered_lines ""
set o_cursor_col 1
}
set o_cursor_col 1
}
if {$insert_lines_above > 0} {
} elseif {$insert_lines_above == 1} {
#for {set i 0} {$i < $insert_lines_above} {incr i} {
# set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""]
# incr nextrow -1
#}
#set o_cursor_col 1
set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""]
set o_cursor_col 1
}
set o_cursor_row $nextrow
@ -1293,23 +1292,110 @@ namespace eval punk::repl::class {
return [llength $o_rendered_lines]
}
method line {idx} {
if {[string is integer -strict $idx]} {
incr idx -1
}
return [lindex $o_rendered_lines $idx]
}
method lines {args} {
if {![llength $args]} {
set range [list 0 end]
} else {
set range $args
switch -- [llength $args] {
0 {return $o_rendered_lines}
1 {
set idx [lindex $args 0]
if {[string is integer -strict $idx]} {
incr idx -1
}
return [list [lindex $o_rendered_lines $idx]]
}
2 {
lassign $args idx1 idx2
if {[string is integer -strict $idx1]} {
incr idx1 -1
}
if {[string is integer -strict $idx2]} {
incr idx2 -1
}
return [lrange $o_rendered_lines $idx1 $idx2]
}
default {error "lines expected 0,1 or 2 indices"}
}
return [lrange $o_rendered_lines {*}$range]
}
#min value 1?
method view_lines {} {
set result ""
foreach ln $o_rendered_lines {
append result $ln \n
#todo - index base???
method lines_numbered {args} {
#build a paired list so we don't have to do various calcs on end+ end- etc checking llength
#punk::range will use lseq if available - else use it's own slower code
set max [llength $o_rendered_lines] ;#assume >=1
set nums [punk::range 1 $max]
set numline_list [list]
foreach n $nums ln $o_rendered_lines {
lappend numline_list [list $n $ln]
}
switch -- [llength $args] {
0 {return $numline_list}
1 {return [lindex $numline_list [lindex $args 0]]}
2 {return [lrange $numline_list {*}$args]}
default {error "lines expected 0,1 or 2 indices"}
}
return $result
}
#1-based
method delete_line {linenum} {
error "unimplemented"
if {$linenum eq "end"} {
set linenum [llength $o_rendered_lines]
}
if {![string is integer -strict $linenum]} {
error "delete_line accepts only end or an integer from 1 to linecount"
}
if {$linenum == 0} {
error "minimum line is 1"
}
set o_rendered_lines [lreplace $o_rendered_lines $index $index]
}
#clear data from last line only
method clear_tail {} {
set o_cursor_row [llength $o_rendered_lines]
set o_cursor_col 1
lset o_rendered_lines end ""
}
#1-based
method view_lines {args} {
set llist [my lines {*}$args]
return [join $llist \n]
}
method view_lines_numbered {args} {
set ANSI_linenum [a+ green]
set RST [a]
set llist [my lines_numbered {*}$args]
set nums [lsearch -all -inline -index 0 -subindices $llist *]
lset nums $o_cursor_row-1 "[a+ bold underline]$o_cursor_row${RST}$ANSI_linenum"
set lines [lsearch -all -inline -index 1 -subindices $llist *]
set cursorline [lindex $lines $o_cursor_row-1]
set charindex_at_cursor [ansistring COLUMNINDEX $cursorline $o_cursor_col]
if {$charindex_at_cursor ne ""} {
lassign [ansistring INDEXCOLUMNS $cursorline $charindex_at_cursor] col0 col1
#we now have the column extents of the possibly double-wide character at the cursor
#we can apply ansi just to those columns using a transparent overtype
set prefix [string repeat " " [expr {$col0 -1}]]
set linecols [punk::ansi::printing_length $cursorline]
set suffix [string repeat " " [expr {$linecols -$col1}]]
set char_at_cursor [ansistring index $cursorline $charindex_at_cursor] ;#this is the char with appropriate ansireset codes
set rawchar [punk::ansi::stripansi $char_at_cursor]
if {$rawchar eq " "} {
set charhighlight "[punk::ansi::a+ White]_[a]"
} else {
set charhighlight [punk::ansi::a+ reverse]$char_at_cursor[a]
}
set cursorline [overtype::renderline -transparent 1 -insert_mode 0 -overflow 0 $cursorline $prefix$charhighlight$suffix]
lset lines $o_cursor_row-1 $cursorline
}
set numcol "$ANSI_linenum[join $nums \n][a]"
set linecol [join $lines \n]
return [textblock::join $numcol " " $linecol]
}
method debugview_lines {} {
set result ""
@ -1338,10 +1424,16 @@ namespace eval punk::repl::class {
set lastchunk [lindex $o_chunk_list end]
set parts [punk::ansi::ta::split_codes_single $lastchunk]
set lastcode [lindex $parts end-1]
return [ansistring VIEW -lf 1 $lastcode]
return $lastcode
#return [ansistring VIEW -lf 1 $lastcode]
}
method chunks {} {
return $o_chunk_list
method chunks {args} {
switch -- [llength $args] {
0 {return $o_chunk_list}
1 {return [lindex $o_chunk_list [lindex $args 0]]}
2 {return [lrange $o_chunk_list {*}$args]}
default {error "chunks expected 0,1 or 2 arguments (index or range)"}
}
}
method view_chunks {} {
set result ""
@ -1441,6 +1533,8 @@ proc repl::repl_handler {inputchan prompt_config} {
}
if {!$rawmode} {
#stdin with line-mode readable events (at least on windows for Tcl 8.7a6 to 9.0a) can get stuck with bytes pending when input longer than 100chars - even though there is a linefeed further on than that.
#This potentially affects a reasonable number of Tcl8.7 kit/tclsh binaries out in the wild.
#see bug https://core.tcl-lang.org/tcl/tktview/bda99f2393 (gets stdin problem when non-blocking - Windows)
@ -1476,7 +1570,7 @@ proc repl::repl_handler {inputchan prompt_config} {
if {$chunksize > 0} {
if {[string index $chunk end] eq "\n"} {
lappend stdinlines $waitingchunk[string range $chunk 0 end-1]
punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]"
#punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]"
if {![chan eof $inputchan]} {
repl_handler_restorechannel $inputchan $original_input_conf
@ -1512,9 +1606,9 @@ proc repl::repl_handler {inputchan prompt_config} {
set chunksize [string length $chunk]
# -- --- ---
if {$chunksize > 0} {
punk::console::cursorsave_move_emitblock_return 35 120 "chunk: [ansistring VIEW -lf 1 "...[string range $chunk end-10 end]"]"
#punk::console::cursorsave_move_emitblock_return 35 120 "chunk: [ansistring VIEW -lf 1 "...[string range $chunk end-10 end]"]"
set ln $chunk ;#temp
punk::console::cursorsave_move_emitblock_return 25 30 [textblock::frame -title line "[a+ green]$waitingchunk[a][a+ red][ansistring VIEW -lf 1 $ln][a+ green]pending:[chan pending input stdin][a]"]
#punk::console::cursorsave_move_emitblock_return 25 30 [textblock::frame -title line "[a+ green]$waitingchunk[a][a+ red][ansistring VIEW -lf 1 $ln][a+ green]pending:[chan pending input stdin][a]"]
if {[string index $ln end] eq "\n"} {
lappend stdinlines [string range $ln 0 end-1]
incr lc
@ -1549,6 +1643,13 @@ proc repl::repl_handler {inputchan prompt_config} {
repl_handler_restorechannel $inputchan $original_input_conf
}
uplevel #0 [list repl::repl_process_data $inputchan raw-read $chunk [list] $prompt_config]
while {[join $input_chunks_waiting($inputchan)] ne ""} {
#puts "...[llength $input_chunks_waiting($inputchan)]"
set wchunks $input_chunks_waiting($inputchan)
set ch [lindex $wchunks 0]
set input_chunks_waiting($inputchan) [lrange $wchunks 1 end]
uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $ch [list] $prompt_config]
}
}
}
@ -1570,11 +1671,17 @@ proc repl::repl_handler {inputchan prompt_config} {
}
set in_repl_handler [list]
}
proc repl::editbuf {args} {
variable editbuf
$editbuf {*}$args
}
interp alias {} editbuf {} ::repl::editbuf
proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
variable loopinstance
variable loopcomplete
incr loopinstance
set moredata 0
upvar ::punk::console::input_chunks_waiting input_chunks_waiting
try {
variable prompt_reset_flag
@ -1591,7 +1698,8 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
# ---
variable editbuf
variable editbuf_list
variable editbuf_lineindex_submitted
variable editbuf_linenum_submitted
# ---
variable readingchunk
variable running
@ -1672,10 +1780,11 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
#esc or ctrl-lb
if {$chunk eq "\x1b"} {
#return
set readingchunk ""
#set readingchunk ""
set stdinlines [list "\x1b"]
set commandstr ""
set chunk ""
$editbuf clear_tail
screen_last_char_add \x1b stdin escape
break
}
@ -1696,6 +1805,9 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
after 1000 exit
return
}
if {$chunk eq "\x7f"} {
set chunk "\b\x7f"
}
#ctrl-bslash
if {$chunk eq "\x1c"} {
#try to brutally terminate process
@ -1735,7 +1847,7 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
set info [list_as_lines $lines]
}
} errM]} {
set info [textblock::frame -title [a red]error[a] $errM]
set info [textblock::frame -title "[a red]error[a]" $errM]
} else {
set info [textblock::frame -ansiborder [a+ green bold] -title "[a cyan]debugview_raw[a]" $info]
}
@ -1748,14 +1860,15 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
puts -nonewline [punk::ansi::cursor_on]
}
if {[catch {
set info [$editbuf view_lines]
#set info [$editbuf view_lines]
set info [$editbuf view_lines_numbered]
set lines [lines_as_list -ansiresets 1 $info]
if {[llength $lines] > 20} {
set lines [lrange $lines end-19 end]
set info [list_as_lines $lines]
}
}]} {
set info [textblock::frame -title [a red]error[a] $errM]
} editbuf_error]} {
set info [textblock::frame -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"]
} else {
set title "[a cyan]editbuf lines [$editbuf linecount][a]"
append title "[a+ yellow bold] col:[$editbuf cursor_column] row:[$editbuf cursor_row][a]"
@ -1771,35 +1884,67 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
}
set lines_unsubmitted [expr {[$editbuf linecount] - $editbuf_lineindex_submitted + 1}]
set lines_unsubmitted [expr {[$editbuf linecount] - $editbuf_linenum_submitted}]
#there is always one 'line' unsubmitted - although it may be the current one being built, which may be empty string
if {$lines_unsubmitted < 1} {
puts stderr "repl editbuf_lineindex_submitted out of sync with editbuf"
puts stderr "repl editbuf_linenum_submitted out of sync with editbuf"
}
set activeline_index [expr {[$editbuf linecount] -1}]
set nextsubmit_index [expr {$editbuf_lineindex_submitted + 1}]
if {$editbuf_lineindex_submitted == -1} {
#set trailing_line_index [expr {[$editbuf linecount] -1}]
set last_line_num [$editbuf linecount]
#set nextsubmit_index [expr {$editbuf_lineindex_submitted + 1}]
set nextsubmit_line_num [expr {$editbuf_linenum_submitted + 1}]
set cursor_row [$editbuf cursor_row]
set cursor_index [expr {$cursor_row -1}]
set lastansi [$editbuf last_ansi]
if {$lastansi eq "\x1b\[A"} {
if {$cursor_row > 1} {
puts -nonewline stdout "\x1b\[A"
}
} elseif {$lastansi eq "\x1b\[B"} {
puts -nonewline stdout "\x1b\[B"
}
flush stdout
set offset 3
puts -nonewline stdout [a+ cyan][punk::ansi::move_column [expr {$offset +1}]][punk::ansi::erase_eol][$editbuf line $cursor_row][a][punk::ansi::move_column [expr {$offset + [$editbuf cursor_column]}]]
#puts -nonewline stdout $chunk
flush stdout
if {[$editbuf last_char] eq "\n"} {
set linelen [punk::ansi::printing_length [$editbuf line $nextsubmit_line_num]]
puts -nonewline stdout [a+ cyan bold][punk::ansi::move_column [expr {$offset +1}]][$editbuf line $nextsubmit_line_num][a][punk::ansi::move_column [expr {$offset + $linelen +1}]]
#screen_last_char_add "\n" input inputline
puts -nonewline stdout [punk::ansi::erase_eol]\n
#puts -nonewline stdout \n
screen_last_char_add "\n" input inputline
set waiting [$editbuf line end]
if {[string length $waiting] > 0} {
set waiting [a+ yellow bold]$waiting[a]
#puts stderr "waiting $waiting"
$editbuf clear_tail
lappend input_chunks_waiting($inputchan) $waiting
}
}
if {$editbuf_linenum_submitted == 0} {
#(there is no line 0 - lines start at 1)
if {[$editbuf last_char] eq "\n"} {
lappend stdinlines [$editbuf line 0]
lappend stdinlines [$editbuf line 1]
incr lc
set editbuf_lineindex_submitted 0
set editbuf_linenum_submitted 1
}
} else {
if {$nextsubmit_index < $activeline_index} {
foreach ln [$editbuf lines $nextsubmit_index end-1] {
if {$nextsubmit_line_num < $last_line_num} {
foreach ln [$editbuf lines $nextsubmit_line_num end-1] {
lappend stdinlines $ln
incr lc
incr editbuf_lineindex_submitted
incr editbuf_linenum_submitted
}
}
}
puts -nonewline stdout $chunk
flush stdout
if {[string index $chunk end] eq "\n"} {
screen_last_char_add "\n" input inputline
}
set last_cursor_colun [$editbuf cursor_column]
} else {
#rputs stderr "->0byte read stdin"
if {[chan eof $inputchan]} {
@ -2186,22 +2331,27 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
foreach c $result {
lassign $c termchan text
if {[string length $text]} {
if {$termchan eq "result"} {
#rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text]
set h [textblock::height $text]
set promptcol [string repeat $resultprompt\n $h]
set promptcol [string range $promptcol 0 end-1]
rputs [textblock::join -- $promptcol $text]
#puts -nonewline stdout $text
} elseif {$termchan eq "resulterr"} {
rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text]
} elseif {$termchan eq "info"} {
rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text]
} else {
#rputs -nonewline $termchan $text
set chanprompt "_ "
rputs $termchan ${chanprompt}[string map [list \n "\n${chanprompt}"] $text]
switch -- $termchan {
result {
#rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text]
set h [textblock::height $text]
set promptcol [string repeat $resultprompt\n $h]
set promptcol [string range $promptcol 0 end-1]
rputs [textblock::join -- $promptcol $text]
#puts -nonewline stdout $text
}
resulterr {
rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text]
}
info {
rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text]
}
default {
#rputs -nonewline $termchan $text
set chanprompt "_ "
rputs $termchan ${chanprompt}[string map [list \n "\n${chanprompt}"] $text]
}
}
}
}
@ -2253,15 +2403,20 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
foreach c $last_run_display {
lassign $c termchan text
if {[string length $text]} {
if {$termchan eq "result"} {
rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text]
#puts -nonewline stdout $text
} elseif {$termchan eq "resulterr"} {
rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text]
} elseif {$termchan eq "info"} {
rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text]
} else {
rputs -nonewline $termchan $text
switch -- $termchan {
result {
rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text]
#puts -nonewline stdout $text
}
resulterr {
rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text]
}
info {
rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text]
}
default {
rputs -nonewline $termchan $text
}
}
}
}
@ -2318,7 +2473,8 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
#we haven't put the data following last le into commandstr - but we want to display proper completion status prior to enter being hit or more data coming in.
#this could give spurious results for large pastes where buffering chunks it in odd places.?
#it does however give sensible output for the common case of a small paste where the last line ending wasn't included
set waiting [punk::lib::system::incomplete $commandstr$readingchunk]
#set waiting [punk::lib::system::incomplete $commandstr$readingchunk]
set waiting [punk::lib::system::incomplete $commandstr[$editbuf line end]]
} else {
set waiting [punk::lib::system::incomplete $commandstr]
}
@ -2342,7 +2498,9 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
#rputs stderr "repl: no complete input line: $commandstr"
#screen_last_char_add "\n" empty empty
screen_last_char_add [string index $readingchunk end] stdinchunk stdinchunk
#screen_last_char_add [string index $readingchunk end] stdinchunk stdinchunk
screen_last_char_add [string index [$editbuf line end] end] stdinchunk stdinchunk
}
@ -2376,6 +2534,10 @@ proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} {
}
}
proc repl::completion {context ebuf} {
}
package provide punk::repl [namespace eval punk::repl {
variable version
set version 0.1

52
src/modules/punk/repo-999999.0a1.0.tm

@ -418,28 +418,36 @@ namespace eval punk::repo {
continue
}
}
if {[string match "EDITED *" $ln]} {
set path [string trim [string range $ln [string length "EDITED "] end]] ;#should handle spaced paths
dict set pathdict $path "changed"
} elseif {[string match "ADDED *" $ln]} {
set path [string trim [string range $ln [string length "ADDED "] end]]
dict set pathdict $path "new"
} elseif {[string match "DELETED *" $ln]} {
set path [string trim [string range $ln [string length "DELETED "] end]]
dict set pathdict $path "missing"
} elseif {[string match "MISSING *" $ln]} {
set path [string trim [string range $ln [string length "MISSING "] end]]
dict set pathdict $path "missing"
} elseif {[string match "EXTRA *" $ln]} {
#fossil will explicitly list files in a new folder - as opposed to git which shows just the folder
set path [string trim [string range $ln [string length "EXTRA "] end]]
dict set pathdict $path "extra"
} elseif {[string match "UNCHANGED *" $ln]} {
set path [string trim [string range $ln [string length "UNCHANGED "] end]]
dict set pathdict $path "unchanged"
} else {
#emit for now
puts stderr "unprocessed fossilstate line: $ln"
switch -glob -- $ln {
"EDITED *" {
set path [string trim [string range $ln [string length "EDITED "] end]] ;#should handle spaced paths
dict set pathdict $path "changed"
}
"ADDED *" {
set path [string trim [string range $ln [string length "ADDED "] end]]
dict set pathdict $path "new"
}
"DELETED *" {
set path [string trim [string range $ln [string length "DELETED "] end]]
dict set pathdict $path "missing"
}
"MISSING *" {
set path [string trim [string range $ln [string length "MISSING "] end]]
dict set pathdict $path "missing"
}
"EXTRA * " {
#fossil will explicitly list files in a new folder - as opposed to git which shows just the folder
set path [string trim [string range $ln [string length "EXTRA "] end]]
dict set pathdict $path "extra"
}
"UNCHANGED *" {
set path [string trim [string range $ln [string length "UNCHANGED "] end]]
dict set pathdict $path "unchanged"
}
default {
#emit for now
puts stderr "unprocessed fossilstate line: $ln"
}
}
#other entries??
}

18
src/modules/punk/timeinterval-999999.0a1.0.tm

@ -223,7 +223,8 @@ namespace eval punk::timeinterval {
while { $s2 ne $s2_test && $counter < 30 } {
set s2_diff [expr { $s2_test - $s2 } ]
puts "\ninterval_ymdhs: debug s1 $s1 s2 $s2 y $y m $m d $d h $h s $s s2_diff $s2_diff"
if { [expr { abs($s2_diff) } ] > 86399 } {
set absdiff [expr {abs($s2_diff)}]
if { $absdiff > 86399 } {
if { $s2_diff > 0 } {
incr d -1
puts "interval_ymdhs: debug, audit adjustment. decreasing 1 day to $d"
@ -231,7 +232,7 @@ namespace eval punk::timeinterval {
incr d
puts "interval_ymdhs: debug, audit adjustment. increasing 1 day to $d"
}
} elseif { [expr { abs($s2_diff) } ] > 3599 } {
} elseif { $absdiff > 3599 } {
if { $s2_diff > 0 } {
incr h -1
puts "interval_ymdhs: debug, audit adjustment. decreasing 1 hour to $h"
@ -239,7 +240,7 @@ namespace eval punk::timeinterval {
incr h
puts "interval_ymdhs: debug, audit adjustment. increasing 1 hour to $h"
}
} elseif { [expr { abs($s2_diff) } ] > 59 } {
} elseif { $absdiff > 59 } {
if { $s2_diff > 0 } {
incr mm -1
puts "interval_ymdhs: debug, audit adjustment. decreasing 1 minute to $mm"
@ -247,7 +248,7 @@ namespace eval punk::timeinterval {
incr mm
puts "interval_ymdhs: debug, audit adjustment. increasing 1 minute to $mm"
}
} elseif { [expr { abs($s2_diff) } ] > 0 } {
} elseif { $absdiff > 0 } {
if { $s2_diff > 0 } {
incr s -1
puts "interval_ymdhs: debug, audit adjustment. decreasing 1 second to $s"
@ -449,7 +450,8 @@ namespace eval punk::timeinterval {
while { $s2 ne $s2_test && $counter < 3 } {
set s2_diff [expr { $s2_test - $s2 } ]
puts "\ninterval_remains_ymdhs: debug s1 $s1 s2 $s2 y $y m $m d $d h $h s $s s2_diff $s2_diff"
if { [expr { abs($s2_diff) } ] >= 86399 } {
set absdiff [expr {abs($s2_diff)}]
if { $absdiff >= 86399 } {
if { $s2_diff > 0 } {
incr d -1
puts "interval_remains_ymdhs: debug, audit adjustment. decreasing 1 day to $d"
@ -457,7 +459,7 @@ namespace eval punk::timeinterval {
incr d
puts "interval_remains_ymdhs: debug, audit adjustment. increasing 1 day to $d"
}
} elseif { [expr { abs($s2_diff) } ] > 3599 } {
} elseif { $absdiff > 3599 } {
if { $s2_diff > 0 } {
incr h -1
puts "interval_remains_ymdhs: debug, audit adjustment. decreasing 1 hour to $h"
@ -465,7 +467,7 @@ namespace eval punk::timeinterval {
incr h
puts "interval_remains_ymdhs: debug, audit adjustment. increasing 1 hour to $h"
}
} elseif { [expr { abs($s2_diff) } ] > 59 } {
} elseif { $absdiff > 59 } {
if { $s2_diff > 0 } {
incr mm -1
puts "interval_remains_ymdhs: debug, audit adjustment. decreasing 1 minute to $mm"
@ -473,7 +475,7 @@ namespace eval punk::timeinterval {
incr mm
puts "interval_remains_ymdhs: debug, audit adjustment. increasing 1 minute to $mm"
}
} elseif { [expr { abs($s2_diff) } ] > 0 } {
} elseif { $absdiff > 0 } {
if { $s2_diff > 0 } {
incr s -1
puts "interval_remains_ymdhs: debug, audit adjustment. decreasing 1 second to $s"

35044
src/modules/punk/uc-999999.0a1.0.tm

File diff suppressed because it is too large Load Diff

3
src/modules/punk/uc-buildversion.txt

@ -0,0 +1,3 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.

124
src/modules/punkcheck-0.1.0.tm

@ -73,7 +73,11 @@ namespace eval punkcheck {
set record_list [list]
if {[file exists $punkcheck_file]} {
set tdlscript [punk::mix::util::fcat $punkcheck_file]
set record_list [punk::tdl::prettyparse $tdlscript]
if {[catch {
set record_list [punk::tdl::prettyparse $tdlscript]
} errparse]} {
error "punkcheck::load_records_from_file failed to parse '$punkcheck_file'\n error:$errparse"
}
}
return $record_list
}
@ -131,8 +135,10 @@ namespace eval punkcheck {
#get last previous that is tagged INSTALL-RECORD,MODIFY-RECORD,DELETE-RECORD
set revlist [lreverse $previous_records]
foreach rec $revlist {
if {[dict get $rec tag] in [list "INSTALL-RECORD" "MODIFY-RECORD" "DELETE-RECORD" "VIRTUAL-RECORD"]} {
return $rec
switch -- [dict get $rec tag] {
INSTALL-RECORD - MODIFY-RECORD - DELETE-RECORD - VIRTUAL-RECORD {
return $rec
}
}
}
return [list]
@ -1487,53 +1493,57 @@ namespace eval punkcheck {
lappend files_copied $current_source_dir/$m
incr filecount_new
} else {
if {$overwrite_what eq "installedsourcechanged-targets"} {
if {[llength $changed]} {
#An unrecorded installation is considered a source change (from unknown/unrecorded source to recorded)
file copy -force $current_source_dir/$m $current_target_dir
set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m]
lappend files_copied $current_source_dir/$m
} else {
set is_skip 1
lappend files_skipped $current_source_dir/$m
}
} elseif {$overwrite_what eq "synced-targets"} {
if {[llength $changed]} {
#only overwrite if the target checksum equals the last installed checksum (ie target is in sync with source and so hasn't been customized)
set existing_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m]
set is_target_unmodified_since_install 0
set target_cksum_compare "unknown"
set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] ;#may be no such record - in which case we get an empty list
if {[dict exists $latest_install_record -targets_cksums]} {
set last_install_cksum [dict get $latest_install_record -targets_cksums] ;#in this case we know there is only one as 'install' always uses targetset size of 1. (FILEINFO record per file in source folder)
if {[dict get $existing_tgt_cksum_info cksum] eq $last_install_cksum} {
set is_target_unmodified_since_install 1
set target_cksum_compare "match"
} else {
set target_cksum_compare "nomatch"
}
} else {
set target_cksum_compare "norecord"
}
if {$is_target_unmodified_since_install} {
switch -- $overwrite_what {
installedsourcechanged-targets {
if {[llength $changed]} {
#An unrecorded installation is considered a source change (from unknown/unrecorded source to recorded)
file copy -force $current_source_dir/$m $current_target_dir
set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m]
lappend files_copied $current_source_dir/$m
} else {
#either cksum is different or we were unable to verify the record. Either way we can't know if the target is in sync so we must skip it
set is_skip 1
puts stderr "Skipping file copy $m target $current_target_dir/$m - require synced_target to overwrite - current target cksum compared to previous install: $target_cksum_compare"
lappend files_skipped $current_source_dir/$m
}
} else {
}
synced-targets {
if {[llength $changed]} {
#only overwrite if the target checksum equals the last installed checksum (ie target is in sync with source and so hasn't been customized)
set existing_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m]
set is_target_unmodified_since_install 0
set target_cksum_compare "unknown"
set latest_install_record [punkcheck::recordlist::file_record_latest_installrecord $filerec] ;#may be no such record - in which case we get an empty list
if {[dict exists $latest_install_record -targets_cksums]} {
set last_install_cksum [dict get $latest_install_record -targets_cksums] ;#in this case we know there is only one as 'install' always uses targetset size of 1. (FILEINFO record per file in source folder)
if {[dict get $existing_tgt_cksum_info cksum] eq $last_install_cksum} {
set is_target_unmodified_since_install 1
set target_cksum_compare "match"
} else {
set target_cksum_compare "nomatch"
}
} else {
set target_cksum_compare "norecord"
}
if {$is_target_unmodified_since_install} {
file copy -force $current_source_dir/$m $current_target_dir
set new_tgt_cksum_info [punk::mix::base::lib::cksum_path $current_target_dir/$m]
lappend files_copied $current_source_dir/$m
} else {
#either cksum is different or we were unable to verify the record. Either way we can't know if the target is in sync so we must skip it
set is_skip 1
puts stderr "Skipping file copy $m target $current_target_dir/$m - require synced_target to overwrite - current target cksum compared to previous install: $target_cksum_compare"
lappend files_skipped $current_source_dir/$m
}
} else {
set is_skip 1
lappend files_skipped $current_source_dir/$m
}
}
default {
set is_skip 1
puts stderr "Skipping file copy $m target $current_target_dir/$m already exists (use -overwrite all-targets to overwrite)"
#TODO! implement newer-targets older-targets? (note ctimes/mtimes are unreliable - may not be worth implementing)
lappend files_skipped $current_source_dir/$m
}
} else {
set is_skip 1
puts stderr "Skipping file copy $m target $current_target_dir/$m already exists (use -overwrite all-targets to overwrite)"
#TODO! implement newer-targets older-targets? (note ctimes/mtimes are unreliable - may not be worth implementing)
lappend files_skipped $current_source_dir/$m
}
}
}
@ -1578,11 +1588,15 @@ namespace eval punkcheck {
set subdirs [glob -nocomplain -dir $current_source_dir -type d -tail *]
set hiddensubdirs [glob -nocomplain -dir $current_source_dir -type {hidden d} -tail *]
foreach h $hiddensubdirs {
if {$h in [list "." ".."]} {
continue
}
if {$h ni $subdirs} {
lappend subdirs $h
switch -- $h {
"." - ".." {
continue
}
default {
if {$h ni $subdirs} {
lappend subdirs $h
}
}
}
}
}
@ -1730,8 +1744,24 @@ namespace eval punkcheck {
}
proc file_install_record_source_changes {install_record} {
#reject INSTALLFAILED items ?
if {[dict get $install_record tag] ni [list "QUERY-INPROGRESS" "INSTALL-RECORD" "INSTALL-SKIPPED" "INSTALL-INPROGRESS" "MODIFY-INPROGRESS" "MODIFY-RECORD" "MODIFY-SKIPPED" "VIRTUAL-INPROGRESS" "VIRTUAL-RECORD" "VIRTUAL-SKIPPED" "DELETE-RECORD" "DELETE-INPROGRESS" "DELETE-SKIPPED"]} {
error "file_install_record_source_changes bad install record: tag '[dict get $install_record tag]' not INSTALL-RECORD|SKIPPED|INSTALL-INPROGRESS|MODIFY-RECORD|MODIFY-INPROGRESS|VIRTUAL-RECORD|VIRTUAL-INPROGRESS|DELETE-RECORD|DELETE-INPROGRESS"
switch -- [dict get $install_record tag] {
"QUERY-INPROGRESS" -
"INSTALL-RECORD" -
"INSTALL-SKIPPED" -
"INSTALL-INPROGRESS" -
"MODIFY-INPROGRESS" -
"MODIFY-RECORD" -
"MODIFY-SKIPPED" -
"VIRTUAL-INPROGRESS" -
"VIRTUAL-RECORD" -
"VIRTUAL-SKIPPED" -
"DELETE-RECORD" -
"DELETE-INPROGRESS" -
"DELETE-SKIPPED" {
}
default {
error "file_install_record_source_changes bad install record: tag '[dict get $install_record tag]' not INSTALL-RECORD|SKIPPED|INSTALL-INPROGRESS|MODIFY-RECORD|MODIFY-INPROGRESS|VIRTUAL-RECORD|VIRTUAL-INPROGRESS|DELETE-RECORD|DELETE-INPROGRESS"
}
}
set source_list [dict_getwithdefault $install_record body [list]]
set changed [list]

38
src/modules/punkcheck/cli-999999.0a1.0.tm

@ -108,15 +108,18 @@ namespace eval punkcheck::cli {
if {[dict get $r tag] eq "SOURCE"} {
set path [dict get $r -path]
set changed [dict get $r -changed]
if {[dict get $r -type] eq "file"} {
lappend source_files $path
if {$changed} {
lappend source_files_changed $path
switch -- [dict get $r -type] {
file {
lappend source_files $path
if {$changed} {
lappend source_files_changed $path
}
}
} elseif {[dict get $r -type] eq "directory"} {
lappend source_folders $path
if {$changed} {
lappend source_folders_changed $path
directory {
lappend source_folders $path
if {$changed} {
lappend source_folders_changed $path
}
}
}
}
@ -203,15 +206,18 @@ namespace eval punkcheck::cli {
if {[dict get $r tag] eq "SOURCE"} {
set path [dict get $r -path]
set changed [dict get $r -changed]
if {[dict get $r -type] eq "file"} {
lappend source_files $path
if {$changed} {
lappend source_files_changed $path
switch -- [dict get $r -type] {
file {
lappend source_files $path
if {$changed} {
lappend source_files_changed $path
}
}
} elseif {[dict get $r -type] eq "directory"} {
lappend source_folders $path
if {$changed} {
lappend source_folders_changed $path
directory {
lappend source_folders $path
if {$changed} {
lappend source_folders_changed $path
}
}
}
}

441
src/modules/shellfilter-0.1.9.tm

@ -165,20 +165,25 @@ namespace eval shellfilter::ansi2 {
#accept examples for foreground
# 256f-# or 256fg-# or 256f#
# rgbf-<r>-<g>-<b> or rgbfg-<r>-<g>-<b> or rgbf<r>-<g>-<b>
if {[string match -nocase "256f*" $i]} {
set cc [string trim [string range $i 4 end] -gG]
lappend t "38;5;$cc"
} elseif {[string match -nocase 256b* $i]} {
set cc [string trim [string range $i 4 end] -gG]
lappend t "48;5;$cc"
} elseif {[string match -nocase rgbf* $i]} {
set rgb [string trim [string range $i 4 end] -gG]
lassign [split $rgb -] r g b
lappend t "38;2;$r;$g;$b"
} elseif {[string match -nocase rgbb* $i]} {
set rgb [string trim [string range $i 4 end] -gG]
lassign [split $rgb -] r g b
lappend t "48;2;$r;$g;$b"
switch -nocase -glob $i {
"256f*" {
set cc [string trim [string range $i 4 end] -gG]
lappend t "38;5;$cc"
}
"256b*" {
set cc [string trim [string range $i 4 end] -gG]
lappend t "48;5;$cc"
}
"rgbf*" {
set rgb [string trim [string range $i 4 end] -gG]
lassign [split $rgb -] r g b
lappend t "38;2;$r;$g;$b"
}
"rgbb*" {
set rgb [string trim [string range $i 4 end] -gG]
lassign [split $rgb -] r g b
lappend t "48;2;$r;$g;$b"
}
}
}
}
@ -239,6 +244,7 @@ namespace eval shellfilter::ansi2 {
namespace eval shellfilter::ansi {
#maint warning - from overtype package
#stripansi is better/more comprehensive
proc stripcodes {text} {
#single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).
dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"]
@ -686,6 +692,9 @@ namespace eval shellfilter::chan {
}
}
#this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it.
#It can be useful for test/debugging
oo::class create ansiwrap {
variable o_trecord
variable o_enc
@ -1024,10 +1033,13 @@ namespace eval shellfilter::stack {
proc _get_stack_floaters {stack} {
set floaters [list]
foreach t [lreverse $stack] {
if {[dict get $t -action] eq "float"} {
lappend floaters $t
} else {
break
switch -- [dict get $t -action] {
float {
lappend floaters $t
}
default {
break
}
}
}
return [lreverse $floaters]
@ -1299,88 +1311,96 @@ namespace eval shellfilter::stack {
# but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?)
# jn
set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args]
if {$action in [list "float" "float-locked"]} {
set obj [$transformname new $transform_record]
set h [chan push $localchan $obj]
dict set transform_record -handle $h
dict set transform_record -obj $obj
lappend stack $transform_record
} elseif {$action in [list "locked" ""]} {
set floaters [_get_stack_floaters $stack]
if {![llength $floaters]} {
switch -glob -- $action {
float - float-locked {
set obj [$transformname new $transform_record]
set h [chan push $localchan $obj]
dict set transform_record -handle $h
dict set transform_record -obj $obj
lappend stack $transform_record
} else {
set poplist $floaters
set stack [insert_transform $pipename $stack $transform_record $poplist]
}
} elseif {[string match sink* $action]} {
set redirinfo [_get_stack_top_redirection $stack]
set idx_existing_redir [dict get $redirinfo index]
if {$idx_existing_redir == -1} {
#no existing redirection transform on the stack
#pop everything.. add this record as the first redirection on the stack
set poplist $stack
set stack [insert_transform $pipename $stack $transform_record $poplist]
} else {
if {$action eq "sink-replace"} {
#include that index in the poplist
set poplist [lrange $stack $idx_existing_redir end]
#pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end'
set stack [insert_transform $pipename $stack $transform_record $poplist 1]
} elseif {[string match "sink-aside*" $action]} {
set existing_redir_record [lindex $stack $idx_existing_redir]
if {[string match "*locked*" [dict get $existing_redir_record -action]]} {
set put_aside 0
#we can't aside this one - sit above it instead.
set poplist [lrange $stack $idx_existing_redir+1 end]
set stack [lrange $stack 0 $idx_existing_redir]
} else {
set put_aside 1
dict set transform_record -aside [lindex $stack $idx_existing_redir]
set poplist [lrange $stack $idx_existing_redir end]
set stack [lrange $stack 0 $idx_existing_redir-1]
}
foreach p $poplist {
chan pop $localchan
}
set transformname [dict get $transform_record -transform]
set transform_settings [dict get $transform_record -settings]
}
"" - locked {
set floaters [_get_stack_floaters $stack]
if {![llength $floaters]} {
set obj [$transformname new $transform_record]
set h [chan push $localchan $obj]
set h [chan push $localchan $obj]
dict set transform_record -handle $h
dict set transform_record -obj $obj
dict set transform_record -note "insert_transform-with-aside"
lappend stack $transform_record
#add back poplist *except* the one we transferred into -aside (if we were able)
foreach p [lrange $poplist $put_aside end] {
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set obj [$t new $p]
set h [chan push $localchan $obj]
#retain previous -id - code that added it may have kept reference and not expecting it to change
dict set p -handle $h
dict set p -obj $obj
dict set p -note "re-added-after-sink-aside"
lappend stack $p
}
} else {
#plain "sink"
#we only sink to the topmost redirecting filter - which makes sense for an output channel
#For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection.
#todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there.
# - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive.
# consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour
set poplist [lrange $stack $idx_existing_redir+1 end]
set poplist $floaters
set stack [insert_transform $pipename $stack $transform_record $poplist]
}
}
} else {
error "shellfilter::stack::add unimplemented action '$action'"
"sink*" {
set redirinfo [_get_stack_top_redirection $stack]
set idx_existing_redir [dict get $redirinfo index]
if {$idx_existing_redir == -1} {
#no existing redirection transform on the stack
#pop everything.. add this record as the first redirection on the stack
set poplist $stack
set stack [insert_transform $pipename $stack $transform_record $poplist]
} else {
switch -glob -- $action {
"sink-replace" {
#include that index in the poplist
set poplist [lrange $stack $idx_existing_redir end]
#pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end'
set stack [insert_transform $pipename $stack $transform_record $poplist 1]
}
"sink-aside*" {
set existing_redir_record [lindex $stack $idx_existing_redir]
if {[string match "*locked*" [dict get $existing_redir_record -action]]} {
set put_aside 0
#we can't aside this one - sit above it instead.
set poplist [lrange $stack $idx_existing_redir+1 end]
set stack [lrange $stack 0 $idx_existing_redir]
} else {
set put_aside 1
dict set transform_record -aside [lindex $stack $idx_existing_redir]
set poplist [lrange $stack $idx_existing_redir end]
set stack [lrange $stack 0 $idx_existing_redir-1]
}
foreach p $poplist {
chan pop $localchan
}
set transformname [dict get $transform_record -transform]
set transform_settings [dict get $transform_record -settings]
set obj [$transformname new $transform_record]
set h [chan push $localchan $obj]
dict set transform_record -handle $h
dict set transform_record -obj $obj
dict set transform_record -note "insert_transform-with-aside"
lappend stack $transform_record
#add back poplist *except* the one we transferred into -aside (if we were able)
foreach p [lrange $poplist $put_aside end] {
set t [dict get $p -transform]
set tsettings [dict get $p -settings]
set obj [$t new $p]
set h [chan push $localchan $obj]
#retain previous -id - code that added it may have kept reference and not expecting it to change
dict set p -handle $h
dict set p -obj $obj
dict set p -note "re-added-after-sink-aside"
lappend stack $p
}
}
default {
#plain "sink"
#we only sink to the topmost redirecting filter - which makes sense for an output channel
#For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection.
#todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there.
# - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive.
# consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour
set poplist [lrange $stack $idx_existing_redir+1 end]
set stack [insert_transform $pipename $stack $transform_record $poplist]
}
}
}
}
default {
error "shellfilter::stack::add unimplemented action '$action'"
}
}
dict set pipelines $pipename stack $stack
@ -1706,17 +1726,21 @@ namespace eval shellfilter {
}
} else {
#currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active.
if {$char eq "("} {
incr word_bdepth
lappend word_bstack $char
append word $char
} elseif {$char eq ")"} {
incr word_bdepth -1
set word_bstack [lrange $word_bstack 0 end-1]
append word $char
} else {
#spaces and chars added to word as it's still in a bracketed section
append word $char
switch -- $char {
"(" {
incr word_bdepth
lappend word_bstack $char
append word $char
}
")" {
incr word_bdepth -1
set word_bstack [lrange $word_bstack 0 end-1]
append word $char
}
default {
#spaces and chars added to word as it's still in a bracketed section
append word $char
}
}
}
}
@ -1801,14 +1825,18 @@ namespace eval shellfilter {
}
}
} else {
if {$char eq "("} {
incr word_bdepth
append word $char
} elseif {$char eq ")"} {
incr word_bdepth -1
append word $char
} else {
append word $char
switch -- $char {
"(" {
incr word_bdepth
append word $char
}
")" {
incr word_bdepth -1
append word $char
}
default {
append word $char
}
}
}
}
@ -1849,13 +1877,15 @@ namespace eval shellfilter {
#only double quote if argument not quoted with single or double quotes
proc dquote_if_not_quoted {a} {
if {([string range $a 0 0] eq {"}) && ([string range $a end end] eq {"})} {
return $a
} elseif {([string range $a 0 0] eq {'}) && ([string range $a end end] eq {'})} {
return $a
} else {
set newinner [string map [list {"} "\\\""] $a]
return "\"$newinner\""
set wrapchars [string cat [string range $a 0 0] [string range $a end end]]
switch -- $wrapchars {
{""} - {''} {
return $a
}
default {
set newinner [string map [list {"} "\\\""] $a]
return "\"$newinner\""
}
}
}
@ -1863,12 +1893,16 @@ namespace eval shellfilter {
#wrap in double quotes if not double-quoted
proc dquote_if_not_dquoted {a} {
if {([string range $a 0 0] eq {"}) && ([string range $a end end] eq {"})} {
return $a
} else {
#escape any inner quotes..
set newinner [string map [list {"} "\\\""] $a]
return "\"$newinner\""
set wrapchars [string cat [string range $a 0 0] [string range $a end end]]
switch -- $wrapchars {
{""} {
return $a
}
default {
#escape any inner quotes..
set newinner [string map [list {"} "\\\""] $a]
return "\"$newinner\""
}
}
}
proc dquote {a} {
@ -2122,8 +2156,26 @@ namespace eval shellfilter {
}
set invalid_flags [list]
dict for {k -} $args {
if {$k ni $valid_flags} {
lappend invalid_flags $k
switch -- $k {
-timeout -
-outprefix -
-errprefix -
-debug -
-copytempfile -
-outbuffering -
-errbuffering -
-inbuffering -
-readprocesstranslation -
-outtranslation -
-stdinhandler -
-outchan -
-errchan -
-inchan -
-teehandle {
}
default {
lappend invalid_flags $k
}
}
}
if {[llength $invalid_flags]} {
@ -2182,36 +2234,39 @@ namespace eval shellfilter {
lassign [chan pipe] rderr wrerr
chan configure $wrerr -blocking 0
set custom_stderr ""
set lastitem [lindex $commandlist end]
#todo - ensure we can handle 2> file (space after >)
if {[string trim [lindex $commandlist end]] eq "&"} {
set name [lindex $commandlist 0]
#background execution - stdout and stderr from child still comes here - but process is backgrounded
#FIX! - this is broken for paths with backslashes for example
#set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]]
set pidlist [exec {*}$commandlist]
return [list pids $pidlist]
}
#review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes!
#
#note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere
#(2>@stdout echoes to main stdout - not into pipeline)
#To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads)
set custom_stderr ""
if {[string trim $lastitem] in [list {2>&1} {2>@1}]} {
set custom_stderr {2>@1} ;#use the tcl style
set commandlist [lrange $commandlist 0 end-1]
} else {
# 2> filename
# 2>> filename
# 2>@ openfileid
set redir2test [string range $lastitem 0 1]
if {$redir2test eq "2>"} {
set custom_stderr $lastitem
switch -- [string trim $lastitem] {
{&} {
set name [lindex $commandlist 0]
#background execution - stdout and stderr from child still comes here - but process is backgrounded
#FIX! - this is broken for paths with backslashes for example
#set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]]
set pidlist [exec {*}$commandlist]
return [list pids $pidlist]
}
{2>&1} - {2>@1} {
set custom_stderr {2>@1} ;#use the tcl style
set commandlist [lrange $commandlist 0 end-1]
}
default {
# 2> filename
# 2>> filename
# 2>@ openfileid
set redir2test [string range $lastitem 0 1]
if {$redir2test eq "2>"} {
set custom_stderr $lastitem
set commandlist [lrange $commandlist 0 end-1]
}
}
}
set lastitem [lindex $commandlist end]
@ -2224,12 +2279,14 @@ namespace eval shellfilter {
::shellfilter::log::write $runtag "checking for redirections in $commandlist"
#sometimes we see a redirection without a following space e.g >C:/somewhere
#normalize
if {[regexp {^>[/[:alpha:]]+} $lastitem]} {
set lastitem "> [string range $lastitem 1 end]"
}
if {[regexp {^>>[/[:alpha:]]+} $lastitem]} {
set lastitem ">> [string range $lastitem 2 end]"
}
switch -regexp -- $lastitem\
{^>[/[:alpha:]]+} {
set lastitem "> [string range $lastitem 1 end]"
}\
{^>>[/[:alpha:]]+} {
set lastitem ">> [string range $lastitem 2 end]"
}
#for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}}
#or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces}
@ -2285,51 +2342,53 @@ namespace eval shellfilter {
}
set commandlist [lrange $commandlist 0 end-1]
} elseif {[lindex $commandlist end-1] in [list ">>" ">"]} {
#unwrapped redirection
#we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list
set redir [lindex $commandlist end-1]
set redirtarget [lindex $commandlist end]
set commandlist [lrange $commandlist 0 end-2]
} else {
#no redirection
set redir ""
set redirtarget ""
#no change to command list
}
} elseif {[lindex $commandlist end-1] in [list ">>" ">"]} {
#unwrapped redirection
#we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list
set redir [lindex $commandlist end-1]
set redirtarget [lindex $commandlist end]
set commandlist [lrange $commandlist 0 end-2]
} else {
#no redirection
set redir ""
set redirtarget ""
#no change to command list
}
if {$redir in [list ">>" ">"]} {
set redirtarget [string trim $redirtarget "\""]
::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'"
switch -- $redir {
">>" - ">" {
set redirtarget [string trim $redirtarget "\""]
::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'"
set winfile $redirtarget ;#default assumption
if {[string match "/c/*" $redirtarget]} {
set winfile "c:/[string range $redirtarget 3 end]"
}
if {[string match "/mnt/c/*" $redirtarget]} {
set winfile "c:/[string range $redirtarget 7 end]"
}
set winfile $redirtarget ;#default assumption
switch -glob -- $redirtarget {
"/c/*" {
set winfile "c:/[string range $redirtarget 3 end]"
}
"/mnt/c/*" {
set winfile "c:/[string range $redirtarget 7 end]"
}
}
if {[file exists [file dirname $winfile]]} {
#containing folder for target exists
if {$redir eq ">"} {
set teefile "write"
} else {
set teefile "append"
if {[file exists [file dirname $winfile]]} {
#containing folder for target exists
if {$redir eq ">"} {
set teefile "write"
} else {
set teefile "append"
}
::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile"
} else {
#we should be writing to a file.. but can't
::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'"
}
}
::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile"
} else {
#we should be writing to a file.. but can't
::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'"
default {
::shellfilter::log::write $runtag "No redir found!!"
}
}
}
} else {
::shellfilter::log::write $runtag "No redir found!!"
}
#often first element of command list is wrapped and cannot be run directly
#e.g {{ls -l} {> {temp.tmp}}}
#we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped.
@ -2456,11 +2515,11 @@ namespace eval shellfilter {
#} else {
# puts stderr "stderr reader: pid [lindex $pids 0] still running"
#}
chan close $chan
#catch {chan close $wrerr}
if {$other ni [chan names]} {
set $waitfor stderr
}
chan close $chan
#catch {chan close $wrerr}
if {$other ni [chan names]} {
set $waitfor stderr
}
}
}} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids]
}

8
src/modules/shellrun-0.1.1.tm

@ -141,12 +141,12 @@ namespace eval shellrun {
set nonewline 0
}
set idlist_stderr [list]
#we leave stdout without imposed ansi colouring - because the source may be colourised
#stderr might have source colouring - but it usually doesn't seem to, and the visual distiction of red stderr is very handy for the run command.
#we leave stdout without imposed ansi colouring - because the source may be colourised and because ansi-wrapping a stream at whatever boundaries it comes in at isn't a really nice thing to do.
#stderr might have source colouring - but it usually doesn't seem to, and the visual distiction of red stderr can be very handy for the run command.
#A further enhancement could be to detect well-known options such as --color and/or use a configuration for specific commands that have useful colourised stderr,
#but defaulting stderr to red is a compromise.
#but having an option to configure stderr to red is a compromise.
#Note that the other run commands, runout,runerr, runx don't emit in real-time - so for those commands there may be options to detect and/or post-process stdout and stderr.
#TODO - fix. This has no effect because the repl adds an ansiwrap transform
#TODO - fix. This has no effect if/when the repl adds an ansiwrap transform
# what we probably want to do is 'aside' that transform for runxxx commands only.
#lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}]

435
src/modules/textblock-999999.0a1.0.tm

@ -42,6 +42,7 @@ namespace eval textblock {
#return a homogenous block of characters - ie lines all same length, all same character
#printing-width in terminal columns is not necessarily same as blockwidth even if a single char is passed (could be full-width unicode character)
#This can have ansi SGR codes - but to repeat blocks containing ansi-movements, the block should first be rendered to a static output with something like overtype::left
proc block {blockwidth blockheight {char " "}} {
if {$blockwidth < 0} {
error "textblock::block blockwidth must be an integer greater than or equal to zero"
@ -50,15 +51,17 @@ namespace eval textblock {
error "textblock::block blockheight must be a positive integer"
}
if {$char eq ""} {return ""}
#using string length is ok
if {[string length $char] == 1} {
set row [string repeat $char $blockwidth]
set mtrx [lrepeat $blockheight $row]
return [::join $mtrx \n]
} else {
set charblock [string map [list \r\n \n] $char]
if {[string first \n $charblock] >= 0} {
if {[string last \n $charblock] >= 0} {
if {$blockwidth > 1} {
set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]]
#set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]] ;#building a repeated "|> command arg" list to evaluate as a pipeline. (from before textblock::join could take arbitrary num of blocks )
set row [textblock::join {*}[lrepeat $blockwidth $charblock]]
} else {
set row $charblock
}
@ -72,34 +75,34 @@ namespace eval textblock {
#todo - consider 'elastic tabstops' for textblocks where tab acts as a column separator and adjacent lines with the same number of tabs form a sort of table
proc width {textblock} {
#backspaces, vertical tabs,carriage returns
#backspaces, vertical tabs ?
if {$textblock eq ""} {
return 0
}
#textutil::tabify is a reasonable hack when there are no ansi SGR codes - but probably not always what we want even then - review
set textblock [textutil::tabify::untabify2 $textblock]
if {[string first \n $textblock] >= 0} {
return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width [stripansi $v]}]]
if {[string last \t $textblock] >= 0} {
if {[info exists punk::console::tabwidth]} {
set tw $::punk::console::tabwidth
} else {
set tw 8
}
set textblock [textutil::tabify::untabify2 $textblock $tw]
}
return [punk::char::ansifreestring_width [stripansi $textblock]]
}
proc width_naive {textblock} {
# doesn't deal with backspaces, vertical tabs,carriage returns, ansi movements
if {$textblock eq ""} {
return 0
if {[punk::ansi::ta::detect $textblock]} {
set textblock [punk::ansi::stripansi $textblock]
}
set textblock [textutil::tabify::untabify2 $textblock] ;#a reasonable hack - but probably not always what we want - review
if {[string first \n $textblock] >= 0} {
return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::string_width [stripansi $v]}]]
if {[string last \n $textblock] >= 0} {
return [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]]
}
return [punk::char::string_width [stripansi $textblock]]
return [punk::char::ansifreestring_width $textblock]
}
#uses tcl's string length on each line. Length will include chars in ansi codes etc - this is not a 'width' determining function.
proc string_length_line_max textblock {
tcl::mathfunc::max {*}[lmap v [split $textblock \n] {string length $v}]
}
proc string_length_line_min textblock {
tcl::mathfunc::min {*}[lmap v [split $textblock \n] {string length $v}]
}
proc height {textblock} {
#This is the height as it will/would-be rendered - not the number of input lines purely in terms of le
#empty string still has height 1 (at least for left-right/right-left languages)
@ -115,11 +118,22 @@ namespace eval textblock {
if {$textblock eq ""} {
return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings
}
set textblock [textutil::tabify::untabify2 $textblock]
#strangely - string last (windows tcl8.7 anway) is faster than string first for large strings when the needle not in the haystack
if {[string last \t $textblock] >= 0} {
if {[info exists punk::console::tabwidth]} {
set tw $::punk::console::tabwidth
} else {
set tw 8
}
set textblock [textutil::tabify::untabify2 $textblock $tw]
}
#stripansi on entire block in one go rather than line by line - result should be the same - review - make tests
set textblock [punk::ansi::stripansi $textblock]
if {[string first \n $textblock] >= 0} {
set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]]
if {[punk::ansi::ta::detect $textblock]} {
set textblock [punk::ansi::stripansi $textblock]
}
if {[string last \n $textblock] >= 0} {
#set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::ansifreestring_width $v}]]
set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]]
} else {
set width [punk::char::ansifreestring_width $textblock]
}
@ -137,8 +151,13 @@ namespace eval textblock {
if {$block eq ""} {
return 0
}
set block [textutil::tabify::untabify2 $block]
if {[string first \n $block] >= 0} {
if {[info exists punk::console::tabwidth]} {
set tw $::punk::console::tabwidth
} else {
set tw 8
}
set block [textutil::tabify::untabify2 $block $tw]
if {[string last \n $block] >= 0} {
return [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $block] {::punk::char::string_width [stripansi $v]}]]
}
if {[catch {llength $block}]} {
@ -230,26 +249,44 @@ namespace eval textblock {
}
return [punk::lib::list_as_lines -- $outlines]
}
#for joining 'rendered' blocks of plain or ansi text. Being 'rendered' means they are without ansi movement sequences as these have been processed
proc ::textblock::join {args} {
lassign [punk::args::opts_values {
blocks -type string -multiple 1
} $args] _o opts _v values
set blocks [dict get $values blocks]
#lassign [punk::lib::opts_values {
# blocks -type string -multiple 1
#} $args] _o opts _v values
#set blocks [dict get $values blocks]
if {[lindex $args 0] eq "--"} {
set blocks [lrange $args 1 end]
} else {
set blocks $args
}
set idx 0
set fordata [list]
set colindices [list]
foreach b $blocks {
set c($idx) [string repeat " " [width $b]]
#lappend fordata "v($idx)" [punk::lib::lines_as_list -- $b]
lappend fordata "v($idx)" [punk::lib::lines_as_list -ansiresets 1 -- $b]
set w($idx) [width $b] ;#we need the width of a rendered block for per-row renderline calls
#fairly commonly a block may be a vertical list of chars e.g something like 1\n2\n3 or -\n-\n-\n-
#for determining a shortcut to avoid unnecessary ta::detect - (e.g width <=1) we can't use result of textblock::width as it strips ansi.
#testing of any decent size block line by line - even with max_string_length_line is slower than ta::detect anyway.
if {[punk::ansi::ta::detect $b]} {
lappend fordata "v($idx)" [punk::lib::lines_as_list -ansiresets 1 -- $b]
} else {
#each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi
lappend fordata "v($idx)" [split $b \n]
}
lappend colindices $idx
incr idx
}
set outlines [list]
set colindices [lsort -integer -increasing [array names c]]
#set colindices [lsort -integer -increasing [array names c]]
foreach {*}$fordata {
set row ""
foreach colidx $colindices {
append row [overtype::left $c($colidx) $v($colidx)]
#we know we have a single line, no ansi in underlay and no overflow required - so we can use renderline directly
append row [overtype::renderline -width $w($colidx) -insert_mode 0 -info 0 $c($colidx) $v($colidx)]
}
lappend outlines $row
}
@ -292,6 +329,7 @@ namespace eval textblock {
set 2frames_b [textblock::join [textblock::frame -ansiborder $cyanb -title "plainpunks" $punks] [textblock::frame -ansiborder $greenb -title "fancypunks" $cpunks]]
append out [textblock::frame -title "punks" $2frames_b\n$RST$2frames_a] \n
append out [overtype::right [overtype::left [textblock::frame -ansiborder [a+ green bold] -type unicode_box_heavy -title ${redb}PATTERN$RST -subtitle ${redb}PUNK$RST $prightair_cyanb] "$blueb\n\n\P\nU\nN\nK$RST"] "$blueb\n\nL\nI\nF\nE"] \n
#append out [textblock::frame -title gr $gr0]
return $out
}
@ -311,8 +349,6 @@ namespace eval textblock {
}
proc frame {args} {
package require punk::char
set contents [lindex $args end]
set arglist [lrange $args 0 end-1]
if {[llength $arglist] % 2 != 0} {
@ -321,6 +357,7 @@ namespace eval textblock {
#todo args -justify left|centre|right (center)
set defaults [dict create\
-etabs 0\
-type unicode_box\
-title ""\
-subtitle ""\
@ -330,11 +367,15 @@ namespace eval textblock {
]
set opts [dict merge $defaults $arglist]
foreach {k v} $opts {
if {$k ni [dict keys $defaults]} {
error "frame option '$k' not understood. Valid options are [dict keys $defaults]"
switch -- $k {
-etabs - -type - -title - -subtitle - -width - -ansiborder - -align {}
default {
error "frame option '$k' not understood. Valid options are [dict keys $defaults]"
}
}
}
# -- --- --- --- --- ---
set opt_etabs [dict get $opts -etabs]
set opt_type [dict get $opts -type]
set known_types [list unicode_box unicode_box_heavy unicode_arc unicode_double ascii altg]
set default_custom [dict create hl " " vl " " tlc " " trc " " blc " " brc " "]
@ -344,9 +385,13 @@ namespace eval textblock {
if {[llength $opt_type] %2 == 0} {
#custom dict may leave out keys - but cannot have unknown keys
dict for {k v} $opt_type {
if {$k ni $custom_keys} {
set is_custom_dict_ok 0
break
switch -- $k {
hl - hlt - hlb - vl - vll - vlr - tlc - trc - blc - brc {}
default {
#k not in custom_keys
set is_custom_dict_ok 0
break
}
}
}
} else {
@ -364,15 +409,27 @@ namespace eval textblock {
# -- --- --- --- --- ---
set opt_align [dict get $opts -align]
set opt_align [string tolower $opt_align]
if {$opt_align ni [list left right centre center]} {
#these are all valid commands for overtype::<cmd>
error "frame option -align must be left|right|centre|center - received: $$opt_align"
switch -- $opt_align {
left - right - centre - center {}
default {
error "frame option -align must be left|right|centre|center - received: $$opt_align"
}
}
#these are all valid commands for overtype::<cmd>
# -- --- --- --- --- ---
set opt_ansiborder [dict get $opts -ansiborder]
# -- --- --- --- --- ---
set contents [textutil::tabify::untabify2 $contents]
if {[string last \t $contents] >= 0} {
if {[info exists punk::console::tabwidth]} {
set tw $::punk::console::tabwidth
} else {
set tw 8
}
if {$opt_etabs} {
set contents [textutil::tabify::untabify2 $contents $tw]
}
}
set contents [string map [list \r\n \n] $contents]
set actual_contentwidth [width $contents]
@ -394,156 +451,164 @@ namespace eval textblock {
set linecount [textblock::height $contents]
set rst [a]
set column [string repeat " " $contentwidth] ;#default - may need to override for custom frame
if {$opt_type eq "altg"} {
#old style ansi escape sequences with alternate graphics page G0
set hl [cd::hl]
set hlt $hl
set hlb $hl
set vl [cd::vl]
set vll $vl
set vlr $vl
set tlc [cd::tlc]
set trc [cd::trc]
set blc [cd::blc]
set brc [cd::brc]
set tbar [string repeat $hl $contentwidth]
set tbar [cd::groptim $tbar]
set bbar $tbar
} elseif {$opt_type eq "ascii"} {
set hl -
set hlt -
set hlb -
set vl |
set vll |
set vlr |
set tlc +
set trc +
set blc +
set brc +
set tbar [string repeat - $contentwidth]
set bbar $tbar
} elseif {$opt_type eq "unicode_box"} {
#unicode box drawing set
set hl [punk::char::charshort boxd_lhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_lv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ldr]
set trc [punk::char::charshort boxd_ldl]
set blc [punk::char::charshort boxd_lur]
set brc [punk::char::charshort boxd_lul]
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
} elseif {$opt_type eq "unicode_box_heavy"} {
#unicode box drawing set
set hl [punk::char::charshort boxd_hhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_hv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_hdr]
set trc [punk::char::charshort boxd_hdl]
set blc [punk::char::charshort boxd_hur]
set brc [punk::char::charshort boxd_hul]
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
} elseif {$opt_type eq "unicode_double"} {
#unicode box drawing set
set hl [punk::char::charshort boxd_dhz] ;# double horizontal \U2550
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_dv] ;#double vertical \U2551
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ddr] ;#double down and right \U2554
set trc [punk::char::charshort boxd_ddl] ;#double down and left \U2557
set blc [punk::char::charshort boxd_dur] ;#double up and right \U255A
set brc [punk::char::charshort boxd_dul] ;#double up and left \U255D
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
} elseif {$opt_type eq "unicode_arc"} {
#unicode box drawing set
set hl [punk::char::charshort boxd_lhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_lv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ladr] ;#light arc down and right \U256D
set trc [punk::char::charshort boxd_ladl] ;#light arc down and left \U256E
set blc [punk::char::charshort boxd_laur] ;#light arc up and right \U2570
set brc [punk::char::charshort boxd_laul] ;#light arc up and left \U256F
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
} else {
dict with custom_frame {} ;#extract keys as vars
if {[dict exists $custom_frame hlt]} {
set hlt [dict get $custom_frame hlt]
} else {
switch -- $opt_type {
"altg" {
#old style ansi escape sequences with alternate graphics page G0
set hl [cd::hl]
set hlt $hl
set hlb $hl
set vl [cd::vl]
set vll $vl
set vlr $vl
set tlc [cd::tlc]
set trc [cd::trc]
set blc [cd::blc]
set brc [cd::brc]
set tbar [string repeat $hl $contentwidth]
set tbar [cd::groptim $tbar]
set bbar $tbar
}
"ascii" {
set hl -
set hlt -
set hlb -
set vl |
set vll |
set vlr |
set tlc +
set trc +
set blc +
set brc +
set tbar [string repeat - $contentwidth]
set bbar $tbar
}
"unicode_box" {
#unicode box drawing set
set hl [punk::char::charshort boxd_lhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_lv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ldr]
set trc [punk::char::charshort boxd_ldl]
set blc [punk::char::charshort boxd_lur]
set brc [punk::char::charshort boxd_lul]
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
}
set hlt_width [punk::ansi::printing_length $hlt]
if {[dict exists $custom_frame hlb]} {
set hlb [dict get $custom_frame hlb]
} else {
"unicode_box_heavy" {
#unicode box drawing set
set hl [punk::char::charshort boxd_hhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_hv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_hdr]
set trc [punk::char::charshort boxd_hdl]
set blc [punk::char::charshort boxd_hur]
set brc [punk::char::charshort boxd_hul]
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
}
set hlb_width [punk::ansi::printing_length $hlb]
if {[dict exists $custom_frame vll]} {
set vll [dict get $custom_frame vll]
} else {
"unicode_double" {
#unicode box drawing set
set hl [punk::char::charshort boxd_dhz] ;# double horizontal \U2550
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_dv] ;#double vertical \U2551
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ddr] ;#double down and right \U2554
set trc [punk::char::charshort boxd_ddl] ;#double down and left \U2557
set blc [punk::char::charshort boxd_dur] ;#double up and right \U255A
set brc [punk::char::charshort boxd_dul] ;#double up and left \U255D
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
}
set vll_width [punk::ansi::printing_length $vll]
if {[dict exists $custom_frame vlr]} {
set vlr [dict get $custom_frame vlr]
} else {
"unicode_arc" {
#unicode box drawing set
set hl [punk::char::charshort boxd_lhz] ;# light horizontal
set hlt $hl
set hlb $hl
set vl [punk::char::charshort boxd_lv] ;#light vertical
set vll $vl
set vlr $vl
set tlc [punk::char::charshort boxd_ladr] ;#light arc down and right \U256D
set trc [punk::char::charshort boxd_ladl] ;#light arc down and left \U256E
set blc [punk::char::charshort boxd_laur] ;#light arc up and right \U2570
set brc [punk::char::charshort boxd_laul] ;#light arc up and left \U256F
set tbar [string repeat $hl $contentwidth]
set bbar $tbar
}
set vlr_width [punk::ansi::printing_length $vlr]
default {
dict with custom_frame {} ;#extract keys as vars
if {[dict exists $custom_frame hlt]} {
set hlt [dict get $custom_frame hlt]
} else {
set hlt $hl
}
set hlt_width [punk::ansi::printing_length $hlt]
if {[dict exists $custom_frame hlb]} {
set hlb [dict get $custom_frame hlb]
} else {
set hlb $hl
}
set hlb_width [punk::ansi::printing_length $hlb]
set tlc_width [punk::ansi::printing_length $tlc]
set trc_width [punk::ansi::printing_length $trc]
set blc_width [punk::ansi::printing_length $blc]
set brc_width [punk::ansi::printing_length $brc]
if {[dict exists $custom_frame vll]} {
set vll [dict get $custom_frame vll]
} else {
set vll $vl
}
set vll_width [punk::ansi::printing_length $vll]
if {[dict exists $custom_frame vlr]} {
set vlr [dict get $custom_frame vlr]
} else {
set vlr $vl
}
set vlr_width [punk::ansi::printing_length $vlr]
set tlc_width [punk::ansi::printing_length $tlc]
set trc_width [punk::ansi::printing_length $trc]
set blc_width [punk::ansi::printing_length $blc]
set brc_width [punk::ansi::printing_length $brc]
set framewidth [expr {$contentwidth + 2}] ;#reverse default assumption
if {$opt_width eq ""} {
#width wasn't specified - so user is expecting frame to adapt to title/contents
#content shouldn't truncate because of extra wide frame
set contentwidth $content_or_title_width
set tbarwidth [expr {$content_or_title_width + 2 - $tlc_width - $trc_width - 2 + $vll_width + $vlr_width}] ;#+/2's for difference between border element widths and standard element single-width
set bbarwidth [expr {$content_or_title_width + 2 - $blc_width - $brc_width - 2 + $vll_width + $vlr_width}]
} else {
set contentwidth [expr $opt_width - $vll_width - $vlr_width] ;#content may be truncated
set tbarwidth [expr {$opt_width - $tlc_width - $trc_width}]
set bbarwidth [expr {$opt_width - $blc_width - $brc_width}]
}
set column [string repeat " " $contentwidth]
if {$hlt_width == 1} {
set tbar [string repeat $hlt $tbarwidth]
} else {
#possibly mixed width chars that make up hlt - string range won't get width right
set blank [string repeat " " $tbarwidth]
set count [expr {($tbarwidth / $hlt_width) + 1}]
set tbar [string repeat $hlt $count]
#set tbar [string range $tbar 0 $tbarwidth-1]
set tbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $tbar];#spaces for exposed halves of 2w chars instead of default replacement character
}
if {$hlb_width == 1} {
set bbar [string repeat $hlb $bbarwidth]
} else {
set blank [string repeat " " $bbarwidth]
set count [expr {($bbarwidth / $hlb_width) + 1}]
set bbar [string repeat $hlb $count]
#set bbar [string range $bbar 0 $bbarwidth-1]
set bbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $bbar]
set framewidth [expr {$contentwidth + 2}] ;#reverse default assumption
if {$opt_width eq ""} {
#width wasn't specified - so user is expecting frame to adapt to title/contents
#content shouldn't truncate because of extra wide frame
set contentwidth $content_or_title_width
set tbarwidth [expr {$content_or_title_width + 2 - $tlc_width - $trc_width - 2 + $vll_width + $vlr_width}] ;#+/2's for difference between border element widths and standard element single-width
set bbarwidth [expr {$content_or_title_width + 2 - $blc_width - $brc_width - 2 + $vll_width + $vlr_width}]
} else {
set contentwidth [expr $opt_width - $vll_width - $vlr_width] ;#content may be truncated
set tbarwidth [expr {$opt_width - $tlc_width - $trc_width}]
set bbarwidth [expr {$opt_width - $blc_width - $brc_width}]
}
set column [string repeat " " $contentwidth]
if {$hlt_width == 1} {
set tbar [string repeat $hlt $tbarwidth]
} else {
#possibly mixed width chars that make up hlt - string range won't get width right
set blank [string repeat " " $tbarwidth]
set count [expr {($tbarwidth / $hlt_width) + 1}]
set tbar [string repeat $hlt $count]
#set tbar [string range $tbar 0 $tbarwidth-1]
set tbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $tbar];#spaces for exposed halves of 2w chars instead of default replacement character
}
if {$hlb_width == 1} {
set bbar [string repeat $hlb $bbarwidth]
} else {
set blank [string repeat " " $bbarwidth]
set count [expr {($bbarwidth / $hlb_width) + 1}]
set bbar [string repeat $hlb $count]
#set bbar [string range $bbar 0 $bbarwidth-1]
set bbar [overtype::left -overflow 0 -exposed1 " " -exposed2 " " $blank $bbar]
}
}
}
#keep lhs/rhs separate? can we do vertical text on sidebars?

76
src/testansi/beastie.ans

@ -0,0 +1,76 @@
²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²
²²²²²²²²²²²²²²²²²²² °°°ÛÛ°°°°°°°°°°°°°°°°°°°°ÛÛ
°°°°°°°°°°°°°°°°°°°°°°°°ÛÛ°°°°°°°°°°°°°°°°°°°°
ÛÛ°°°  ÛÛ ÛÛÛ ÛÛ 
ÛÛÛ ÛÛ ÛÛ ÛÛÛ ÛÛ ÛÛ
Û ÛÛ ÛÛ ÛÛÛ Û ÜÜÜÜÜÜÜÜ
 Û ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û 
Û ÛÜÜ°² ²°ÜÜÛ Û Û ÛÛÛ ÛÛ ÛÛ 
ÛÛÛ Û²±°±°°±²Û 
ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û Û±²
°±±°±²Û Û ÛÛÛ ÛÛ ÛÛ ÛÛÛ 
Û °±°ß\±²/ß°±° Û ÛÛÛ
 ÛÛ ÛÛ ÛÛÛ Û ßÛ²±ÜOÞÝ
Oܱ²Ûß Û ÛÛÛ ÛÛ ÛÛ Û
ÛÛ Û Û±Û±°°±Û±Û Û ÛÛÛ 
ÛÛ ÛÛ ÛÛÛ Û ² ±²þþþþ²± ²
 Û ÛÛÛ ÛÛ ÛÛ Û±ÛÛ
²Û ²°±° ²°ÛÛ°² ±Û°± Û²ÛÛ
±Û ÛÛ ÛÛ Û²Û°±°²±±²
°±²°°²±²°°°±²Û°Û²±°Û²Û 
ÛÛ ÛÛ Û²°±ÛÛ°°°±²°±²
°°±°²Û°±Û²Û°±²±²ÛÛ±°²Û ÛÛ
 ÛÛ Û°±²±ÛÛ±°²°°°±°°
±²°²°±²°±±²°°Û°ÛÛ²±±Û 
 ÛÛ ÛÛ Û²±±ÛÛÛ°°±²
 ±°±°°²±²±°Û±±±° ±°±Û
Û²°±²Û ÛÛ ÛÛ Û°²²
ÛÛ±°± ±°±°±²°±²°±² ±²ÛÛ±²
±Û ÛÛ ÛÛ Û±ÛÛ±² 
 °±±°±°±²±Û± Û°ÛÛ±Û 
ÛÛ ÛÛ ÛÛÛ Û ±°°±²° °±²°
±² Û ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û 
°±°²±± °±²°²² Û ÛÛ
Û ÛÛ ÛÛ ÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÛÛÜÛÜ°±²
±²±²±°±²±²ÛÜÛÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÛ ÛÛ 
 ÛÛÜßÛÛ °±°±°²±°² ±°±°²±°² 
ÛÛßÜ ÛÛ²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²
²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²²² °°°ÛÛ°°°°°°°°°°°
°°°°°°°°°ÛÛ°°°°°°°°°°°°°°°°°°°°°°°°ÛÛ°°°°°°°°°
°°°°°°°°°°°ÛÛ°°°  ÛÛ Û
ÛÛ ÛÛ  ÛÛÛ ÛÛ ÛÛ ÛÛÛ 
ÛÛ  ÛÛÛ ÛÛ ÛÛ ÛÛÛ 
Û ÜÜÜÜÜÜÜÜ Û  ÛÛÛ ÛÛ ÛÛ 
ÛÛÛ Û Û ÛÜÜ°² ²°ÜÜÛ Û Û  
 ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û
²±°±°°±²Û  ÛÛÛ ÛÛ 
 ÛÛ ÛÛÛ Û Û±²°±±°±²Û Û  
 ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û 
°±°ß\±²/ß°±° Û  ÛÛÛ 
ÛÛ ÛÛ ÛÛÛ Û ßÛ²±ÜOÞÝ
Oܱ²Ûß Û  ÛÛÛ ÛÛ ÛÛ Û
ÛÛ Û Û±Û±°°±Û±Û Û  
ÛÛÛ ÛÛ ÛÛ ÛÛÛ Û 
² ±²þþþþ²± ² Û  ÛÛÛ ÛÛ
 ÛÛ Û°ÛÛ° Û ²°±° 
²°ÛÛ°² ±Û°± Û ²ÛÛ± Û 
 ÛÛ ÛÛ Û±Û±°²° 
°²±±²°±²°°²±²°°°±²Û°Û² °±
°Û² Û ÛÛ ÛÛ ÛÛÛ°°±²±²
 °±°±²°±²°°±°²Û°±Û²Û°±²±²±
 ±°±²°ÛÛÛ ÛÛ ÛÛ ÛÛÛ
°±²²°±²± °±±²°°°±°°±²°²°±²
°±±²°±Û²²Û ±±²ÛÛ±ÛÛÛ ÛÛ Û
Û ÛÛÛ °±²±±Û°±°±°± ±°±
°°²±²±°Û±±±° ±°±Û±²°±²Û° 
ÛÛÛ ÛÛ ÛÛ ÛÛÛ °±²°
²±°± ±°±°±²°±²°±² ±²±Û±²±² ÛÛ
Û ÛÛ ÛÛ ÛÛÛ ±°±² °
±±°±°±²±Û± Û°Û²± ÛÛÛ ÛÛ
 ÛÛ ÛÛÛ Û ±°°±²° °±²°±² 
 Û  ÛÛÛ ÛÛ ÛÛ ÛÛÛ 
Û °±°²±± °±²°²² Û 
 ÛÛÛ ÛÛ ÛÛ ÛÜÜÜÜÜÜÜÜÜÜÜÜÜÛÛÜÜÜ
ÜÜÛÜ°±²±²±²±°±²±²ÛÜÜÜÜÜÛÛÜÜÜÜÜÜÜÜÜÜÜÜÛ
 ÛÛ ÛÛÜßÛÛ °±°±°²±°² ±
°±°²±°²  ÛÛßÜ ÛÛ

72
src/testansi/fish.ans

@ -0,0 +1,72 @@

 
 ワ゚゚゚ ワワワワワワワワ゚゚ロワ ワワ ワ ワワ 
 
 ロ ワロ゚゚ ワ゚ワ ゚ワ゚ロ 
゚゚゚ ワ゚ 
 ワワ゚ ワ ロ゚ロ
ロ゚゚ ロ゚ ワ ワ ロワ
゚゚ 
 ワワワワロロロワ ワ゚ワ゚ロ゚ワワワ 
 ワワワワ 
 ワワ゚ロワロワ゚ロ
 ゚ワ ゚ ゚ ゚ ゚ ゚ワ゚ロワワ 
 ワロ゚゚゚ ロ 
 ワロ゚ロ ゚ロ 
ワ゚ワ ワ゚ワ ゚ ワ ゚ワ゚ ワ゚ ゚ ゚ワワ ワロ
゚ワ゚ ゚ ロ ロ
ワワワ゚ ロ ゚ ロ ワ ゚゚ワ゚ワ ワワ ゚ ゚ワワロ
ワ ワロワ゚ ワ ロ ワ
゚゚ワ ワロワ゚゚゚゚ワ
ロワワ ロ ゚ ゚ワ゚ ワ ロワ゚ワ ワ ゚ ワロワワ゚゚ロワ
ロワ ワロ ワワ ワ ロ 
ロ ロロワ ワロ ワロ
ロロロロロロロロロワ ロ
゚ ワ ワ ゚゚ワワ ワロロロロロロロ
ロワロワ ワロ゚ロ ロ ワ゚ 
ロ ゚゚゚゚゚ ロロロ
ロロロワロロロロロロ ゚ ワワ ロ ゚ロ ワ ゚
 ロロロロロロワロロロロ
ロロ゚ワワ゚ ワ゚゚ ワロ ワロ゚゚゚
ワ ロロロロロロロロロロロ゚
ロ ロワ゚ ワ ワ ゚ ワロ ロロロロ
ロロロロロロロロロ゚ロワ゚ワロ ワ゚ワ゚ 
ロ ロロワ ワロ ゚ロ
゚ワロ゚ ゚ロワ ロロ 
ロロロワロロワ゚ロ
ワワ゚ワ゚ワ゚ ワ ゚ワ ワロ ゚゚゚゚゚゚ 
ロロ゚ロ゚゚゚゚゚゚ ワ゚ワ ワロ 
 ロ゚ ゚ ロワ゚゚ ゚゚゚゚゚゚゚゚゚
ロ゚ワ゚ワ゚ワ゚ワロ ロ ゚ ゚ロ 
 ロロワ゚ワワ゚ ゚ワロ ワ゚ ワ 
 ゚ ロ ワワ ロ ゚ワ ゚ワ ゚ロ゚ワロワ゚ロワロ゚ワ 
゚ワ ワ゚ ワワワワワワワ  
゚゚ワ ゚ ゚ワ゚ロ゚ワ゚ワワ゚゚゚
゚゚゚゚゚゚゚ワワ ゚ ワロ 
ワ ゚ ロ゚ワ゚ロロワロロ ゚ロ ワ゚ワ゚ ワ
ロ゚ ゚ワ  ロ
゚ ゚゚ワ゚ワロワロ゚ ゚
ワワ ゚ワ ロ ゚ロ ワワ゚ロ ゚
ロワ ロ゚ ロ ロ ロロワ 
 ゚ロ ワロワワ゚ワ ロ゚ ワワ
ロロロロロロロロワワ ゚ロワ ワ ロ
 ワワ゚ ロ ロワ ゚ワ ロロ
ワ ゚゚ロロワワワワロ゚ ゚ロ
 ゚ワ゚゚ ロロロロロロロロロロロロワワ ゚゚゚
ロ ゚ワ ゚ワ ロ ゚ロワワ ゚
ロ ゚ロワロ
 ゚ワ゚ワワワ ゚ロロロロ゚ロ゚ロ゚ロロワロロロ゚゚ワロ
゚ ゚ロ ロ゚ワ ロ゚ 
 ゚゚゚ワワワ ゚
 ワ゚ワ ワ ゚ロワ ゚゚゚゚゚゚゚゚゚ 
 ワ゚゚゚ ワ ワロ ワ ロ゚ 
 ゚ワ ゚゚ワワ
ロ゚ロロロロワワワワワワワワワ゚ロ ゚
ワワ ゚ ロ゚ ワロ゚ 
 ワワワワワワワロワワワワロ
゚ロワロ゚ロ ロワワ゚ワワ゚ワロ ワ゚ワ゚ワワロ゚ワワ゚゚ロロワワワワワワワワ
 ワロ゚゚ワロ゚゚ロワ゚ワロロ゚
ロロロロロロワロ゚ワロロロ ロ゚ワ゚゚ロロロロ゚ロワロ゚ワ ロワ ワ゚ ゚ワワ゚ 
 (c) Magic-MARK-er Graphics

95
src/testansi/flower.ans

@ -0,0 +1,95 @@
      leahciM  
  イロロロロ ワ゚   イロ
ロロロ ワワ ゚゚゚゚ロロ゙
 ロロロロ゚ ゚゚゚    
       :yb neercS  
 イロロロロ     イロロ
ロロワ  ゚ ロロロロ ー イ
ロロロ゙ワ イロロロ゚ワワロ゚
゚゚                 
   イロロロワ ワ゚ロ 
 イロロロロワ ゚゚ ロロロ
ワ ー イロロロ゚ワ イロロロ
゚ ワワロロ゚゚゚       
         イロロロ  
ワ   イロロロワワ゚゚゚゚
ワワ  ーーロン イロロロ
゚ワ イイロロ゚  ワロ゚
ロロ゚゚ ロ     ゚゚゚ ゚゚゚
イロロロ     イロロロワ
 ゚゚ワワワ   ワ ー  
ロロワ゚ワロ ワワ イ
ロロ゚  ワワワロロロ゚   
  ワ゚゚゚゚゚ロロロ ゙ 
 ゙ワ イロロワ  ロワ
 ゚゚゚゚ーー゚ロ゚゚゚ロロ
ロ   ワ イロロ     ワワ
ワワワ      ワワワワワワ
ワンン アイ ゙ ワワワ
ワ   ワワワ  ワロロロロロ゚゚ロロロ
ロロロワ゚゚゚゚゚ワワロ   
                     
  ゙ン アイイ゙ ゙ロロ゚
゚゚゚ ゚゚゚゚゚゚ワロロロロロロロ
イイイ゚ロロロロロロロロロロ゚゚゚  
                     
 ン ゙ アアイワ ロロロロロ
ロ゚゚゚ロロロイイロロ゚゚ワロロイ
イイ゚゚イイイイイロロロロロロワワ゚
゚                   
  ン ン  イイン ロロ゚ロ
ロロロロロロロロロロイイイイイワワ゚゚
イイイイロロロロワワワワワロロ
 ゚                 
     ゙  ワイン イロワワ
ワロロロロロロロロロロワ゚ロワワワロロロワワワ
゚゚ロ゚゚ ワロロ ゚゚    
               ン    ワ
ワワーイロ゚ロロ゚゚゚ワワワワワ゚゚
イイロロロロ゚゚゚゚イイイイイロロロ
゚ ワワロ゚゚゚゚゚゚゚   
                 ゚ワワワ
゚゚ロロロロロロロロイイイイイロイイイイ
イイロロロ゚ロロロロイイイイロロロ
゚゚ ワワワワワワワワロ   
                 ワロロロ
ロロロロロロロイイイイイロロロ゚ロイイイ
イイイロロロ゚ワワワワロロロロロロロ
ロ゚                   
          ワ ゚゚ワロロロ
ロロロロロロロロロワヷン゚ロワ゚゚゚
゚゚゚ワワロ゚゙  ワ゚゚ワワワロロロ
゚゚                
            ロロワワワ
ワワワワワワワワ゚゚゚゚゚゚ロワ
 ゚゚゚ワロロ゚゚゚゚゚  ロロ
ワワワロ           
               ゚ ロロ
           ゚゚ワワワ゚゚
゚ロイ゙゙ロイ゚゚゚゚ワワワ゚゚ 
ロ゚  ワワワ          
                 ロロ
ワ          ゚ワ゚ロワ
ロロロイイロンン゙ロイロロロロロ
ロ゚ワロ  ロ           
                    
 ロロロ           ワ゚
ワワロロロロロロ゙ロアロ゙ロロロロ
ロロロワ゚ワ  ロ゚      
                     
  ワロロワ           
 ワワロ゚ワワワロロロロロロロロロロ
ワワワ゚ワ  ワ ロ゚   
                     
    ンロワ           
    ワワワワロ゚ワワワロロワワ
ワ゚ロワワワ    ワワロ  
                      
    ンワ            
        ワワワワワワワワワ
ワワワ         ワワワ     
                      
 ワワワ                
                     
                     

201
src/testansi/fruit.ans

@ -0,0 +1,201 @@
ß ß ß 
˛ß° Ţ° 
°    °Ü °
ß ÜÜŰßÄ
˛  ÜÜ  
°cŰ ÜŰ
 ßÄţßÜ  
  ßßŰÜ 
   Ü Ü°
Ü°ß ° Ü
   Ü Ű 
  ß °°Üß
Ü° Űß°°  
°ţÜ  ÜÜ
ß  ˛°   
° ßÜ ÜÜ
  ß  ß  
ß °ÜŰ ß Ü
Äßß ° ÜÜß
Ü°   ßÜ
ß Ű ßŰ 
Ü°Ü ÜŰ   
 ÜßÜÜß°
ŰţŰÜßÜ  
Ü     
°Ü    °
ß°° ŰÜ  
ßÜ ˛ Üß 
ÜŰe nÜŰÜ
  Ű°ßßß Ü
ŰŰÜÜúţÜ
 ł   ß 
ŰßąÜß ß
 ß Üß ß
° ÜÜßÜÜßß
 Ü ÜţÜß
ß  ÜŰ  Ü
 Ü  ß  ą
 ß° Ű 
ß    ß° ß
ŰżŰ Ű°Ű
Ü Ü °ÜÜÜ
.Üe ßÜß
 °°Ü°ß° 
ŰÜÜ° ÜÜ
 °  Ü 
ßß Ű Ü 
   Ű rŰÜ Ä
ÜßÜ°   Ü
ßßßÜŰÜ  ß
  ßÜ ß°
ß ÜŰ ßß
ßÜ  ą  
   Ü° ß 
 Ü۲  ß
  ß Űß Ä
°  łÜ °
Üß ß° Ü
°Ü ß°˛ŰŰ
  Ű  ţ߲
 ßßÜ  Üß
°ß  ß   Ü  
Ü ÜÜÜ 
ßÜ  aßß
 Ü ß Ü iß
ÜÜŰ ß   
˛ Üßß Ţ ą
 Ü Üßß 
°Ü ÜÜ   ßß
° Ű ˛ Ü Üy
ÜÜ° ˛ °
   ßÜß
  Ű ß Ü
ß Ü ÜÜ
    Ű   
° ˛Ü ܲ
˛ą Ű °
ß° Ü° ˛°
°  ßß   e
 °˛ ÜŰ ß
ŰÜß ŰŰß
Ü  ŰÜ ß 
 °ß  ą
° Üß  ß
Ü  ÜŰß 
 ß  ß۲
ßßÜßßÜ°Ű
ß  °  ţ
߲ ß  
ŰŰ  °  
 ܲ  Üß
   ŰÜÜÜ
ßß°ß Üß
ßß°°  ß
ß° Ű Ű
ßß ß°ß°
ß  ßÜÜ Ä
˛°° Ü° 
°ßÜÜ ßh
ß  Ü  ßßß
ß   ÜŰß ß
ŰŰÜ  °  
ßÜ  ˛ß 
ŰÜß Ű ß
°Ü  Ű ÜÜ
 Ű°ÜŰ :ą
łß Ü ß° 
  Ü ß 
߲˛ ßÜŰ
    ܲ° 
  Ü°ą  
°Üܲ۲ Ü
 °  úÜ
ŰÜŰ ßŰÜ
 ßţÜ   
Ü  ˛ Ü  
ß ß° Ű 
 Ü ß°˛° 
Üß°ßßß
   ßŰ Üß
ÜÜßß Ü
 °  Ü Üß
Ű ŰÜ ß˛
 Ű  °SÜ
ŰßßßÜ ß
   ţÜ ß 
° ° Üţ Űß
Ű ÜÜßß 
ßŰ MÜÜą
ܲܰ°  °
° ţß  Ű
°Ű°ßÜ ß
 ßßÜÜŰÜ
Ű ßß  Ű °
ßÜ   ° 
ßţßß °ß
°ß.Ü  Ü 
ŰŰßßŰßÜ
 ˛°Ü  Ü
° ˛Ü˛ ß
ß.ÜÜ  °
ßÜ°  ßÜß
Ű   Üß°
˛Ü˛Ü ß 
ßß °ł Ű
 ß  Ü  
° °ß  ß°
Üß ţß   
 Ű Ü°Ü 
ß ˛ßßÚÜ
ÜÜŰ Ű ˛
 °Ü  ˛ß
°ß°ß   
Ű ß ÜŰÜ
ÜÜ° °  
ß°ßÜ°°°ß 
Ü˛Ű ßÜÜ
 ß  ŰÜb
˛ÜÜ ° Ü
Ü   Ü ßÜ
 ܲ   Ü 
  Ü°° ţ
 Ä  ŰßÜ
 °ÜÜ  Ű߲
°Ü Ü° ۲
  ÜŰÜ°Ü
Ü ßß ß°
ß Ü Ü ÜÜ
  °ßßßÜ 
ú Ű°Ű°
Ű° Ü°ß 
ß   ˛Üß 
 Ü   ßÜ
  Ü °ß
 ÜÜ Ü ß 
°Ü°  ÜŰ
°   ŰÜß
Ü ß Üß ˛
Üßß° ß 
  Ü Ű °Ű
ŰŰ  ţ  
ŰÜ  ß  
c  ßß  ŰÜ
Ű  ܲ°
ß°  Ü°ß 
 Ü ŰÜŰ ß
 ÜÜÜßÜÜÜ
Ű°Ű °l ß
ß  Ü°  ß
Üß  Ü ß
Ü  úÜ ˛
    ßÜ ßÄ
Ű°ßß°ßŰ
Ü ˛ ˛°
ŰßŰ ß  
ŰÜ   Üß
   ßÜ  ߲
Ű ßąłŰŰÜ
 ÜŰ  Ű  
Ü  °° 

20
src/testansi/punk.ansi

@ -0,0 +1,20 @@
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳\\\_╳╳╳╳╳╳_///╳╳╳
╳╳╳\@>╳╳╳╳<@/╳╳╳
╳╳╳╳|~╳╳╳╳╳╳~|╳╳╳╳
\_--_╳╳╳╳_--_/
╳╳╳╳\\╳/╳╳╳╳\╳//╳╳╳╳
╳╳╳╳/╳\╳╳╳╳╳╳/╳\╳╳╳╳
╳╳_+╳╳╳+_╳╳_+╳╳╳+_╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳
╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳╳

1236
src/vendormodules/overtype-1.5.8.tm

File diff suppressed because it is too large Load Diff

2194
src/vendormodules/overtype-1.5.9.tm

File diff suppressed because it is too large Load Diff

1
src/vendormodules/textutil/wcswidth-35.1.tm

@ -8,6 +8,7 @@
# Author: Sean Woods <yoda@etoyoc.com>
###
package provide textutil::wcswidth 35.1
namespace eval ::textutil {}
proc ::textutil::wcswidth_type char {
if {$char == 161} { return A }
if {$char == 164} { return A }

Loading…
Cancel
Save