Browse Source

fifo2 & startup interim fixes. sqids lib. vfs tidy

master
Julian Noble 2 months ago
parent
commit
2b5cef9d36
  1. 6411
      src/bootsupport/modules/metaface-1.2.5.tm
  2. 673
      src/bootsupport/modules/modpod-0.1.4.tm
  3. 4892
      src/bootsupport/modules/overtype-1.7.2.tm
  4. 93
      src/bootsupport/modules/overtype-1.7.4.tm
  5. 24
      src/bootsupport/modules/punk-0.1.tm
  6. 14
      src/bootsupport/modules/punk/ansi-0.1.1.tm
  7. 1
      src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm
  8. 10574
      src/bootsupport/modules/punk/args-0.2.tm
  9. 16
      src/bootsupport/modules/punk/console-0.1.1.tm
  10. 18
      src/bootsupport/modules/punk/du-0.1.0.tm
  11. 4581
      src/bootsupport/modules/punk/lib-0.1.3.tm
  12. 4981
      src/bootsupport/modules/punk/lib-0.1.4.tm
  13. 5463
      src/bootsupport/modules/punk/lib-0.1.5.tm
  14. 122
      src/bootsupport/modules/punk/lib-0.1.6.tm
  15. 37
      src/bootsupport/modules/punk/mix-0.2.tm
  16. 38
      src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  17. 189
      src/bootsupport/modules/punk/ns-0.1.0.tm
  18. 73
      src/bootsupport/modules/punk/repl-0.1.2.tm
  19. 276
      src/bootsupport/modules/punk/repl/codethread-0.1.0.tm
  20. 836
      src/bootsupport/modules/punk/winlnk-0.1.0.tm
  21. 28
      src/bootsupport/modules/shellfilter-0.2.2.tm
  22. 829
      src/bootsupport/modules/shellthread-1.6.1.tm
  23. 5680
      src/bootsupport/modules/tomlish-1.1.2.tm
  24. 6002
      src/bootsupport/modules/tomlish-1.1.3.tm
  25. 6199
      src/bootsupport/modules/tomlish-1.1.4.tm
  26. 6973
      src/bootsupport/modules/tomlish-1.1.5.tm
  27. 9470
      src/bootsupport/modules/tomlish-1.1.6.tm
  28. 9470
      src/bootsupport/modules/tomlish-1.1.7.tm
  29. 40
      src/lib/app-punk/repl.tcl
  30. 34
      src/lib/app-punkshell/punkshell.tcl
  31. 251
      src/lib/app-shellspy/shellspy.tcl
  32. 2
      src/lib/app_shell/app_shell.tcl
  33. 2
      src/lib/app_shellrun/app_shellrun.tcl
  34. 93
      src/modules/overtype-999999.0a1.0.tm
  35. 2
      src/modules/poshinfo-999999.0a1.0.tm
  36. 34
      src/modules/punk-0.1.tm
  37. 1
      src/modules/punk/aliascore-999999.0a1.0.tm
  38. 14
      src/modules/punk/ansi-999999.0a1.0.tm
  39. 1
      src/modules/punk/ansi/sauce-999999.0a1.0.tm
  40. 144
      src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm
  41. 3
      src/modules/punk/args/moduledoc/iocp-buildversion.txt
  42. 11
      src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm
  43. 2
      src/modules/punk/cap/handlers/templates-999999.0a1.0.tm
  44. 24
      src/modules/punk/cesu-999999.0a1.0.tm
  45. 4
      src/modules/punk/char-999999.0a1.0.tm
  46. 2
      src/modules/punk/config-0.1.tm
  47. 16
      src/modules/punk/console-999999.0a1.0.tm
  48. 36
      src/modules/punk/du-999999.0a1.0.tm
  49. 415
      src/modules/punk/lib-999999.0a1.0.tm
  50. 32
      src/modules/punk/mix/cli-999999.0a1.0.tm
  51. 16
      src/modules/punk/mix/commandset/module-999999.0a1.0.tm
  52. 30
      src/modules/punk/mix/util-999999.0a1.0.tm
  53. 38
      src/modules/punk/nav/fs-999999.0a1.0.tm
  54. 192
      src/modules/punk/ns-999999.0a1.0.tm
  55. 23
      src/modules/punk/packagepreference-999999.0a1.0.tm
  56. 206
      src/modules/punk/repl-999999.0a1.0.tm
  57. 20
      src/modules/punkcheck-0.1.0.tm
  58. 470
      src/modules/shellfilter-999999.0a1.0.tm
  59. 2
      src/modules/shellfilter-buildversion.txt
  60. 177
      src/modules/shellthread-999999.0a1.0.tm
  61. 1
      src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test
  62. 42
      src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test
  63. 59
      src/modules/textblock-999999.0a1.0.tm
  64. 93
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm
  65. 60
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm
  66. 16
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm
  67. 1
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm
  68. 200
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm
  69. 20
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  70. 176
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  71. 8
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  72. 38
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  73. 189
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm
  74. 101
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm
  75. 91
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm
  76. 14
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm
  77. 816
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm
  78. 93
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm
  79. 60
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm
  80. 16
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm
  81. 1
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm
  82. 200
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm
  83. 20
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm
  84. 176
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  85. 8
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/cli-0.3.1.tm
  86. 38
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  87. 189
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm
  88. 101
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl-0.1.2.tm
  89. 91
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm
  90. 14
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm
  91. 3399
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm
  92. 8
      src/runtime/mapvfs.config
  93. BIN
      src/vendorlib_tcl8/win32-x86_64/Memchan2.3/Memchan23.dll
  94. BIN
      src/vendorlib_tcl8/win32-x86_64/Memchan2.3/libMemchanstub23.a
  95. 2
      src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl
  96. 67
      src/vendormodules/include_modules.config
  97. 931
      src/vendormodules/sqids-0.3.1.tm
  98. BIN
      src/vendormodules_tcl9/Thread-3.0b3.tm
  99. BIN
      src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm
  100. 113
      src/vfs/_config/punk_main.tcl
  101. Some files were not shown because too many files have changed in this diff Show More

6411
src/bootsupport/modules/metaface-1.2.5.tm

File diff suppressed because it is too large Load Diff

673
src/bootsupport/modules/modpod-0.1.4.tm

@ -1,673 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024
#
# @@ Meta Begin
# Application modpod 0.1.4
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin modpod_module_modpod 0 0.1.4]
#[copyright "2024"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
#[require modpod]
#[keywords module]
#[description]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of modpod
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by modpod
#[list_begin itemized]
package require Tcl 8.6-
package require struct::set ;#review
package require punk::lib
package require punk::args
#*** !doctools
#[item] [package {Tcl 8.6-}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
#changes
#0.1.4 - when mounting with vfs::zip (because zipfs not available) - mount relative to executable folder instead of module dir
# (given just a module name it's easier to find exepath than look at package ifneeded script to get module path)
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval modpod {
namespace export {[a-z]*}; # Convention: export all lowercase
variable connected
if {![info exists connected(to)]} {
set connected(to) list
}
variable modpodscript
set modpodscript [info script]
if {[string tolower [file extension $modpodscript]] eq ".tcl"} {
set connected(self) [file dirname $modpodscript]
} else {
#expecting a .tm
set connected(self) $modpodscript
}
variable loadables [info sharedlibextension]
variable sourceables {.tcl .tk} ;# .tm ?
#*** !doctools
#[subsection {Namespace modpod}]
#[para] Core API functions for modpod
#[list_begin definitions]
#old tar connect mechanism - review - not needed?
proc connect {args} {
puts stderr "modpod::connect--->>$args"
set argd [punk::args::parse $args withdef {
@id -id ::modpod::connect
-type -default ""
@values -min 1 -max 1
path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-<pkg>-<ver> folder (unwrapped modpod)"
}]
catch {
punk::lib::showdict $argd ;#heavy dependencies
}
set opt_path [dict get $argd values path]
variable connected
set original_connectpath $opt_path
set modpodpath [modpod::system::normalize $opt_path] ;#
if {$modpodpath in $connected(to)} {
return [dict create ok ALREADY_CONNECTED]
}
lappend connected(to) $modpodpath
set connected(connectpath,$opt_path) $original_connectpath
set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}]
set connected(location,$modpodpath) [file dirname $modpodpath]
set connected(startdata,$modpodpath) -1
set connected(type,$modpodpath) [dict get $argd opts -type]
set connected(fh,$modpodpath) ""
if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} {
set connected(type,$modpodpath) "unwrapped"
lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath)
set this_pkg_tm_folder [file dirname [file dirname $modpodpath]]
} else {
#connect to .tm but may still be unwrapped version available
lassign [::split [file rootname [file tail $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath)
set this_pkg_tm_folder [file dirname $modpodpath]
if {$connected(type,$modpodpath) ne "unwrapped"} {
#Not directly connected to unwrapped version - but may still be redirected there
set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)]
if {[file exists $unwrappedFolder]} {
#folder with exact version-match must exist for redirect to 'unwrapped'
set con(type,$modpodpath) "modpod-redirecting"
}
}
}
set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm"
set connected(tmfile,$modpodpath)
set tail_segments [list]
set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]]
set lcase_modulepaths [string tolower [tcl::tm::list]]
foreach lc_mpath $lcase_modulepaths {
set mpath_segments [file split $lc_mpath]
if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} {
set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end]
break
}
}
if {[llength $tail_segments]} {
set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require
} else {
set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)]
}
switch -exact -- $connected(type,$modpodpath) {
"modpod-redirecting" {
#redirect to the unwrapped version
set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl]
}
"unwrapped" {
if {[info commands ::thread::id] ne ""} {
set from [pid],[thread::id]
} else {
set from [pid]
}
#::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath"
return [list ok ""]
}
default {
#autodetect .tm - zip/tar ?
#todo - use vfs ?
#connect to tarball - start at 1st header
set connected(startdata,$modpodpath) 0
set fh [open $modpodpath r]
set connected(fh,$modpodpath) $fh
fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {}
if {$connected(startdata,$modpodpath) >= 0} {
#verify we have a valid tar header
if {![catch {::modpod::system::tar::readHeader [read $fh 512]}]} {
seek $fh $connected(startdata,$modpodpath) start
return [list ok $fh]
} else {
#error "cannot verify tar header"
#try zipfs
if {[info commands tcl::zipfs::mount] ne ""} {
}
}
}
lpop connected(to) end
set connected(startdata,$modpodpath) -1
unset connected(fh,$modpodpath)
catch {close $fh}
return [dict create err {Does not appear to be a valid modpod}]
}
}
}
proc disconnect {{modpod ""}} {
variable connected
if {![llength $connected(to)]} {
return 0
}
if {$modpod eq ""} {
puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]"
set modpod [lindex $connected(to) end]
}
if {[set posn [lsearch $connected(to) $modpod]] == -1} {
puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod"
return 0
}
if {[string length $connected(fh,$modpod)]} {
close $connected(fh,$modpod)
}
array unset connected *,$modpod
set connected(to) [lreplace $connected(to) $posn $posn]
return 1
}
proc get {args} {
set argd [punk::args::parse $args withdef {
@id -id ::modpod::get
-from -default "" -help "path to pod"
@values -min 1 -max 1
filename
}]
set frompod [dict get $argd opts -from]
set filename [dict get $argd values filename]
variable connected
#//review
set modpod [::modpod::system::connect_if_not $frompod]
set fh $connected(fh,$modpod)
if {$connected(type,$modpod) eq "unwrapped"} {
#for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder
if {[string range $filename 0 0 eq "/"]} {
#absolute path (?)
set path [file join $connected(location,$modpod) .. [string trim $filename /]]
} else {
#relative path - use #modpod-xxx as base
set path [file join $connected(location,$modpod) $filename]
}
set fd [open $path r]
#utf-8?
#fconfigure $fd -encoding iso8859-1 -translation binary
return [list ok [lindex [list [read $fd] [close $fd]] 0]]
} else {
#read from vfs
puts stderr "get $filename from wrapped pod '$frompod' not implemented"
}
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace modpod ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval modpod::lib {
namespace export {[a-z]*}; # Convention: export all lowercase
namespace path [namespace parent]
#*** !doctools
#[subsection {Namespace modpod::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
proc is_valid_tm_version {versionpart} {
#Needs to be suitable for use with Tcl's 'package vcompare'
if {![catch [list package vcompare $versionparts $versionparts]]} {
return 1
} else {
return 0
}
}
#zipfile is a pure zip at this point - ie no script/exe header
proc make_zip_modpod {args} {
set argd [punk::args::parse $args withdef {
@id -id ::modpod::lib::make_zip_modpod
-offsettype -default "archive" -choices {archive file} -help\
"Whether zip offsets are relative to start of file or start of zip-data within the file.
'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip,
but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip)
info-zip's 'zip -A' can sometimes convert archive-relative to file-relative.
-offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'"
@values -min 2 -max 2
zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries"
outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm"
}]
set zipfile [dict get $argd values zipfile]
set outfile [dict get $argd values outfile]
set opt_offsettype [dict get $argd opts -offsettype]
set mount_stub [string map [list %offsettype% $opt_offsettype] {
#zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped.
#Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file.
#generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% <zipfile> <tmfile>
if {[catch {file normalize [info script]} modfile]} {
error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)"
}
if {$modfile eq "" || ![file exists $modfile]} {
error "modpod zip stub error. Unable to determine module path"
}
set moddir [file dirname $modfile]
set exedir [file dirname [file normalize [info nameofexecutable]]]
set mod_and_ver [file rootname [file tail $modfile]]
lassign [split $mod_and_ver -] moduletail version
#determine module namespace so we can mount appropriately
proc intersect {A B} {
if {[llength $A] == 0} {return {}}
if {[llength $B] == 0} {return {}}
if {[llength $B] > [llength $A]} {
set res $A
set A $B
set B $res
}
set res {}
foreach x $A {set ($x) {}}
foreach x $B {
if {[info exists ($x)]} {
lappend res $x
}
}
return $res
}
set lcase_tmfile_segments [string tolower [file split $moddir]]
set lcase_modulepaths [string tolower [tcl::tm::list]]
foreach lc_mpath $lcase_modulepaths {
set mpath_segments [file split $lc_mpath]
if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} {
set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail
break
}
}
if {[llength $tail_segments]} {
set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require
set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver
} else {
set fullpackage $moduletail
set mount_at #modpod/#mounted-modpod-$mod_and_ver
}
if {[info commands tcl::zipfs::mount] ne ""} {
#argument order changed to be consistent with vfs::zip::Mount etc
#early versions: zipfs::Mount mountpoint zipname
#since 2023-09: zipfs::Mount zipname mountpoint
#don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance)
#This is presumably related to // being interpreted as a network path
set mountpoints [dict keys [tcl::zipfs::mount]]
if {"//zipfs:/$mount_at" ni $mountpoints} {
#despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it
if {[catch {
#tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?)
#puts "tcl::zipfs::mount $modfile $mount_at"
tcl::zipfs::mount $modfile $mount_at
} errM]} {
#try old api
if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} {
puts stderr "modpod stub>>> tcl::zipfs::mount <file> <mountpoint> failed.\nbut old api: tcl::zipfs::mount <mountpoint> <file> succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile"
puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API"
}
}
if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} {
puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]"
#tcl::zipfs::unmount //zipfs:/$mount_at
error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage"
}
}
# #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form
source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm
} else {
#fallback to slower vfs::zip
#NB. We don't create the intermediate dirs - but the mount still works
if {![file exists $exedir/$mount_at]} {
if {[catch {package require vfs::zip} errM]} {
set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)"
append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place."
append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile"
error $msg
} else {
set fd [vfs::zip::Mount $modfile $exedir/$mount_at]
if {![file exists $exedir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} {
vfs::zip::Unmount $fd $exedir/$mount_at
error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage"
}
}
}
source $exedir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm
}
#zipped data follows
}]
#todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating?
append mount_stub \x1A
modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace modpod::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
namespace eval modpod::system {
#*** !doctools
#[subsection {Namespace modpod::system}]
#[para] Internal functions that are not part of the API
#deflate,store only supported
#zipfile here is plain zip - no script/exe prefix part.
proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} {
set inzip [open $zipfile r]
fconfigure $inzip -encoding iso8859-1 -translation binary
set out [open $outfile w+]
fconfigure $out -encoding iso8859-1 -translation binary
puts -nonewline $out $mount_stub
set stuboffset [tell $out]
lappend report "stub size: $stuboffset"
fcopy $inzip $out
close $inzip
set size [tell $out]
lappend report "modpod::system::make_mountable_zip"
lappend report "tmfile : [file tail $outfile]"
lappend report "output size : $size"
lappend report "offsettype : $offsettype"
if {$offsettype eq "file"} {
#make zip offsets relative to start of whole file including prepended script.
#same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10
#2025 - zipfs mkimg fixed to use 'archive' offset.
#not editable by 7z,nanazip,peazip
#we aren't adding any new files/folders so we can edit the offsets in place
#Now seek in $out to find the end of directory signature:
#The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text
if {$size < 65559} {
set tailsearch_start 0
} else {
set tailsearch_start [expr {$size - 65559}]
}
seek $out $tailsearch_start
set data [read $out]
#EOCD - End of Central Directory record
#PK\5\6
set start_of_end [string last "\x50\x4b\x05\x06" $data]
#set start_of_end [expr {$start_of_end + $seek}]
#incr start_of_end $seek
set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}]
lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn"
seek $out $filerelative_eocd_posn
set end_of_ctrl_dir [read $out]
binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
lappend report "End of central directory: [array get eocd]"
seek $out [expr {$filerelative_eocd_posn+16}]
#adjust offset of start of central directory by the length of our sfx stub
puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]]
flush $out
seek $out $filerelative_eocd_posn
set end_of_ctrl_dir [read $out]
binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
# 0x06054b50 - end of central dir signature
puts stderr "$end_of_ctrl_dir"
puts stderr "comment_len: $eocd(comment_len)"
puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]"
lappend report "New dir offset: $eocd(diroffset)"
lappend report "Adjusting $eocd(totalnum) zip file items."
catch {
punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies
}
seek $out $eocd(diroffset)
for {set i 0} {$i <$eocd(totalnum)} {incr i} {
set current_file [tell $out]
set fileheader [read $out 46]
puts --------------
puts [ansistring VIEW -lf 1 $fileheader]
puts --------------
#binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
# x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
set ::last_header $fileheader
puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])"
puts "ver: $x(version)"
puts "method: $x(method)"
#PK\1\2
#33639248 dec = 0x02014b50 - central directory file header signature
if { $x(sig) != 33639248 } {
error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]"
}
foreach size $x(lengths) var {filename extrafield comment} {
if { $size > 0 } {
set x($var) [read $out $size]
} else {
set x($var) ""
}
}
set next_file [tell $out]
lappend report "file $i: $x(offset) $x(sizes) $x(filename)"
seek $out [expr {$current_file+42}]
puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]]
#verify:
flush $out
seek $out $current_file
set fileheader [read $out 46]
lappend report "old $x(offset) + $stuboffset"
binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
lappend report "new $x(offset)"
seek $out $next_file
}
}
close $out
#pdict/showdict reuire punk & textlib - ie lots of dependencies
#don't fall over just because of that
catch {
punk::lib::showdict -roottype list -chan stderr $report
}
#puts [join $report \n]
return
}
proc connect_if_not {{podpath ""}} {
upvar ::modpod::connected connected
set podpath [::modpod::system::normalize $podpath]
set docon 0
if {![llength $connected(to)]} {
if {![string length $podpath]} {
error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified"
} else {
set docon 1
}
} else {
if {![string length $podpath]} {
set podpath [lindex $connected(to) end]
puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]"
} else {
if {$podpath ni $connected(to)} {
set docon 1
}
}
}
if {$docon} {
if {[lindex [modpod::connect $podpath]] 0] ne "ok"} {
error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod"
} else {
return $podpath
}
}
#we were already connected
return $podpath
}
proc myversion {} {
upvar ::modpod::connected connected
set script [info script]
if {![string length $script]} {
error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod"
}
set fname [file tail [file rootname [file normalize $script]]]
set scriptdir [file dirname $script]
if {![string match "#modpod-*" $fname]} {
lassign [lrange [split $fname -] end-1 end] _pkgname version
} else {
lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version
if {![string length $version]} {
#try again on the name of the containing folder
lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version
#todo - proper walk up the directory tree
if {![string length $version]} {
#try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod)
lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version
}
}
}
#tarjar::Log debug "'myversion' determined version for [info script]: $version"
return $version
}
proc myname {} {
upvar ::modpod::connected connected
set script [info script]
if {![string length $script]} {
error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod"
}
return $connected(fullpackage,$script)
}
proc myfullname {} {
upvar ::modpod::connected connected
set script [info script]
#set script [::tarjar::normalize $script]
set script [file normalize $script]
if {![string length $script]} {
error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar"
}
return $::tarjar::connected(fullpackage,$script)
}
proc normalize {path} {
#newer versions of Tcl don't do tilde sub
#Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths)
# we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function.
set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes..
set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after
set path [file normalize $path]
#set path [string tolower $path] ;#must do this after file normalize
return [string map [list $matilda ~] $path] ;#get our tildes back.
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide modpod [namespace eval modpod {
variable pkg modpod
variable version
set version 0.1.4
}]
return
#*** !doctools
#[manpage_end]

4892
src/bootsupport/modules/overtype-1.7.2.tm

File diff suppressed because it is too large Load Diff

93
src/bootsupport/modules/overtype-1.7.4.tm

@ -401,13 +401,20 @@ tcl::namespace::eval overtype {
set opt_console [tcl::dict::get $opts -console]
#--------------------------------------------------------------------------
#TODO
#REVIEW - punk::console package may not be loaded
set cursor_style_overtype {3 underline-blink}
set cursor_style_insert {5 beam-blink}
if {$opt_insert_mode} {
punk::console::cursor_style -console $opt_console $cursor_style_insert
set initial_cursor_style $cursor_style_insert
} else {
set initial_cursor_style $cursor_style_overtype
}
catch {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#--------------------------------------------------------------------------
# ----------------------------
# -experimental dev flag to set flags etc
@ -695,22 +702,23 @@ tcl::namespace::eval overtype {
#review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary -
#but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l
set renderopts [list -experimental $opt_experimental\
-cp437 $opt_cp437\
-info 1\
-crm_mode [tcl::dict::get $vtstate crm_mode]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-reverse_mode [tcl::dict::get $vtstate reverse_mode]\
-cursor_restore_attributes $cursor_saved_attributes\
-transparent $opt_transparent\
-width [tcl::dict::get $vtstate renderwidth]\
-exposed1 $opt_exposed1\
-exposed2 $opt_exposed2\
-expand_right $opt_expand_right\
-cursor_column $col\
-cursor_row $row\
-overtext_type $overtext_type\
set renderopts [list -experimental $opt_experimental {*}{
} -cp437 $opt_cp437 {*}{
} -info 1 {*}{
} -crm_mode [tcl::dict::get $vtstate crm_mode] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -reverse_mode [tcl::dict::get $vtstate reverse_mode] {*}{
} -cursor_restore_attributes $cursor_saved_attributes {*}{
} -transparent $opt_transparent {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -exposed1 $opt_exposed1 {*}{
} -exposed2 $opt_exposed2 {*}{
} -expand_right $opt_expand_right {*}{
} -cursor_column $col {*}{
} -cursor_row $row {*}{
} -overtext_type $overtext_type {*}{
}
]
set rinfo [renderline {*}$renderopts $undertext $overtext]
@ -940,14 +948,15 @@ tcl::namespace::eval overtype {
puts stdout ">>>renderspace<<<[a+ red bold]overflow_right during restore_cursor[a]"
set sub_info [overtype::renderline\
-info 1\
-width [tcl::dict::get $vtstate renderwidth]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-expand_right [tcl::dict::get $opts -expand_right]\
""\
$overflow_right\
set sub_info [overtype::renderline {*}{
} -info 1 {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -expand_right [tcl::dict::get $opts -expand_right] {*}{
} "" {*}{
} $overflow_right {*}{
}
]
set foldline [tcl::dict::get $sub_info result]
tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..?
@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype {
}
#JMN
if {[tcl::dict::get $vtstate insert_mode]} {
puts "setting cursor to insert style"
punk::console::cursor_style -console $opt_console $cursor_style_insert
} else {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#REVIEW - we don't want to emit cursor_style ANSI unless it changes.
#if {[tcl::dict::get $vtstate insert_mode]} {
# puts "setting cursor to insert style"
# punk::console::cursor_style -console $opt_console $cursor_style_insert
#} else {
# punk::console::cursor_style -console $opt_console $cursor_style_overtype
#}
#puts "renderedrow_max: $renderedrow_max"
#check for null lines below renderedrow_max (and at tail) and trim.
@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype {
#broken:
#todo - renderline -overflow is invalid.
# we need renderline to support -expand_left ??
set rinfo [renderline\
-info 1\
-insert_mode 0\
-transparent $opt_transparent\
-exposed1 $opt_exposed1 -exposed2 $opt_exposed2\
-overflow $opt_overflow\
-startcolumn [expr {1 + $startoffset}]\
$undertext $overtext]
set rinfo [renderline {*}{
} -info 1 {*}{
} -insert_mode 0 {*}{
} -transparent $opt_transparent {*}{
} -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 {*}{
} -overflow $opt_overflow {*}{
} -startcolumn [expr {1 + $startoffset}] {*}{
} $undertext $overtext {*}{
}
]
set replay_codes [tcl::dict::get $rinfo replay_codes]
set rendered [tcl::dict::get $rinfo result]
if {!$opt_overflow} {
@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype {
-crm_mode -default 0 -type boolean
-autowrap_mode -default 1 -type boolean
-reverse_mode -default 0 -type boolean
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\
-choicelabels\
{
1 "return a dict with raw fields"
2 "return a dict using ansistring VIEW"

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

@ -283,14 +283,6 @@ namespace eval punk {
#set path "[file dirname [info nameofexecutable]];.;"
set path "[file dirname [info nameofexecutable]];"
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
# ------------------------
#Note that unlike an ordinary Tcl array - the linked ::env behaves differently.
@ -307,6 +299,15 @@ namespace eval punk {
}
# ------------------------
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
#change2
if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
set lookfor [list $name]
@ -5492,9 +5493,10 @@ namespace eval punk {
#ctrl-c propagation also needs to be considered
set teehandle punksh
uplevel 1 [list ::catch \
[list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \
::tcl::UnknownResult ::tcl::UnknownOptions]
uplevel 1 [list ::catch {*}{
} [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{
} ::tcl::UnknownResult ::tcl::UnknownOptions
]
if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} {
dict set ::tcl::UnknownOptions -code error

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

@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi {
@cmd -name punk::ansi::sauce -summary\
"SAUCE info from file"\
-help\
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data."
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data.
Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format
that was commonly used in old ANSI art files to store information about the file, such as
title, author, group, date, and comments.
It may also have fields to specify the number of columns and rows in the ANSI art,
as well as flags for specific display attributes.
It may also be used on other types of files such as bitmap, vector, audio, binarytext,
xbin, archive and executable files.
It is a 128-byte block of data that is typically appended to the end of a file.
https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm"
-encoding -default iso8859-1 -type string -help\
"The default iso8859-1 is equivalent to binary ans should
work in the usual case.

1
src/bootsupport/modules/punk/ansi/sauce-0.1.0.tm

@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce {
variable PUNKARGS
variable PUNKARGS_aliases
#https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm
lappend PUNKARGS [list {
@id -id "(package)punk::ansi::sauce"
@package -name "punk::ansi::sauce" -help\

10574
src/bootsupport/modules/punk/args-0.2.tm

File diff suppressed because it is too large Load Diff

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

@ -44,7 +44,14 @@
#[list_begin itemized]
package require Tcl 8.6-
#----------------------------------------------------
#Although we need to be in an environment with Thread available to use punk::console,
# we don't want to require Thread as a hard dependency in the interp we're running in.
# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp.
#TODO.
package require Thread ;#tsv required to sync is_raw
#----------------------------------------------------
package require punk::ansi
package require punk::args
#*** !doctools
@ -282,7 +289,7 @@ namespace eval punk::console {
ignore the regex match 'ok' response
and keep going."
-return -type string -default payload -choices {payload dict} -choicelabels {
dict\
dict
"dict with keys prefix,response,payload,all"
} -help\
"Return format"
@ -290,12 +297,12 @@ namespace eval punk::console {
-console -default {stdin stdout} -type list -help\
"console/terminal (currently list of in/out channels) (todo - object?)"
-passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels {
none\
none
{ ANSI sent without any passthrough wrapping.
A terminal multiplexer such as tmux,screen,zellij may
not pass the request through to the underlying terminal(s)
This is the recommended/normal value for the option.}
tmux\
tmux
{ Wrap ANSI sequence with tmux passthrough sequence.
\x1bPtmux\;<originalsequence_with_escapes_doubled>\x1b\\
Note that a tmux session could be connected to multiple
@ -304,7 +311,7 @@ namespace eval punk::console {
Passthrough should generally be avoided except for debug/test
purposes.
}
auto\
auto
{ Use existence of ::env(TMUX) to detect tmux and
send tmux passthrough sequence.
Not recommended except for debug/test purposes.
@ -2779,6 +2786,7 @@ namespace eval punk::console {
This allows querying the current style and then re-setting it after temporarily changing it."
}]
}
proc cursor_style {args} {
set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style]
lassign [dict values $argd] leaders opts values

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

@ -2114,15 +2114,15 @@ namespace eval punk::du {
}
proc du_dirlisting_tclvfs {folderpath args} {
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

4581
src/bootsupport/modules/punk/lib-0.1.3.tm

File diff suppressed because it is too large Load Diff

4981
src/bootsupport/modules/punk/lib-0.1.4.tm

File diff suppressed because it is too large Load Diff

5463
src/bootsupport/modules/punk/lib-0.1.5.tm

File diff suppressed because it is too large Load Diff

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

@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check {
if {"windows" ne $::tcl_platform(platform)} {
set bug 0
} else {
set tmpdir [file tempdir] ;#tcl 9+
set testfile [file join $tmpdir "bugtest"]
if {![catch {file tempdir} tmpdir]} {
#tcl 9+ has 'file tempdir'
set testfile [file join $tmpdir "bugtest"]
} else {
#fallback for older tcl versions - use env TEMP/TMP or current directory
set tmpdir ""
foreach e {TEMP TMP} {
if {[info exists ::env($e)] && [file isdirectory ::env($e)]} {
set tmpdir ::env($e)
break
}
}
if {$tmpdir eq ""} {
#no env vars - fallback to current directory
set tmpdir [pwd]
}
set testfile [file join $tmpdir "bugtest"]
}
set fd [open $testfile w]
puts $fd test
close $fd
set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*]
if {[file exists $testfile]} {
file delete $testfile
}
foreach r $globresult {
if {$r ne "bugtest"} {
set bug 1
@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat {
#*** !doctools
#[call [fun lpop] [arg listvar] [opt {index}]]
#[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop
upvar $lvar l
#upvar $lvar l
upvar 1 $lvar l
if {![llength $args]} {
set args [list end]
}
@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat {
#set newlist [lremove $newlist $tailidx]
#set newlist [lreplace $newlist $tailidx $tailidx]
set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx]
#don't use ledit here!
#we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin.
} else {
set sublist [lindex $newlist {*}$sublist_path]
#set sublist [lremove $sublist $tailidx]
@ -3467,32 +3488,53 @@ namespace eval punk::lib {
showdict {*}$opts $dvalue {*}$patterns
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
#set sep " [a+ Web-seagreen]=[a] "
variable has_punk_ansi
if {!$has_punk_ansi} {
set RST ""
set sep " = "
#set sep_mismatch " mismatch "
set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
set RST [punk::ansi::a]
set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
# set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
#} else {
# set RST [punk::ansi::a]
# #set sep " [a+ Web-seagreen]=[a] "
# set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
# #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
# #NOTE that \u2260 not suitable for non utf-8 terminals.
# set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
#}
#todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8.
# (safe detection methods for utf-8 support?)
#if colour is disabled we want to refresh this.
#therefore we use @dynamic
proc get_sep {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support
}
return $sep
}
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
proc get_sep_mismatch {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
#NOTE that \u2260 not suitable for non utf-8 terminals.
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] "
}
return $sep_mismatch
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::showdict
@cmd -name punk::lib::showdict -help "display dictionary keys and values"
#todo - table tableobject
@ -3502,10 +3544,8 @@ namespace eval punk::lib {
"Trim whitespace off rhs of each line.
This can help prevent a single long line that wraps in terminal from making
every line wrap due to long rhs padding."
-separator -default {%sep%} -help\
"Separator column between keys and values"
-separator_mismatch -default {%sep_mismatch%} -help\
"Separator to use when patterns mismatch"
-separator -default "${$DYN_SEP}" -help "Separator column between keys and values"
-separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch"
-roottype -default "dict" -help\
"list,dict,string"
-ansibase_keys -default "" -help\
@ -3524,7 +3564,23 @@ namespace eval punk::lib {
"dict or list value"
patterns -default "*" -type string -multiple 1 -help\
"key or key glob pattern"
}]]
}]
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
set RST [punk::ansi::a]
set argd [punk::args::parse $args withid ::punk::lib::showdict]
#for punk::lib - we want to reduce pkg dependencies.
# - so we won't even use the tcllib debug pkg here

37
src/bootsupport/modules/punk/mix-0.2.tm

@ -1,37 +0,0 @@
package require punk::cap
tcl::namespace::eval punk::mix {
proc init {} {
package require punk::cap::handlers::templates ;#handler for templates cap
punk::cap::register_capabilityname punk.templates ::punk::cap::handlers::templates ;#time taken should generally be sub 200us
#todo: use tcllib pluginmgr to load all modules that provide 'punk.templates'
#review - tcllib pluginmgr 0.5 @2025 has some bugs - esp regarding .tm modules vs packages
#We may also need to better control the order of module and library paths in the safe interps pluginmgr uses.
#todo - develop punk::pluginmgr to fix these issues (bug reports already submitted re tcllib, but the path issues may need customisation)
package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap
set t [time {
if {[catch {punk::mix::templates::provider register *} errM]} {
puts stderr "punk::mix failure during punk::mix::templates::provider register *"
puts stderr $errM
puts stderr "-----"
puts stderr $::errorInfo
}
}]
puts stderr "->punk::mix::templates::provider register * t=$t"
}
init
}
package require punk::mix::base
package require punk::mix::cli
package provide punk::mix [tcl::namespace::eval punk::mix {
variable version
set version 0.2
}]

38
src/bootsupport/modules/punk/nav/fs-0.1.0.tm

@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
if {$linktarget_file_type eq "directory"} {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {
@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs {
link {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
set linktarget [file readlink $cdtarget]
if {$linktarget_file_type eq "directory"} {
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {

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

@ -2167,8 +2167,13 @@ y" {return quirkykeyscript}
puts stdout "leaving $target"
puts stdout "call $commandstring\x1b\[m"
puts stdout "result:"
puts stdout $result
if {$code == 0} {
puts stdout "result:"
puts stdout $result
} else {
puts stdout "error message:"
puts stdout $::errorInfo
}
puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset
set cmdtype [dict get $linedict $target cmdtype]
@ -2647,6 +2652,8 @@ y" {return quirkykeyscript}
upvar ::punk::ns::linedict linedict
set ::punk::ns::linedict [::tcl::dict::create]
set body_cache [tcl::dict::create]
set resolved_targets [list]
foreach tgt $targets {
set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]]
@ -5018,6 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -6618,18 +6626,19 @@ y" {return quirkykeyscript}
separately calling 'info args <proc>' 'info body <proc>'
etc.
The body may display with an additional
comment inserted to display information such as the
comment inserted above the proc line to display information such as the
namespace origin. Such a comment begins with #corp#.
Returns a list: proc <procname> <arglist> <body>
(as long as any syntax highlighter is written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
Returns a string: proc <procname> <arglist> <body>
If the output is to be used as a script to regenerate a
procedure, '-syntax none' should be used to avoid ANSI
colours, or the resulting arglist and body should be
run through 'ansistrip'.
(any syntax highlighter should be written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
"
@opts
#todo - make definition @dynamic - load highlighters as functions?
@ -6647,7 +6656,13 @@ y" {return quirkykeyscript}
"Whether to replace tabs in the body with spaces or a visible Unicode symbol."
-ranges -type indexset -default "0..end" -help\
"comma delimited set of line ranges.
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1."
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1.
For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body.
The special index 0 is used to specify the line before the first line of the body,
which is where the #corp# info comment is placed if it exists.
So the default range 0..end includes the info comment and all lines of the body.
Specifying -ranges 1..5,0 would include the info comment at the end of the output.
"
-syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\
-choicelabels {
none
@ -6690,11 +6705,6 @@ y" {return quirkykeyscript}
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 infoheader "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#we want to handle edge cases of commands such as "" or :x
#various builtins such as 'namespace which' won't work
@ -6753,13 +6763,29 @@ y" {return quirkykeyscript}
return [list alias {*}$alias]
}
}
if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {
append infoheader \n "${indent}#corp# namespace origin $origin"
}
if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
append infoheader \n
#--------------------------------------------------------------------------
if {[info exists ::auto_index($path)]} {
#set infoheader "${indent}#corp# auto_index $::auto_index($path)"
set infoheader "#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'"
#if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {}
if {$origin ne $targetcmd} {
#append infoheader "${indent}#corp# namespace origin $origin"
append infoheader "#corp# namespace origin $origin"
}
if {$infoheader ne "" && $syntax eq "basic"} {
set infoheader [ansiwrap green $infoheader]
}
#if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
# append infoheader \n
#}
#--------------------------------------------------------------------------
set body ""
#set bodytext [info body $origin]
#relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname
@ -6851,47 +6877,126 @@ y" {return quirkykeyscript}
}
}
if {$ranges ni {"0..end" ".." "0.."}} {
set lines [split $body \n]
set linecount [llength $lines]
set lines [split $body \n]
set linecount [llength $lines]
#------------------------------------------------------------------------------------------------
#When we resolve our 1-based indexset - the zero index used to specify the info comment is lost.
#we need to search for it manually and add it back in if it's in the specified ranges.
set rangelist [split $ranges ,]
set info_positions [list]
set lnum 0
foreach range $rangelist {
lassign [split $range ..] start _ end
set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range]
if {$start eq "0"} {
lappend info_positions $lnum
}
incr lnum [llength $r_indices]
#if {$end eq "0"} {
# #ignore
#}
}
#puts "info_positions: $info_positions"
#------------------------------------------------------------------------------------------------
set body ""
if {[lindex $info_positions 0] == 0 && $infoheader ne ""} {
append body "$infoheader" \n
}
if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} {
set w [string length $linecount]
set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges]
set body ""
set outputlines [llength $indices]
if {$do_ln} {
set n 0
foreach idx $indices {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
if {$idx == 1} {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}"
} else {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
} else {
foreach idx $indices {
append body [lindex $lines $idx-1] \n
if {$idx == 1} {
set ln1 "proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body [lindex $lines end] "\}"
} else {
append body [lindex $lines $idx-1] \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
}
#no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match
if {[string index $body end] eq "\n"} {
set body [string range $body 0 end-1]
}
} else {
#range was specified in a standard way to mean 'all lines'
set outputlines $linecount
if {$do_ln} {
set linebody ""
set n 0
set lines [split $body \n]
set linecount [llength $lines]
set w [string length $linecount]
foreach ln $lines {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
set n 2
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$lnc[format %${w}s $n]$lnr $ln"
incr n
append linebody "$lnc[format %${w}s $n]$lnr $ln" \n
}
set body [string range $linebody 0 end-1]
#set body $linebody
if {$linecount > 1} {
append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
} else {
set ln1 "proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$ln"
}
if {$linecount > 1} {
append linebody \n "[lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
}
}
if {$is_highlighted} {
#ansi colourised items in list format may not always have desired string representation (list escaping can occur)
#return as a string - which may not be a proper Tcl list!
return "proc $resolved {$argl} {\n$infoheader$body\n}"
} else {
list proc $resolved $argl $infoheader$body
#ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end?
if {[lindex $info_positions end] > 0} {
if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} {
append body "\n$infoheader"
}
}
return $body
}

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

@ -165,11 +165,11 @@ namespace eval punk::repl {
variable frametype
set frametype ascii; #conservative default
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$testcharwidth == 1} {
set frametype light
}
}
#if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
# if {$testcharwidth == 1} {
# set frametype light
# }
#}
variable debug_repl 0
variable signal_control_c 0
@ -1961,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
set is_vt52 0
}
variable codethread
variable loopinstance
incr loopinstance
@ -2091,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
set chunk "\b\x7f\b\x7f"
} elseif {$chunk eq "\x1c"} {
#ctrl-bslash
#This is commonly used in terminals as a 'harder' ctrl-c.
#try to brutally terminate process
#attempt to leave terminal in a reasonable state
mode line ;#may be aliased to ::repl::interphelpers::mode
after 250 {exit 42}
punk::console::mode line
#for now - exit with small delay for tidyup
after 1000 {exit 43}
return
} elseif {$chunk eq "\x1a"} {
#for now - exit with small delay for tidyup
#ctrl-z
#::punk::repl::handler_console_control "ctrl-z_via_rawloop"
if {[catch {punk::console::mode line}]} {
#REVIEW
interp eval code {punk::console::mode line}
#JMN
#set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}]
set iname $::punk::repl::codethread::replthread_interp
#only the highest level subshell has an interp name of empty string (lower levels are named 'code')
if {$iname eq ""} {
punk::console::mode line
}
after 1000 {exit 43}
after 250 [list thread::send $codethread [list interp eval code {quit 42}]]
return
}
@ -2397,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#set commandstr "set ::punk::repl::debug_repl"
set commandstr ""
}
if {$::punk::repl::debug_repl > 100} {
proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] {
set p %p%
@ -2416,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs debugreport $clearance$p[string map [list \n \n$p] $msg]
}]
set info ""
append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n"
append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n"
set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]]
append info "lastrunchunks\n"
append info "chunks: [llength $lastrunchunks]\n"
append info "namespace: $::punk::nav::ns::ns_current"
append info "chunks : [llength $lastrunchunks]\n"
#JMN
set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]]
append info "codethread namespace: $codethread_ns\n"
append info "stdinlines : [llength $stdinlines] lines\n"
foreach ln $stdinlines {
append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n"
}
append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n"
#append info "namespace: $::punk::nav::ns::ns_current"
debug_repl_emit $info
} else {
proc debug_repl_emit {msg} {return}
@ -2466,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
# lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]]
#}
variable codethread
variable codethread_cond
variable codethread_mutex
@ -2895,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
if {[llength $waiting]} {
set c [lindex $waiting end]
} else {
#set c " "
set c \u240a
set c \u240a ;#unicode linefeed symbol.
}
doprompt ">$c "
}
@ -3008,16 +3022,17 @@ namespace eval repl {
set codethread_mutex [thread::mutex create]
set scriptmap [list %args% [list $opts] \
%argv0% [list $::argv0] \
%argv% [list $::argv] \
%argc% [list $::argc] \
%replthread% [thread::id] \
%replthread_cond% $codethread_cond \
%replthread_interp% [list $opt_callback_interp] \
%tmlist% [list [tcl::tm::list]] \
%autopath% [list $::auto_path] \
%lib_epoch% [list $::punk::libunknown::epoch]\
set scriptmap [list %args% [list $opts] {*}{
} %argv0% [list $::argv0] {*}{
} %argv% [list $::argv] {*}{
} %argc% [list $::argc] {*}{
} %replthread% [thread::id] {*}{
} %replthread_cond% $codethread_cond {*}{
} %replthread_interp% [list $opt_callback_interp] {*}{
} %tmlist% [list [tcl::tm::list]] {*}{
} %autopath% [list $::auto_path] {*}{
} %lib_epoch% [list $::punk::libunknown::epoch] {*}{
}
]
#scriptmap applied at end to satisfy silly editor highlighting.
set init_script {

276
src/bootsupport/modules/punk/repl/codethread-0.1.0.tm

@ -1,276 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024
#
# @@ Meta Begin
# Application punk::repl::codethread 0.1.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin shellspy_module_punk::repl::codethread 0 0.1.0]
#[copyright "2024"]
#[titledesc {Module repl codethread}] [comment {-- Name section and table of contents description --}]
#[moddesc {codethread for repl - root interpreter}] [comment {-- Description at end of page heading --}]
#[require punk::repl::codethread]
#[keywords module repl]
#[description]
#[para] This is part of the infrastructure required for the punk::repl to operate
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::repl::codethread
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::repl::codethread
#[list_begin itemized]
package require Tcl 8.6-
package require punk::config
#*** !doctools
#[item] [package {Tcl 8.6}]
# #package require frobz
# #*** !doctools
# #[item] [package {frobz}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# oo::class namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#tcl::namespace::eval punk::repl::codethread::class {
#*** !doctools
#[subsection {Namespace punk::repl::codethread::class}]
#[para] class definitions
#if {[info commands [tcl::namespace::current]::interface_sample1] eq ""} {
#*** !doctools
#[list_begin enumerated]
# oo::class create interface_sample1 {
# #*** !doctools
# #[enum] CLASS [class interface_sample1]
# #[list_begin definitions]
# method test {arg1} {
# #*** !doctools
# #[call class::interface_sample1 [method test] [arg arg1]]
# #[para] test method
# puts "test: $arg1"
# }
# #*** !doctools
# #[list_end] [comment {-- end definitions interface_sample1}]
# }
#*** !doctools
#[list_end] [comment {--- end class enumeration ---}]
#}
#}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::repl::codethread {
tcl::namespace::export *
variable replthread
variable replthread_cond
variable running 0
variable output_stdout ""
variable output_stderr ""
#variable xyz
#*** !doctools
#[subsection {Namespace punk::repl::codethread}]
#[para] Core API functions for punk::repl::codethread
#[list_begin definitions]
#proc sample1 {p1 n args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]]
# #[para]Description of sample1
# #[para] Arguments:
# # [list_begin arguments]
# # [arg_def tring p1] A description of string argument p1.
# # [arg_def integer n] A description of integer argument n.
# # [list_end]
# return "ok"
#}
variable run_command_cache
proc is_running {} {
variable running
return $running
}
proc runscript {script} {
#puts stderr "->runscript"
variable replthread_cond
#variable output_stdout
#set output_stdout ""
#variable output_stderr
#set output_stderr ""
#expecting to be called from a thread::send in parent repl - ie in the toplevel interp so that the sub-interp "code" is available
#if a thread::send is done from the commandline in a codethread - Tcl will
if {"code" ni [interp children] || ![info exists replthread_cond]} {
#in case someone tries calling from codethread directly - don't do anything or change any state
#(direct caller could create an interp named code at the level "" -> "code" -"code" and add a replthread_cond value to avoid this check - but it probably won't do anything useful)
#if called directly - the context will be within the first 'code' interp.
#inappropriate caller could add superfluous entries to shellfilter stack if function errors out
#inappropriate caller could affect tsv vars (if their interp allows that anyway)
puts stderr "runscript is meant to be called from the parent repl thread via a thread::send to the codethread"
return
}
interp eval code [list set ::punk::repl::codethread::output_stdout ""]
interp eval code [list set ::punk::repl::codethread::output_stderr ""]
set outstack [list]
set errstack [list]
upvar ::punk::config::running running_config
if {[string length [dict get $running_config color_stdout_repl]] && [interp eval code punk::console::colour]} {
lappend outstack [interp eval code [list shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout_repl]]]]
}
lappend outstack [interp eval code [list shellfilter::stack::add stdout tee_to_var -settings {-varname ::punk::repl::codethread::output_stdout}]]
if {[string length [dict get $running_config color_stderr_repl]] && [interp eval code punk::console::colour]} {
lappend errstack [interp eval code [list shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr_repl]]]]
# #lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour cyan]]
}
lappend errstack [interp eval code [list shellfilter::stack::add stderr tee_to_var -settings {-varname ::punk::repl::codethread::output_stderr}]]
#an experiment
#set errhandle [shellfilter::stack::item_tophandle stderr]
#interp transfer "" $errhandle code
set status [catch {
#shennanigans to keep compiled script around after call.
#otherwise when $script goes out of scope - internal rep of vars set in script changes.
#The shimmering may be no big deal(?) - but debug/analysis using tcl::unsupported::representation becomes impossible.
interp eval code [list ::punk::lib::set_clone ::codeinterp::clonescript $script] ;#like objclone
interp eval code {
lappend ::codeinterp::run_command_cache $::codeinterp::clonescript
if {[llength $::codeinterp::run_command_cache] > 2000} {
set ::codeinterp::run_command_cache [lrange $::codeinterp::run_command_cache 1750 end][unset ::codeinterp::run_command_cache]
}
tcl::namespace::inscope $::punk::ns::ns_current $::codeinterp::clonescript
}
} result]
flush stdout
flush stderr
#interp transfer code $errhandle ""
#flush $errhandle
set lastoutchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stdout]] end]
set lasterrchar [string index [punk::ansi::ansistrip [interp eval code set ::punk::repl::codethread::output_stderr]] end]
#puts stderr "-->[ansistring VIEW -lf 1 $lastoutchar$lasterrchar]"
set tid [thread::id]
tsv::set codethread_$tid info [list lastoutchar $lastoutchar lasterrchar $lasterrchar]
tsv::set codethread_$tid status $status
tsv::set codethread_$tid result $result
tsv::set codethread_$tid errorcode $::errorCode
#only remove from shellfilter::stack the items we added to stack in this function
foreach s [lreverse $outstack] {
interp eval code [list shellfilter::stack::remove stdout $s]
}
foreach s [lreverse $errstack] {
interp eval code [list shellfilter::stack::remove stderr $s]
}
thread::cond notify $replthread_cond
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::repl::codethread ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::repl::codethread::lib {
tcl::namespace::export *
tcl::namespace::path [tcl::namespace::parent]
#*** !doctools
#[subsection {Namespace punk::repl::codethread::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::repl::codethread::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
tcl::namespace::eval punk::repl::codethread::system {
#*** !doctools
#[subsection {Namespace punk::repl::codethread::system}]
#[para] Internal functions that are not part of the API
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::repl::codethread [tcl::namespace::eval punk::repl::codethread {
variable pkg punk::repl::codethread
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

836
src/bootsupport/modules/punk/winlnk-0.1.0.tm

@ -1,836 +0,0 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2024
#
# @@ Meta Begin
# Application punk::winlnk 0.1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::winlnk 0 0.1.0]
#[copyright "2024"]
#[titledesc {windows shortcut .lnk library}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk::winlnk}] [comment {-- Description at end of page heading --}]
#[require punk::winlnk]
#[keywords module shortcut lnk parse windows crossplatform]
#[description]
#[para] Tools for reading windows shortcuts (.lnk files) on any platform
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::winlnk
#[subsection Concepts]
#[para] Windows shortcuts are a binary format file with a .lnk extension
#[para] Shell Link (.LNK) Binary File Format is documented in [lb]MS_SHLLINK[rb].pdf published by Microsoft.
#[para] Revision 8.0 published 2024-04-23
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::winlnk
#[list_begin itemized]
package require Tcl 8.6-
#*** !doctools
#[item] [package {Tcl 8.6}]
#TODO - logger
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::winlnk {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
#variable xyz
#*** !doctools
#[subsection {Namespace punk::winlnk}]
#[para] Core API functions for punk::winlnk
#[list_begin definitions]
variable magic_HeaderSize "0000004C" ;#HeaderSize MUST equal this
variable magic_LinkCLSID "00021401-0000-0000-C000-000000000046" ;#LinkCLSID MUST equal this
proc Get_contents {path {bytes all}} {
if {![file exists $path] || [file type $path] ne "file"} {
error "punk::winlnk::get_contents cannot find a filesystem object of type 'file' at location: $path"
}
set fd [open $path r]
chan configure $fd -translation binary -encoding iso8859-1
if {$bytes eq "all"} {
set data [read $fd]
} else {
set data [read $fd $bytes]
}
close $fd
return $data
}
proc Contents_check_header {contents} {
variable magic_HeaderSize
variable magic_LinkCLSID
expr {[Header_Get_HeaderSize $contents] eq $magic_HeaderSize && [Header_Get_LinkCLSID $contents] eq $magic_LinkCLSID}
}
#LinkFlags - 4 bytes - specifies information about the shell link and the presence of optional portions of the structure.
proc Show_LinkFlags {contents} {
set 4bytes [string range $contents 20 23]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
puts "val: $val"
set declist [scan [string reverse $4bytes] %c%c%c%c]
set fmt [string repeat %08b 4]
puts "LinkFlags:[format $fmt {*}$declist]"
set r [binary scan $4bytes b32 val]
puts "bscan-le: $val"
set r [binary scan [string reverse $4bytes] b32 val]
puts "bscan-2 : $val"
}
variable LinkFlags
set LinkFlags [dict create\
hasLinkTargetIDList 1\
HasLinkInfo 2\
HasName 4\
HasRelativePath 8\
HasWorkingDir 16\
HasArguments 32\
HasIconLocation 64\
IsUnicode 128\
ForceNoLinkInfo 256\
HasExpString 512\
RunInSeparateProcess 1024\
Unused1 2048\
HasDarwinID 4096\
RunAsUser 8192\
HasExpIcon 16394\
NoPidlAlias 32768\
Unused2 65536\
RunWithShimLayer 131072\
ForceNoLinkTrack 262144\
EnableTargetMetadata 524288\
DisableLinkPathTracking 1048576\
DisableKnownFolderTracking 2097152\
DisableKnownFolderAlias 4194304\
AllowLinkToLink 8388608\
UnaliasOnSave 16777216\
PreferEnvironmentPath 33554432\
KeepLocalIDListForUNCTarget 67108864\
]
variable LinkFlagLetters [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 AA]
proc Header_Has_LinkFlag {contents flagname} {
variable LinkFlags
variable LinkFlagLetters
if {[string length $flagname] <= 2} {
set idx [lsearch $LinkFlagLetters $flagname]
if {$idx < 0} {
error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known"
}
set binflag [expr {2**$idx}]
set allflags [Header_Get_LinkFlags $contents]
return [expr {$allflags & $binflag}]
}
if {[dict exists $LinkFlags $flagname]} {
set binflag [dict get $LinkFlags $flagname]
set allflags [Header_Get_LinkFlags $contents]
return [expr {$allflags & $binflag}]
} else {
error "punk::winlnk::Header_Has_LinkFlag error - flagname $flagname not known"
}
}
#MS-SHLLINK.pdf documents the .lnk file format in detail, but here is a brief overview of the structure of a .lnk file:
#protocol revision 10.0 (November 2025) https://winprotocoldocs-bhdugrdyduf5h2e4.b02.azurefd.net/MS-SHLLINK/%5bMS-SHLLINK%5d.pdf
#SHELL_LINK_HEADER structure is 76 bytes long and starts at the beginning of the file
#offset hex:0x00 dec:0 4 bytes
#Header size (HeaderSize) (must be 0x0000004C for .lnk files)
proc Header_Get_HeaderSize {contents} {
set 4bytes [split [string range $contents 0 3] ""]
set hex4 ""
foreach b [lreverse $4bytes] {
set dec [scan $b %c] ;# 0-255 decimal
set HH [format %2.2llX $dec]
append hex4 $HH
}
return $hex4
}
#offset hex:0x04 dec:4 16 bytes
#LinkCLSID (must be 00021401-0000-0000-C000-000000000046 for .lnk files)
proc Header_Get_LinkCLSID {contents} {
set 16bytes [string range $contents 4 19]
#CLSID hex textual representation is split as 4-2-2-2-6 bytes(hex pairs)
#e.g We expect 00021401-0000-0000-C000-000000000046 for .lnk files
#for endianness - it is little endian all the way but the split is 4-2-2-1-1-1-1-1-1-1-1 REVIEW
#(so it can appear as mixed endianness if you don't know the splits)
#https://devblogs.microsoft.com/oldnewthing/20220928-00/?p=107221
#This is based on COM textual representation of GUIDS
#Apparently a CLSID is a GUID that identifies a COM object
set clsid ""
set s1 [tcl::string::range $16bytes 0 3]
set declist [scan [string reverse $s1] %c%c%c%c]
set fmt "%02X%02X%02X%02X"
append clsid [format $fmt {*}$declist]
append clsid -
set s2 [tcl::string::range $16bytes 4 5]
set declist [scan [string reverse $s2] %c%c]
set fmt "%02X%02X"
append clsid [format $fmt {*}$declist]
append clsid -
set s3 [tcl::string::range $16bytes 6 7]
set declist [scan [string reverse $s3] %c%c]
append clsid [format $fmt {*}$declist]
append clsid -
#now treat bytes individually - so no endianness conversion
set declist [scan [tcl::string::range $16bytes 8 9] %c%c]
append clsid [format $fmt {*}$declist]
append clsid -
set scan [string repeat %c 6]
set fmt [string repeat %02X 6]
set declist [scan [tcl::string::range $16bytes 10 15] $scan]
append clsid [format $fmt {*}$declist]
return $clsid
}
#offset hex:0x14 dec:20 4 bytes
#Link flags (LinkFlags) - bit field specifying information about the shell link and the presence of optional portions of the structure.
#HasLinkTargetIDList bit 0 (0x00000001) - if set, a LinkTargetIDList structure is present immediately following the header
#HasLinkInfo bit 1 (0x00000002) - if set, a LinkInfo structure is present immediately following the header (or the LinkTargetIDList if that is present)
#HasName bit 2 (0x00000004) - if set, a null-terminated string containing the name of the link is present immediately following the header (or the LinkTargetIDList and LinkInfo if they are present)
#HasRelativePath bit 3 (0x00000008) - if set, a null-terminated string containing the relative path of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo and Name if they are present)
#HasWorkingDir bit 4 (0x00000010) - if set, a null-terminated string containing the working directory of the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name and Relative Path if they are present)
#HasArguments bit 5 (0x00000020) - if set, a null-terminated string containing the command line arguments for the link target is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path and Working Dir if they are present)
#HasIconLocation bit 6 (0x00000040) - if set, a null-terminated string containing the location of the icon for the link is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir and Arguments if they are present)
#IsUnicode bit 7 (0x00000080) - if set, the strings in the link are stored in Unicode (UTF-16LE) format; if not set, the strings are stored in ANSI format (usually the system's default code page)
#ForceNoLinkInfo bit 8 (0x00000100) - if set, the LinkInfo structure is not stored in the file even if the HasLinkInfo bit is set; this can be used to force the link to be resolved using only the information in the header and the optional strings, without using the LinkInfo structure
#HasExpString bit 9 (0x00000200) - if set, a null-terminated string containing an "environment variable" style string is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments and Icon Location if they are present); this string can contain environment variable references (e.g. %USERPROFILE%) that can be expanded to obtain the actual path of the link target
#RunInSeparateProcess bit 10 (0x00000400) - if set, the link target should be run in a separate process; if not set, the link target may be run in the same process as the caller
#Unused1 bit 11 (0x00000800) - reserved for future use; should be set to 0
#HasDarwinID bit 12 (0x00001000) - if set, a null-terminated string containing a "Darwin ID" is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location and ExpString if they are present); this string can be used to identify the link target in a way that is independent of the file system (e.g. for links to Control Panel items or special folders)
#RunAsUser bit 13 (0x00002000) - if set, the link target should be run with the permissions of the user specified in the HasDarwinID string; if not set, the link target should be run with the permissions of the caller
#HasExpIcon bit 14 (0x00004000) - if set, a null-terminated string containing an "environment variable" style string for the icon location is present immediately following the header (or the LinkTargetIDList, LinkInfo, Name, Relative Path, Working Dir, Arguments, Icon Location, ExpString and DarwinID if they are present); this string can contain environment variable references that can be expanded to obtain the actual path of the icon for the link
#NoPidlAlias bit 15 (0x00008000) - if set, the link target should not be resolved using the PIDL alias mechanism; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed
#Unused2 bit 16 (0x00010000) - reserved for future use; should be set to 0
#RunWithShimLayer bit 17 (0x00020000) - if set, the link target should be run with the application compatibility shim layer; if not set, the link target should be run without the shim layer
#ForceNoLinkTrack bit 18 (0x00040000) - if set, the link target should not be tracked by the shell's link tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed
#EnableTargetMetadata bit 19 (0x00080000) - if set, the link target should have metadata enabled; this can be used to allow the link to store additional information about the target (e.g. for links to files, the link can store the file's attributes, creation time, access time and modification time)
#DisableLinkPathTracking bit 20 (0x00100000) - if set, the link target should not be tracked by the shell's link path tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its path
#DisableKnownFolderTracking bit 21 (0x00200000) - if set, the link target should not be tracked by the shell's known folder tracking mechanism; this can be used to prevent the link from being automatically updated if the target is moved or renamed based on its known folder ID
#DisableKnownFolderAlias bit 22 (0x00400000) - if set, the link target should not be aliased to a known folder; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on its known folder ID
#AllowLinkToLink bit 23 (0x00800000) - if set, the link target can be another link; if not set, the link target should not be another link (i.e. it should be a file or directory); this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link
#UnaliasOnSave bit 24 (0x01000000) - if set, the link should be unaliased when it is saved; this can be used to prevent the link from being resolved to a different target if the original target is moved or renamed based on the fact that it is a link
#PreferEnvironmentPath bit 25 (0x02000000) - if set, the link should prefer to resolve the target using environment variable references; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the environment variable references still point to the correct location
#KeepLocalIDListForUNCTarget bit 26 (0x04000000) - if set, the link should keep the local ID list for UNC targets; this can be used to allow the link to be resolved correctly even if the target is moved or renamed, as long as the local ID list still points to the correct location
# - the presence of these flags indicates the presence of optional structures in the .lnk file and also provides information about how to interpret the data in the file
proc Header_Get_LinkFlags {contents} {
set 4bytes [string range $contents 20 23]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
return $val
}
#offset hex:0x18 dec:24 4 bytes
#File attributes (FileAttributes) - bit field specifying the file attributes of the link target (if the EnableTargetMetadata flag is set in the LinkFlags field); this field is a bitwise combination of the following values:
proc Header_Get_FileAttributes {contents} {
if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} {
return {}
}
set 4bytes [string range $contents 24 27]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
set attrlist {}
if {$val & 0x00000001} {lappend attrlist "READONLY"}
if {$val & 0x00000002} {lappend attrlist "HIDDEN"}
if {$val & 0x00000004} {lappend attrlist "SYSTEM"}
if {$val & 0x00000010} {lappend attrlist "DIRECTORY"}
if {$val & 0x00000020} {lappend attrlist "ARCHIVE"}
if {$val & 0x00000040} {lappend attrlist "DEVICE"}
if {$val & 0x00000080} {lappend attrlist "NORMAL"}
if {$val & 0x00000100} {lappend attrlist "TEMPORARY"}
if {$val & 0x00000200} {lappend attrlist "SPARSE_FILE"}
if {$val & 0x00000400} {lappend attrlist "REPARSE_POINT"}
if {$val & 0x00000800} {lappend attrlist "COMPRESSED"}
if {$val & 0x00001000} {lappend attrlist "OFFLINE"}
if {$val & 0x00002000} {lappend attrlist "NOT_CONTENT_INDEXED"}
if {$val & 0x00004000} {lappend attrlist "ENCRYPTED"}
return $attrlist
}
proc Header_Get_FileAttributes_Raw {contents} {
if {![Header_Has_LinkFlag $contents "EnableTargetMetadata"]} {
return 0
}
set 4bytes [string range $contents 24 27]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
return $val
}
#offset hex:0x1C dec:28 8 bytes
#creation date and time (CreationTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC))
proc Header_Get_CreationTime {contents} {
set 8bytes [string range $contents 28 35]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
#convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC)
#we can convert it to seconds and then to a human readable format
set seconds [expr {$val / 10000000.0}]
set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970
set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true]
return $human_time
}
proc Header_Get_CreationTime_Raw {contents} {
set 8bytes [string range $contents 28 35]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
return $val
}
#offset 36 8 bytes
#last access date and time (AccessTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC))
proc Header_Get_AccessTime {contents} {
set 8bytes [string range $contents 36 43]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
#convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC)
#we can convert it to seconds and then to a human readable format
set seconds [expr {$val / 10000000.0}]
set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970
set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true]
return $human_time
}
proc Header_Get_AccessTime_Raw {contents} {
set 8bytes [string range $contents 36 43]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
return $val
}
#offset hex:0x2C dec:44 8 bytes
#last modification date and time (WriteTime) (FILETIME structure - 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC))
proc Header_Get_WriteTime {contents} {
set 8bytes [string range $contents 44 51]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
#convert FILETIME to human readable format - this is a bit complex because FILETIME is in 100-nanosecond intervals since January 1, 1601 (UTC)
#we can convert it to seconds and then to a human readable format
set seconds [expr {$val / 10000000.0}]
set epoch_seconds [expr {round($seconds) - 11644473600}] ;# number of seconds between January 1, 1601 and January 1, 1970
set human_time [clock format $epoch_seconds -format "%Y-%m-%d %H:%M:%S" -gmt true]
return $human_time
}
proc Header_Get_WriteTime_Raw {contents} {
set 8bytes [string range $contents 44 51]
set r [binary scan $8bytes w val] ;# w for little endian 64-bit signed int
return $val
}
#offset hex:0x34 dec:52 Bytes:4 - unsigned int
#file size in bytes (of target - low 32 bits if >4GB)
proc Header_Get_FileSize {contents} {
set 4bytes [string range $contents 52 55]
set r [binary scan $4bytes i val]
return $val
}
#offset hex:0x38 dec:56 Bytes:4 - signed integer
#icon index value
proc Header_Get_IconIndex {contents} {
set 4bytes [string range $contents 56 59]
set r [binary scan $4bytes i val]
return $val
}
#offset hex:0x3C dec:60 Bytes:4 - unsigned integer
#SW_SHOWNORMAL 0x00000001
#SW_SHOWMAXIMIZED 0x00000001
#SW_SHOWMINNOACTIVE 0x00000007
# - all other values MUST be treated as SW_SHOWNORMAL
proc Header_Get_ShowCommand {contents} {
set 4bytes [string range $contents 60 63]
set r [binary scan $4bytes i val]
return $val
}
#offset hex:0x40 dec:64 Bytes:2
#Hot key
proc Header_Get_HotKey {contents} {
# Existing code that extracts the raw 16‑bit hotkey value:
set raw [Header_Get_HotKey_Raw $contents]
# The low byte holds the virtual‑key, high byte holds modifier flags
set vk [expr {$raw & 0xFF}]
set mods [expr {($raw >> 8) & 0xFF}]
set name [_vk_to_name $vk]
set modStr [_modifiers_to_string $mods]
if {$modStr eq ""} {
return $name
} else {
return "${modStr}+${name}"
}
}
proc Header_Get_HotKey_Raw {contents} {
set 2bytes [string range $contents 64 65]
set r [binary scan $2bytes s val] ;#short
return $val
}
proc _modifiers_to_string {mods} {
set parts {}
if {$mods & 0x01} {lappend parts "Shift"}
if {$mods & 0x02} {lappend parts "Ctrl"}
if {$mods & 0x04} {lappend parts "Alt"}
if {$mods & 0x08} {lappend parts "Win"} ;# optional
return [join $parts "+"]
}
proc _vk_to_name {vk} {
# Minimal map – extend as needed
array set vkMap {
0x00 "No key assigned"
0x08 Backspace 0x09 Tab 0x0D Return
0x10 Shift 0x11 Control 0x12 Alt
0x20 Space 0x21 PageUp 0x22 PageDown
0x23 End 0x24 Home 0x25 Left
0x26 Up 0x27 Right 0x28 Down
0x2D Insert 0x2E Delete
0x70 F1 0x71 F2 0x72 F3
0x73 F4 0x74 F5 0x75 F6
0x76 F7 0x77 F8 0x78 F9
0x79 F10 0x7A F11 0x7B F12
0x7c F13 0x7d F14 0x7e F15
0x7f F16 0x80 F17 0x81 F18
0x82 F19 0x83 F20 0x84 F21
0x85 F22 0x86 F23 0x87 F24
0x90 "NUM LOCK" 0x91 "SCROLL LOCK"
}
if {[info exists vkMap($vk)]} {
return $vkMap($vk)
} else {
if {$vk >= 0x30 && $vk <= 0x39} {
return [format "%c" $vk] ;# 0-9
} elseif {$vk >= 0x41 && $vk <= 0x5A} {
return [format "%c" $vk] ;# A-Z
}
# fallback: hex representation
return [format "0x%02X" $vk]
}
}
#offset hex:0x42 dec:66 Bytes:2 - reserved1
proc Header_Get_Reserved1 {contents} {
set 2bytes [string range $contents 66 67]
set r [binary scan $2bytes s val] ;#short
return $val
}
#offset hex:0x44 dec:68 Bytes:4 - reserved2
proc Header_Get_Reserved2 {contents} {
set 4bytes [string range $contents 68 71]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
return $val
}
#offset hex:0x48 dec:72 Bytes:4 - reserved3
proc Header_Get_Reserved3 {contents} {
set 4bytes [string range $contents 72 75]
set r [binary scan $4bytes i val] ;# i for little endian 32-bit signed int
return $val
}
#end of 76 byte header
proc Get_LinkTargetIDList_size {contents} {
if {[Header_Has_LinkFlag $contents "A"]} {
set 2bytes [string range $contents 76 77]
set r [binary scan $2bytes s val] ;#short
#logger
#puts stderr "LinkTargetIDList_size: $val"
return $val
} else {
return 0
}
}
proc Get_LinkInfo_content {contents} {
set idlist_size [Get_LinkTargetIDList_size $contents]
if {$idlist_size == 0} {
set offset 0
} else {
set offset [expr {2 + $idlist_size}] ;#LinkTargetIdList IDListSize field + value
}
set linkinfo_start [expr {76 + $offset}]
if {[Header_Has_LinkFlag $contents "B"]} {
#puts stderr "linkinfo_start: $linkinfo_start"
set 4bytes [string range $contents $linkinfo_start $linkinfo_start+3]
binary scan $4bytes i val ;#size *including* these 4 bytes
set linkinfo_content [string range $contents $linkinfo_start [expr {$linkinfo_start + $val -1}]]
return [dict create linkinfo_start $linkinfo_start size $val next_start [expr {$linkinfo_start + $val}] content $linkinfo_content]
} else {
return [dict create linkinfo_start $linkinfo_start size 0 next_start $linkinfo_start content ""]
}
}
proc LinkInfo_get_fields {linkinfocontent} {
set 4bytes [string range $linkinfocontent 0 3]
binary scan $4bytes i val ;#size *including* these 4 bytes
set bytes_linkinfoheadersize [string range $linkinfocontent 4 7]
set bytes_linkinfoflags [string range $linkinfocontent 8 11]
set r [binary scan $4bytes i flags] ;# i for little endian 32-bit signed int
#puts "linkinfoflags: $flags"
set localbasepath ""
set commonpathsuffix ""
#REVIEW - flags problem?
if {$flags & 1} {
#VolumeIDAndLocalBasePath
#logger
#puts stderr "VolumeIDAndLocalBasePath"
}
if {$flags & 2} {
#logger
#puts stderr "CommonNetworkRelativeLinkAndPathSuffix"
}
set bytes_volumeid_offset [string range $linkinfocontent 12 15]
set bytes_localbasepath_offset [string range $linkinfocontent 16 19] ;# a
set bytes_commonnetworkrelativelinkoffset [string range $linkinfocontent 20 23]
set bytes_commonpathsuffix_offset [string range $linkinfocontent 24 27] ;# a
binary scan $bytes_localbasepath_offset i bp_offset
if {$bp_offset > 0} {
set tail [string range $linkinfocontent $bp_offset end]
set stringterminator 0
set i 0
set localbasepath ""
#TODO
while {!$stringterminator & $i < 100} {
set c [string index $tail $i]
if {$c eq "\x00"} {
set stringterminator 1
} else {
append localbasepath $c
}
incr i
}
}
binary scan $bytes_commonpathsuffix_offset i cps_offset
if {$cps_offset > 0} {
set tail [string range $linkinfocontent $cps_offset end]
set stringterminator 0
set i 0
set commonpathsuffix ""
#TODO
while {!$stringterminator && $i < 100} {
set c [string index $tail $i]
if {$c eq "\x00"} {
set stringterminator 1
} else {
append commonpathsuffix $c
}
incr i
}
}
return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix]
}
proc contents_get_info {contents} {
#todo - return something like the perl lnk-parse-1.0.pl script?
#Link File: C:/repo/jn/tclmodules/tomlish/src/modules/test/#modpod-tomlish-0.1.0/suites/all/arrays_1.toml#roundtrip+roundtrip_files+arrays_1.toml.fauxlink.lnk
#Link Flags: HAS SHELLIDLIST | POINTS TO FILE/DIR | NO DESCRIPTION | HAS RELATIVE PATH STRING | HAS WORKING DIRECTORY | NO CMD LINE ARGS | NO CUSTOM ICON |
#File Attributes: ARCHIVE
#Create Time: Sun Jul 14 2024 10:41:34
#Last Accessed time: Sat Sept 21 2024 02:46:10
#Last Modified Time: Tue Sept 10 2024 17:16:07
#Target Length: 479
#Icon Index: 0
#ShowWnd: 1 SW_NORMAL
#HotKey: 0
#(App Path:) Remaining Path: repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files\arrays_1.toml
#Relative Path: ..\roundtrip\roundtrip_files\arrays_1.toml
#Working Dir: C:\repo\jn\tclmodules\tomlish\src\modules\test\#modpod-tomlish-0.1.0\suites\roundtrip\roundtrip_files
variable LinkFlags
set flags_enabled [list]
dict for {k v} $LinkFlags {
if {[Header_Has_LinkFlag $contents $k] > 0} {
lappend flags_enabled $k
}
}
set showcommand_val [Header_Get_ShowCommand $contents]
switch -- $showcommand_val {
1 {
set showwnd [list 1 SW_SHOWNORMAL]
}
3 {
set showwnd [list 3 SW_SHOWMAXIMIZED]
}
7 {
set showwnd [list 7 SW_SHOWMINNOACTIVE]
}
default {
set showwnd [list $showcommand_val SW_SHOWNORMAL-effective]
}
}
set linkinfo_content_dict [Get_LinkInfo_content $contents]
set localbase_path ""
set suffix_path ""
set linkinfocontent [dict get $linkinfo_content_dict content]
set link_target ""
if {$linkinfocontent ne ""} {
set linkfields [LinkInfo_get_fields $linkinfocontent]
set localbase_path [dict get $linkfields localbasepath]
set suffix_path [dict get $linkfields commonpathsuffix]
if {"windows" eq $::tcl_platform(platform)} {
set link_target [file join $localbase_path $suffix_path]
} else {
set suffix_path [string trimleft [string map {\\ /} $suffix_path] /]
if {[regexp {([a-zA-Z]):\\(.*)} $localbase_path _match drive_letter tail]} {
set localbase_path [string map {\\ /} $localbase_path]
set tail [string trimleft [string map {\\ /} $tail] /]
set link_target ""
#shortcut basepath is a windows path with drive letter - try to resolve it on unix by looking for a corresponding mount from fstab or a point under /mnt
set mountinfo [exec mount]
foreach line [split $mountinfo "\n"] {
#review - a more specific mount target might exist that includes the drive letter as part of the mount point name and is a longer prefix of the localbase_path
#- we should probably look for the longest prefix match rather than just the drive letter
if {[regexp -nocase -- [string cat ^$drive_letter {:\\\s+on\s+(\S+)}] $line _match mount_point]} {
set link_target [file join $mount_point $tail $suffix_path]
break
}
}
if {$link_target eq ""} {
#review - under what circumstances could this happen? If the drive letter doesn't match any mount points, then /mnt/drive_letter should generally already have been found above above
# - However, it may be possible for /mnt/drive_Letter to still exist even if it's not reflected in the output of mount or the output of mount is in an unexpected format.
#nothing in mount result matches the drive letter - try looking for a mount point under /mnt with the drive letter as the name
if {[file exists /mnt/$drive_letter]} {
set link_target [file join /mnt/$drive_letter $tail $suffix_path]
} else {
if {$drive_letter eq [string tolower $drive_letter]]} {
set op_drive_letter [string toupper $drive_letter]
} else {
set op_drive_letter [string tolower $drive_letter]
}
if {[file exists /mnt/$op_drive_letter]} {
set link_target [file join /mnt/$op_drive_letter $tail $suffix_path]
} else {
#leave as is except for backslashes converted to forward
#- probably won't resolve correctly unless the unix system has a folder named drive_letter: in the current folder with a copy of the original filestructure.
set link_target [file join $localbase_path $suffix_path]
}
}
} else {
#shortcut basepath is a windows path with drive letter and we found a matching mount point - link_target is set to the resolved path
}
} else {
#shortcut basepath doesn't match expected windows path format - just join it with the suffix and hope for the best
#could be something like a network path or it could be something else entirely
set link_target [file join $localbase_path $suffix_path]
}
}
}
set result [dict create\
link_target $link_target\
link_flags $flags_enabled\
file_attributes [Header_Get_FileAttributes $contents]\
creation_time [Header_Get_CreationTime $contents]\
access_time [Header_Get_AccessTime $contents]\
write_time [Header_Get_WriteTime $contents]\
target_length [Header_Get_FileSize $contents]\
icon_index "<unimplemented>"\
showwnd "$showwnd"\
hotkey [Header_Get_HotKey $contents]\
relative_path "?"\
]
}
proc file_check_header {path} {
#*** !doctools
#[call [fun file_check_header] [arg path] ]
#[para]Return 0|1
#[para]Determines if the .lnk file specified in path has a valid header for a windows shortcut
set c [Get_contents $path 20]
return [Contents_check_header $c]
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::winlnk::resolve
@cmd -name punk::winlnk::resolve\
-summary\
"Return information about a .lnk file (windows shortcut)"\
-help\
"Return a dict of info obtained by parsing the binary data in a windows .lnk file.
If the .lnk header check fails, then the .lnk file probably isn't really a shortcut
file and the dictionary will contain an 'error' key."
@values -min 1 -max 1
path -type string -help "Path to the .lnk file to resolve"
}]
}
proc resolve {path} {
#*** !doctools
#[call [fun resolve] [arg path] ]
#[para] Return a dict of info obtained by parsing the binary data in a windows .lnk file
#[para] If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and the dictionary will contain an 'error' key
set c [Get_contents $path]
if {[Contents_check_header $c]} {
return [contents_get_info $c]
} else {
return [dict create error "lnk_header_check_failed"]
}
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::winlnk::file_show_info
@cmd -name punk::winlnk::file_show_info\
-summary\
"Show information about a .lnk file (windows shortcut)"\
-help\
"Print to stdout the information obtained by parsing the binary data in a windows .lnk file, in a human readable format.
If the .lnk header check fails, then the .lnk file probably isn't really a shortcut file and an error message will be printed."
@values -min 1 -max 1
path -type string -help "Path to the .lnk file to resolve"
}]
}
proc file_show_info {path} {
package require punk::lib
punk::lib::showdict [resolve $path] *
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::winlnk::target
@cmd -name punk::winlnk::target\
-summary\
"Return the target path of a .lnk file (windows shortcut)"\
-help\
"Return the target path of the .lnk file specified in path.
This is a convenience function that extracts the target path from the .lnk file and returns it directly,
without all the additional information that resolve provides. If the .lnk header check fails, then
the .lnk file probably isn't really a shortcut file and an error message will be returned."
@values -min 1 -max 1
path -type string -help "Path to the .lnk file to resolve"
}]
}
proc target {path} {
#*** !doctools
#[call [fun target] [arg path] ]
#[para]Return the target path of the .lnk file specified in path
set info [resolve $path]
if {[dict exists $info error]} {
error [dict get $info error]
} else {
return [dict get $info link_target]
}
}
#proc sample1 {p1 n args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]]
# #[para]Description of sample1
# #[para] Arguments:
# # [list_begin arguments]
# # [arg_def tring p1] A description of string argument p1.
# # [arg_def integer n] A description of integer argument n.
# # [list_end]
# return "ok"
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::winlnk ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::winlnk::lib {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
tcl::namespace::path [tcl::namespace::parent]
#*** !doctools
#[subsection {Namespace punk::winlnk::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#proc utility1 {p1 args} {
# #*** !doctools
# #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
# #[para]Description of utility1
# return 1
#}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::winlnk::lib ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Internal]
#tcl::namespace::eval punk::winlnk::system {
#*** !doctools
#[subsection {Namespace punk::winlnk::system}]
#[para] Internal functions that are not part of the API
#}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::winlnk
}
## Ready
package provide punk::winlnk [tcl::namespace::eval punk::winlnk {
variable pkg punk::winlnk
variable version
set version 0.1.0
}]
return
#*** !doctools
#[manpage_end]

28
src/bootsupport/modules/shellfilter-0.2.1.tm → src/bootsupport/modules/shellfilter-0.2.2.tm

@ -1582,8 +1582,11 @@ namespace eval shellfilter::stack {
#include idposn in poplist
set poplist [lrange $stack $idposn end]
#set stack [lreplace $stack $idposn end]
set stack [lreplace $stack[set stack {}] $idposn end]
#set stack [lreplace $stack[set stack {}] $idposn end]
# 2026-05-19
ledit stack $idposn end
#pop all chans before adding anything back in!
foreach p $poplist {
chan pop $localchan
@ -1622,8 +1625,9 @@ namespace eval shellfilter::stack {
variable pipelines
set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}]
set poplist [lrange $stack $bottom_pop_posn end]
#set stack [lreplace $stack $bottom_pop_posn end]
set stack [lreplace $stack[set stack {}] $bottom_pop_posn end]
#set stack [lreplace $stack[set stack {}] $bottom_pop_posn end]
# 2026-05-19
ledit stack $bottom_pop_posn end
set localchan [dict get $pipelines $pipename device localchan]
foreach p [lreverse $poplist] {
@ -2728,13 +2732,13 @@ 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
switch -regexp -- $lastitem\
{^>[/[:alpha:]]+} {
set lastitem "> [string range $lastitem 1 end]"
}\
{^>>[/[:alpha:]]+} {
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}}
@ -3391,5 +3395,5 @@ namespace eval shellfilter {
package provide shellfilter [namespace eval shellfilter {
variable version
set version 0.2.1
set version 0.2.2
}]

829
src/bootsupport/modules/shellthread-1.6.1.tm

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

5680
src/bootsupport/modules/tomlish-1.1.2.tm

File diff suppressed because it is too large Load Diff

6002
src/bootsupport/modules/tomlish-1.1.3.tm

File diff suppressed because it is too large Load Diff

6199
src/bootsupport/modules/tomlish-1.1.4.tm

File diff suppressed because it is too large Load Diff

6973
src/bootsupport/modules/tomlish-1.1.5.tm

File diff suppressed because it is too large Load Diff

9470
src/bootsupport/modules/tomlish-1.1.6.tm

File diff suppressed because it is too large Load Diff

9470
src/bootsupport/modules/tomlish-1.1.7.tm

File diff suppressed because it is too large Load Diff

40
src/lib/app-punk/repl.tcl

@ -3,56 +3,18 @@ package provide app-punk 1.0
#By the time we get here, we don't expect other packages to have been loaded - but the lib/module paths have already been scanned to populate 'package names'
#puts stdout "$::auto_path"
#puts stderr "-----------"
#puts stderr "tcl::tm::list"
#puts stderr "-----------"
#puts stderr "[join [tcl::tm::list] \n]"
#puts stderr "-----------"
#puts stderr "auto_path"
#puts stderr "-----------"
#puts stderr "[join $::auto_path \n]"
#puts stderr "-----------"
#puts stderr "thread? [package provide Thread]"
set thread_version [package require Thread]
#puts stderr "repl.tcl thread version:$thread_version"
#puts stderr "info loaded:"
#puts stderr [join [info loaded] \n]
#set tpath [lindex [info loaded] 0 0]
#puts stdout "--$tpath--"
#puts stdout "--[file exists $tpath]--"
#set tid [thread::create -preserved]
#thread::send $tid {puts thread1}
#puts stdout "mythread: [thread::id]"
#review
#catch {package require tcllibc}
#punk & shellrun should be in codethreads - but not required in the parent repl threads
package require shellfilter
package require punk::repl
#set v [package provide punk::repl]
#puts stderr "punk::repl version:$v script: [package ifneeded punk::repl $v]"
#puts stderr "package names"
#set packages_present [list]
#foreach p [package names] {
# if {[package provide $p] ne ""} {
# lappend packages_present $p
# }
#}
#puts stderr [join $packages_present \n]
repl::init -safe 0
#puts stderr "Launching repl::start stdin -title app-punk"
#flush stderr
repl::init -type 0
set replresult [repl::start stdin -title app-punk]
#catch {
# puts "app-punk ifneeded: [package ifneeded app-punk 1.0]"
#}
#review
if {[string is integer -strict $replresult]} {
#puts stdout "repl.tcl exiting with numeric code $replresult"

34
src/lib/app-punkshell/punkshell.tcl

@ -43,8 +43,7 @@ namespace eval punkshell {
set stdout_log ""
set stderr_log ""
#set stdout_log [file normalize ~]/punkshell-stdout.txt
#set stderr_log [file normalize ~]/punkshell-stderr.txt
set stdout_log "[pwd]/punkshell_out.log"
set stderr_log "[pwd]/punkshell_err.log"
@ -58,7 +57,7 @@ namespace eval punkshell {
#ideally we don't want to launch an external process to run the script
#variable punkshell_status_log
#shellfilter::log::write $punkshell_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'"
set exepath [file dirname [file join [info nameofexecutable] __dummy__]]
set exepath [file dirname [file join [info nameofexecutable] __dummy__]]
set exedir [file dirname $exepath]
set scriptpath [file normalize $scriptname]
if {![file exists $scriptpath]} {
@ -87,6 +86,14 @@ info script $prevscript
dict with prevglobal {}
}]
#----------------------------------------------------
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -type 0} \n
append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n
append repl_lines {repl::start stdin} \n
set script $repl_lines
#----------------------------------------------------
dict set params -tclscript 1 ;#don't give callback a chance to omit/break this
dict set params -teehandle punkshell
#dict set params -teehandle punksh
@ -177,7 +184,7 @@ dict with prevglobal {}
set repl_lines ""
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -safe 0} \n
append repl_lines {repl::init -type 0} \n
append repl_lines {repl::start stdin} \n
#test
@ -351,16 +358,24 @@ dict with prevglobal {}
}
flush stderr
flush stdout
catch {
shellfilter::stack::remove stderr $chanstack_stderr_redir
shellfilter::stack::remove stdout $chanstack_stdout_redir
}
#puts stderr "\n - punkshell.tcl 1"
#flush stderr
#catch {
# shellfilter::stack::remove stderr $chanstack_stderr_redir
# shellfilter::stack::remove stdout $chanstack_stdout_redir
#}
#puts stderr "punkshell.tcl 2"
shellfilter::stack::delete punkshellout
shellfilter::stack::delete punkshellerr
#puts stderr "punkshell.tcl 3"
set free_info [shellthread::manager::shutdown_free_threads]
#puts stderr "punkshell.tcl 4"
foreach tid [thread::names] {
thread::release $tid
}
#puts stderr "punkshell.tcl 5"
if {[dict size $exitinfo] == 0} {
puts stderr "No result"
@ -380,7 +395,8 @@ dict with prevglobal {}
exit 1
} else {
puts -nonewline stdout [dict get $exitinfo result]
exit 0
#don't call exit on success - allows tk apps to stay alive.
#exit 0
}
}

251
src/lib/app-shellspy/shellspy.tcl

@ -77,6 +77,7 @@ package require shellfilter
package require punk::ansi
package require punk::packagepreference
punk::packagepreference::install
package require logger
#The whole punk infrastructure is overkill for calling arbitrary scripts
#package require punk
@ -98,32 +99,69 @@ namespace eval shellspy {
#todo - default to no logging not even to local syslog
#load a .toml config which can configure logging as desired
set do_log 0
set do_log 1
if {$do_log} {
set debug_syslog_server 127.0.0.1:514
#set debug_syslog_server 172.16.6.42:51500
set error_syslog_server 127.0.0.1:514
set data_syslog_server 127.0.0.1:514
} else {
set debug_syslog_server ""
set error_syslog_server ""
set debug_syslog_server ""
set error_syslog_server ""
set data_syslog_server ""
}
#-------------------------------------------------------------------------------------------------
#tcllib logger infrastructure.
#-------------------------------------------------------------------------------------------------
namespace eval ::shellspy::loggerprocs {
#container for procs/aliases to be pointed to by tcllib logger using log::logproc
proc Dolog {lvl txt} {
#logger calls this in such a way that a straight uplevel can get us the vars/commands in messages substituted
set msg "[clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] ::shellspy $lvl '[uplevel [list subst $txt]]'"
puts stderr $msg
}
proc Runlog {lvl script} {
uplevel 1 $script
}
}
if {![catch {
package require logger
}]} {
logger::initNamespace ::shellspy
foreach lvl [logger::levels] {
interp alias {} ::shellspy::loggerprocs::Log_$lvl {} ::shellspy::loggerprocs::Runlog $lvl
log::logproc $lvl ::shellspy::loggerprocs::Log_$lvl
}
logger::setlevel warn
#namespace path ::shellspy::log
} else {
#e.g tcllib not available, safe interp?
#fake out the logger calls
namespace eval ::shellspy::log {
foreach lvl {debug info notice warn error critical alert emergency} {
proc $lvl {args} {}
}
}
}
#-------------------------------------------------------------------------------------------------
log::notice {puts stderr "shellspy starting with args '$::argv'"}
shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog $debug_syslog_server -file ""]
shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'"
shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]"
log::notice {shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'"}
log::info {shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]"}
#-------------------------------------------------------------------------
##don't write to stdout/stderr before you've redirected them to a log using shellfilter functions
## puts to stdout/stderr will comingle with command's output if performed before the channel stacks are configured.
chan configure stdin -buffering line
chan configure stdout -buffering none
chan configure stderr -buffering none
chan configure stdin -buffering line
chan configure stdout -buffering none
chan configure stderr -buffering none
#set id_ansistrip [shellfilter::stack::add stderr ansistrip -settings {}]
#set id_ansistrip [shellfilter::stack::add stdout ansistrip -settings {}]
@ -131,12 +169,12 @@ namespace eval shellspy {
#redir on the shellfilter stack with no log or syslog specified acts to suppress output of stdout & stderr.
#todo - fix shellfilter code to make this noop more efficient (avoid creating corresponding logging thread and filter?)
#JMN
#set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}}
set redirconfig {}
set redirconfig {-settings {-syslog 127.0.0.1:514 -file ""}}
#set redirconfig {}
lassign [shellfilter::redir_output_to_log "SUPPRESS" {*}$redirconfig] chanstack_stdout_redir chanstack_stderr_redir
shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]"
log::notice {shellfilter::log::write $shellspy_status_log "shellfilter::redir_output_to_log SUPPRESS DONE [clock_sec]"}
###
#we can now safely write to stderr/stdout and it will not interfere with stderr/stdout from the dispatched command.
#This is because when shellfilter::run is called it temporarily adds it's own filter in place of the redirection we just added.
@ -179,24 +217,32 @@ namespace eval shellspy {
set stdout_log ""
set stderr_log ""
shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]"
log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr ABOUTTO [clock_sec]"}
set errdeviceinfo [shellfilter::stack::new shellspyerr -settings [list -tag "shellspyerr" -buffering none -raw 1 -syslog $data_syslog_server -file $stderr_log]]
shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]"
log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyerr DONE [clock_sec]"}
shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]"
log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout ABOUTTO [clock_sec]"}
set outdeviceinfo [shellfilter::stack::new shellspyout -settings [list -tag "shellspyout" -buffering none -raw 1 -syslog $data_syslog_server -file $stdout_log]]
set commandlog [dict get $outdeviceinfo localchan]
#puts $commandlog "HELLO $commandlog"
#flush $commandlog
shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]"
log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::new shellspyout DONE [clock_sec]"}
#note that this filter is inline with the data teed off to the shellspyout log.
#To filter the main stdout an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data.
set id_ansistrip [shellfilter::stack::add shellspyout ansistrip -settings {}]
shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]"
#------------------------------------------------------------------------------
#note that this filter is inline with the data teed off to the shellspyout log. (ie it is a transform on one of the fifo2 ends)
#To filter the main stdout, an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data.
#TODO - review
set id_ansistrip_out [shellfilter::stack::add shellspyout ansistrip -settings {}]
set id_ansistrip_err [shellfilter::stack::add shellspyerr ansistrip -settings {}]
#This can cause problems e.g unknown channel rc2 (tcl::chan::fifo2) in the repl - review
#something wrong with adding and removing transforms on the fifo2 channels in shellfilter::run?
#only seems to happen when in mode raw where we need to emit to stdout after each keypress,
#and only when we have a queued tcl script that will run before or after the repl.
#e.g punk902z dev shellspy lib:hello.tcL
log::debug {shellfilter::log::write $shellspy_status_log "shellfilter::stack::add shellspyout ansistrip DONE [clock_sec]"}
#------------------------------------------------------------------------------
#set id_out [shellfilter::stack::add stdout rebuffer -settings {}]
@ -378,7 +424,7 @@ namespace eval shellspy {
package require shellrun
package require punk::repl
puts stdout "quit to exit"
repl::init -safe 0
repl::init -type 0
repl::start stdin -defaultresult %r%
}]]
}
@ -709,7 +755,7 @@ dict with prevglobal {}
set repl_lines ""
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -safe 0} \n
append repl_lines {repl::init -type 0} \n
append repl_lines {repl::start stdin} \n
#test
@ -728,12 +774,11 @@ dict with prevglobal {}
dict set params -tclscript 1 ;#don't give callback a chance to omit/break this
dict set params -teehandle shellspy
set params [dict merge $params [get_channel_config $::testconfig]]
set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}]
set exitinfo [shellfilter::run $script {*}$params]
shellfilter::stack::remove stderr $id_err
@ -822,17 +867,21 @@ dict with prevglobal {}
if {$replwhen eq "repl_first"} {
#this concept is probably overly complex and unnecessary. If the user is in a repl then they can just source scripts from the prompt.
#
#we need to cooperate with the repl to get the script to run on exit
namespace eval ::repl {}
set ::repl::post_script $script
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -safe 0} \n
append repl_lines {repl::init -type 0} \n
append repl_lines [list repl::submit stdin "eval \{ puts \"Queued script will run after 'quit'. 'exit' to abort.\" \};\n"] \n
append repl_lines {repl::start stdin} \n
set script "$repl_lines"
} elseif {$replwhen eq "repl_last"} {
#run a script first e.g could be a tk app - but maintain control via the repl.
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -safe 0} \n
append repl_lines {repl::init -type 0} \n
append repl_lines [list repl::submit stdin "eval \{ $script \};\n"] \n
append repl_lines {repl::start stdin} \n
@ -848,27 +897,31 @@ dict with prevglobal {}
dict set params -tclscript 1 ;#don't give callback a chance to omit/break this
dict set params -teehandle shellspy
dict set params -debug 1 ;#JMN
#dict set params -syslog "127.0.0.1:514"
dict set params -syslog {}
#dict set params -teehandle punksh
set params [dict merge $params [get_channel_config $::testconfig]]
set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}]
#set id_out [shellfilter::stack::add stdout ansiwrap -action sink-locked -settings {-colour {green bold}}]
set exitinfo [shellfilter::run $script {*}$params]
#jmn 2026-05-19
shellfilter::log::write $shellspy_status_log "do_script exitinfo $exitinfo"
shellfilter::stack::remove stderr $id_err
#shellfilter::stack::remove stdout $id_out
#if {[lindex $exitinfo 0] eq "exitcode"} {
# shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo"
#}
#jjj
#shellfilter::log::write $shellspy_status_log "do_script raw exitinfo: $exitinfo"
if {[dict exists $exitinfo errorInfo]} {
#strip out the irrelevant info from the errorInfo - we don't want info beyond 'invoked from within' as this is just plumbing related to the script sourcing
set stacktrace [string map [list \r\n \n] [dict get $exitinfo errorInfo]]
set output ""
set output ""
set tracelines [split $stacktrace \n]
foreach ln $tracelines {
if {[string match "*invoked from within*" $ln]} {
@ -891,7 +944,8 @@ dict with prevglobal {}
lappend out $a
}
return $out
}
}
proc do_shell {shell args} {
variable shellspy_status_log
shellfilter::log::write $shellspy_status_log "do_shell $shell got '$args' [llength $args]"
@ -899,37 +953,38 @@ dict with prevglobal {}
shellfilter::log::write $shellspy_status_log "do_shell $shell xgot '$args'"
set params [do_callback_parameters $shell]
dict set params -teehandle shellspy
set params [dict merge $params [get_channel_config $::testconfig]]
set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}]
#shells that take -c and need all args passed together as a string
set exitinfo [shellfilter::run [concat $shell -c [shellescape $args]] {*}$params]
shellfilter::stack::remove stdout $id_out
if {[lindex $exitinfo 0] eq "exitcode"} {
shellfilter::log::write $shellspy_status_log "do_shell $shell returning $exitinfo"
}
return $exitinfo
}
proc do_wsl {distdefault args} {
variable shellspy_status_log
shellfilter::log::write $shellspy_status_log "do_wsl $distdefault got '$args' [llength $args]"
set args [do_callback wsl {*}$args] ;#use dist?
shellfilter::log::write $shellspy_status_log "do_wsl $distdefault xgot '$args'"
set params [do_callback_parameters wsl]
dict set params -debug 0
set params [dict merge $params [get_channel_config $::testconfig]]
set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}]
dict set params -teehandle shellspy ;#shellspyout shellspyerr must exist
set exitinfo [shellfilter::run [concat wsl -d $distdefault -e [shellescape $args]] {*}$params]
@ -949,11 +1004,11 @@ dict with prevglobal {}
lappend commands [list punkd [list sub punkdict singleopts {any}]]
#'shout' extension (all uppercase) to force use of tclsh as a separate process
#'shout' extension (all uppercase) to force use of tclsh as a separate process
#todo - detect various extensions - and use all script-preceding unmatched args as interpreter+options
#e.g perl,php,python etc.
#e.g perl,php,python etc.
#For tcl will make it easy to test different interpreter output from tclkitsh, tclsh8.6, tclsh8.7 etc
#for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config
#for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config
#(or just attempt launch in case there is shebang line in script)
#we may get ambiguity with existing shell match-specs such as -c /c -r. todo - only match those in first slot?
lappend commands [list tclscriptprocess [list match [list {.*\.TCL$} {.*\.TM$} {.*\.TK$}] dispatch [list shellspy::do_script_process [info nameofexecutable] %matched%] dispatchtype raw dispatchglobal 1 singleopts {any}]]
@ -997,7 +1052,8 @@ dict with prevglobal {}
lappend commands [list runcmdfile [list sub word$i singleopts {any}]]
}
lappend commands [list libscript [list match [list {lib:.*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]]
#Only match lib:scriptname at start, with a single colon (avoid matching on -tcl lib::blah::func -tcl punk::lib::func etc)
lappend commands [list libscript [list match [list {^lib:[^:].*$} ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]]
for {set i 0} {$i < 25} {incr i} {
lappend commands [list libscript [list sub word$i singleopts {any}]]
}
@ -1043,8 +1099,7 @@ dict with prevglobal {}
for {set i 0} {$i < 25} {incr i} {
lappend commands [list runpwsht [list sub word$i singleopts {any}]]
}
lappend commands {runcmd {match {^/c$} dispatch shellspy::do_in_cmdshell dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}}
for {set i 0} {$i < 25} {incr i} {
lappend commands [list runcmd [list sub word$i singleopts {any}]]
@ -1058,16 +1113,16 @@ dict with prevglobal {}
for {set i 0} {$i < 25} {incr i} {
lappend commands [list runcmdb [list sub word$i singleopts {any} longopts {any} pairopts {any}]]
}
lappend commands [list wslraw [list match {^wsl$} dispatch [list shellspy::do_wsl AlmaLinux9] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]]
for {set i 0} {$i < 25} {incr i} {
lappend commands [list wslraw [list sub word$i singleopts {any}]]
}
#e.g
# punk -tcl info patch
# punk -tcl info patch
# punk -tcl eval "package require punk::char;punk::char::charset_page dingbats"
lappend commands [list tclline [list match [list {^-tcl$}] dispatch [list shellspy::do_tclline tcl] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]]
for {set i 0} {$i < 25} {incr i} {
lappend commands [list tclline [list sub word$i singleopts {any}]]
@ -1126,38 +1181,50 @@ dict with prevglobal {}
set is_call_error 0
set arglist [list] ;#processed args result - contains dispatch info etc.
if {[catch {
#-----------------------------------------------------------------
#check_flags does the actual dispatch ie calling of the command above such as do_script, do_shell etc.
#in turn these use shellfilter::run which does the actual running of the command and capturing of output, exitcode etc.
set arglist [check_flags {*}$argdefinitions]
#-----------------------------------------------------------------
} callError]} {
log::error {shellfilter::log::write $shellspy_status_log "check_flags error: $callError"}
puts -nonewline stderr "|shellspy-stderr> ERROR during command dispatch\n"
puts -nonewline stderr "|shellspy-stderr> $callError\n"
puts -nonewline stderr "|shellspy-stderr> [set ::errorInfo]\n"
shellfilter::log::write $shellspy_status_log "check_flags error: $callError"
set is_call_error 1
} else {
shellfilter::log::write $shellspy_status_log "check_flags result: $arglist"
log::notice {shellfilter::log::write $shellspy_status_log "check_flags result: $arglist"}
}
shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]"
log::debug {shellfilter::log::write $shellspy_status_log "check_flags dispatch -done- [clock_sec]"}
#puts stdout "sp2. $::argv"
if {[catch {
set tidyinfo [shellfilter::logtidyup]
} errMsg]} {
shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server}
shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]"
after 200
}
#don't open more logs..
#----------------------------------------------------------------------------------
# Calling shellfilter::logtidyup (with no tags) when using tcl::chan::fifo2 will cause a deadlock.
# review - why exactly?
#----------------------------------------------------------------------------------
#if {[catch {
# set tidyinfo [shellfilter::logtidyup]
# } errMsg]} {
#
# shellfilter::log::open shellspy-error {-tag shellspy-error -syslog $error_syslog_server}
# shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]"
# after 200
#}
##don't open more logs..
#puts stdout ">$tidyinfo"
#----------------------------------------------------------------------------------
#lassign [shellfilter::redir_output_to_log "SUPPRESS"] id_stdout_redir id_stderr_redir
catch {
#catch {
# shellfilter::stack::remove stderr $chanstack_stderr_redir
# shellfilter::stack::remove stdout $chanstack_stdout_redir
#}
shellfilter::stack::remove stderr $chanstack_stderr_redir
shellfilter::stack::remove stdout $chanstack_stdout_redir
}
#shellfilter::log::write $shellspy_status_log "logtidyup -done- $tidyinfo"
@ -1165,7 +1232,7 @@ dict with prevglobal {}
set errorlist [dict get $tidyinfo errors]
if {[llength $errorlist]} {
foreach err $errorlist {
puts -nonewline stderr "|shellspy-final> worker-error-set $err\n"
puts -nonewline stderr "|shellspy-final> worker-error-set $err\n"
}
}
}
@ -1182,8 +1249,8 @@ dict with prevglobal {}
} errMsg]} {
puts stdout "shellspy logtidyup error $errMsg"
flush stdout
shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server}
shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]"
log::error {shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server}}
log::error {shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]"}
after 100
}
#puts [shellfilter::stack::status shellspyout]
@ -1215,19 +1282,29 @@ dict with prevglobal {}
#}
shellfilter::stack::delete shellspyout
shellfilter::stack::delete shellspyerr
if {[catch {
shellfilter::stack::delete shellspyout
shellfilter::stack::delete shellspyerr
} errMsg]} {
#shellfilter::log::open shellspy-final {-tag shellspy-final -syslog $error_syslog_server}
#shellfilter::log::write shellspy-final "FINAL stack delete error $errMsg\n [set ::errorInfo]"
puts stderr "FINAL stack delete error $errMsg\n [set ::errorInfo]"
after 100
}
set free_info [shellthread::manager::shutdown_free_threads]
#puts stdout $free_info
#flush stdout
if {[package provide zzzload] ne ""} {
#if zzzload used and not shutdown - we can get deadlock
#puts "zzzload::loader_tid [set ::zzzload::loader_tid]"
#zzzload::shutdown
}
# if {[package provide zzzload] ne ""} {
# #if zzzload used and not shutdown - we can get deadlock
# #puts "zzzload::loader_tid [set ::zzzload::loader_tid]"
# #zzzload::shutdown
# }
#puts stdout "threads: [thread::names]"
#flush stdout
#puts stdout "calling release on remaining threads"
#flush stdout
foreach tid [thread::names] {
thread::release $tid
}
@ -1254,7 +1331,7 @@ dict with prevglobal {}
flush stderr
exit 1
}
}
}
foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] {
if {[dict exists $arglist dispatch $tclscript_flavour result error]} {
@ -1278,12 +1355,12 @@ dict with prevglobal {}
foreach tclscript_flavour [list tclline tclkit punkline punkshellline tkline tkshellline libscript help] {
if {[dict exists $arglist dispatch $tclscript_flavour result result]} {
puts -nonewline stdout [dict get $arglist dispatch $tclscript_flavour result result]
flush stdout
exit 0
}
}
#if we call exit - package require Tk script files will exit prematurely
#review
#if we call exit - package require Tk script files will exit prematurely
#exit 0
}

2
src/lib/app_shell/app_shell.tcl

@ -3,7 +3,7 @@ package provide app_shell 1.0
package require Thread
package require shellfilter
package require punk::repl
repl::init -safe 0
repl::init -type 0
set replresult [repl::start stdin -title app_shell]
if {[string is integer -strict $replresult]} {
exit $replresult

2
src/lib/app_shellrun/app_shellrun.tcl

@ -178,7 +178,7 @@ dict with prevglobal {}
set repl_lines ""
append repl_lines {package require punk::repl} \n
append repl_lines {repl::init -safe 0} \n
append repl_lines {repl::init -type 0} \n
append repl_lines {repl::start stdin} \n
#test

93
src/modules/overtype-999999.0a1.0.tm

@ -401,13 +401,20 @@ tcl::namespace::eval overtype {
set opt_console [tcl::dict::get $opts -console]
#--------------------------------------------------------------------------
#TODO
#REVIEW - punk::console package may not be loaded
set cursor_style_overtype {3 underline-blink}
set cursor_style_insert {5 beam-blink}
if {$opt_insert_mode} {
punk::console::cursor_style -console $opt_console $cursor_style_insert
set initial_cursor_style $cursor_style_insert
} else {
set initial_cursor_style $cursor_style_overtype
}
catch {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#--------------------------------------------------------------------------
# ----------------------------
# -experimental dev flag to set flags etc
@ -695,22 +702,23 @@ tcl::namespace::eval overtype {
#review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary -
#but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l
set renderopts [list -experimental $opt_experimental\
-cp437 $opt_cp437\
-info 1\
-crm_mode [tcl::dict::get $vtstate crm_mode]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-reverse_mode [tcl::dict::get $vtstate reverse_mode]\
-cursor_restore_attributes $cursor_saved_attributes\
-transparent $opt_transparent\
-width [tcl::dict::get $vtstate renderwidth]\
-exposed1 $opt_exposed1\
-exposed2 $opt_exposed2\
-expand_right $opt_expand_right\
-cursor_column $col\
-cursor_row $row\
-overtext_type $overtext_type\
set renderopts [list -experimental $opt_experimental {*}{
} -cp437 $opt_cp437 {*}{
} -info 1 {*}{
} -crm_mode [tcl::dict::get $vtstate crm_mode] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -reverse_mode [tcl::dict::get $vtstate reverse_mode] {*}{
} -cursor_restore_attributes $cursor_saved_attributes {*}{
} -transparent $opt_transparent {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -exposed1 $opt_exposed1 {*}{
} -exposed2 $opt_exposed2 {*}{
} -expand_right $opt_expand_right {*}{
} -cursor_column $col {*}{
} -cursor_row $row {*}{
} -overtext_type $overtext_type {*}{
}
]
set rinfo [renderline {*}$renderopts $undertext $overtext]
@ -940,14 +948,15 @@ tcl::namespace::eval overtype {
puts stdout ">>>renderspace<<<[a+ red bold]overflow_right during restore_cursor[a]"
set sub_info [overtype::renderline\
-info 1\
-width [tcl::dict::get $vtstate renderwidth]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-expand_right [tcl::dict::get $opts -expand_right]\
""\
$overflow_right\
set sub_info [overtype::renderline {*}{
} -info 1 {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -expand_right [tcl::dict::get $opts -expand_right] {*}{
} "" {*}{
} $overflow_right {*}{
}
]
set foldline [tcl::dict::get $sub_info result]
tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..?
@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype {
}
#JMN
if {[tcl::dict::get $vtstate insert_mode]} {
puts "setting cursor to insert style"
punk::console::cursor_style -console $opt_console $cursor_style_insert
} else {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#REVIEW - we don't want to emit cursor_style ANSI unless it changes.
#if {[tcl::dict::get $vtstate insert_mode]} {
# puts "setting cursor to insert style"
# punk::console::cursor_style -console $opt_console $cursor_style_insert
#} else {
# punk::console::cursor_style -console $opt_console $cursor_style_overtype
#}
#puts "renderedrow_max: $renderedrow_max"
#check for null lines below renderedrow_max (and at tail) and trim.
@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype {
#broken:
#todo - renderline -overflow is invalid.
# we need renderline to support -expand_left ??
set rinfo [renderline\
-info 1\
-insert_mode 0\
-transparent $opt_transparent\
-exposed1 $opt_exposed1 -exposed2 $opt_exposed2\
-overflow $opt_overflow\
-startcolumn [expr {1 + $startoffset}]\
$undertext $overtext]
set rinfo [renderline {*}{
} -info 1 {*}{
} -insert_mode 0 {*}{
} -transparent $opt_transparent {*}{
} -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 {*}{
} -overflow $opt_overflow {*}{
} -startcolumn [expr {1 + $startoffset}] {*}{
} $undertext $overtext {*}{
}
]
set replay_codes [tcl::dict::get $rinfo replay_codes]
set rendered [tcl::dict::get $rinfo result]
if {!$opt_overflow} {
@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype {
-crm_mode -default 0 -type boolean
-autowrap_mode -default 1 -type boolean
-reverse_mode -default 0 -type boolean
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\
-choicelabels\
{
1 "return a dict with raw fields"
2 "return a dict using ansistring VIEW"

2
src/modules/poshinfo-999999.0a1.0.tm

@ -207,7 +207,7 @@ tcl::namespace::eval poshinfo {
-help "e.g omp"
-as -default "table" -choices {list showlist dict showdict table tableobject plaintext}\
-help "return type of result"
@values -min 0
@values -min 0
globs -multiple 1 -default * -help ""
}
proc themes {args} {

34
src/modules/punk-0.1.tm

@ -26,13 +26,11 @@ namespace eval punk {
#we call tcl::tm::list to trigger the initial set of tm paths before
#we can override it, otherwise our changes will be lost
#REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
return "\
apply {{ap tmlist} {
set ::auto_path \$ap
list apply {{ap tmlist} {
set ::auto_path $ap
tcl::tm::list
set ::tcl::tm::paths \$tmlist
}} {$::auto_path} {[tcl::tm::list]}
"
set ::tcl::tm::paths $tmlist
}} $::auto_path [tcl::tm::list]
}
@ -283,14 +281,6 @@ namespace eval punk {
#set path "[file dirname [info nameofexecutable]];.;"
set path "[file dirname [info nameofexecutable]];"
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
# ------------------------
#Note that unlike an ordinary Tcl array - the linked ::env behaves differently.
@ -307,6 +297,15 @@ namespace eval punk {
}
# ------------------------
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
#change2
if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
set lookfor [list $name]
@ -5492,9 +5491,10 @@ namespace eval punk {
#ctrl-c propagation also needs to be considered
set teehandle punksh
uplevel 1 [list ::catch \
[list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \
::tcl::UnknownResult ::tcl::UnknownOptions]
uplevel 1 [list ::catch {*}{
} [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{
} ::tcl::UnknownResult ::tcl::UnknownOptions
]
if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} {
dict set ::tcl::UnknownOptions -code error

1
src/modules/punk/aliascore-999999.0a1.0.tm

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

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

@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi {
@cmd -name punk::ansi::sauce -summary\
"SAUCE info from file"\
-help\
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data."
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data.
Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format
that was commonly used in old ANSI art files to store information about the file, such as
title, author, group, date, and comments.
It may also have fields to specify the number of columns and rows in the ANSI art,
as well as flags for specific display attributes.
It may also be used on other types of files such as bitmap, vector, audio, binarytext,
xbin, archive and executable files.
It is a 128-byte block of data that is typically appended to the end of a file.
https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm"
-encoding -default iso8859-1 -type string -help\
"The default iso8859-1 is equivalent to binary ans should
work in the usual case.

1
src/modules/punk/ansi/sauce-999999.0a1.0.tm

@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce {
variable PUNKARGS
variable PUNKARGS_aliases
#https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm
lappend PUNKARGS [list {
@id -id "(package)punk::ansi::sauce"
@package -name "punk::ansi::sauce" -help\

144
src/modules/punk/args/moduledoc/iocp-999999.0a1.0.tm

@ -0,0 +1,144 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application punk::args::moduledoc::iocp 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
package require Tcl 8.6-
tcl::namespace::eval punk::args::moduledoc::iocp {
variable PUNKARGS
namespace eval argdoc {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS_aliases {::iocp::inet::socket ::socket}
lappend PUNKARGS [list {
@id -id "(package)punk::args::moduledoc::iocp"
@package -name "punk::args::moduledoc::iocp" -help\
"punk::args documentation for magicsplat iocp package"
}]
}
}
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval punk::args::moduledoc::iocp {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
namespace eval argdoc {
#namespace for custom argument documentation
proc package_name {} {
return punk::args::moduledoc::iocp
}
proc about_topics {} {
#info commands results are returned in an arbitrary order (like array keys)
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
#Adjust this function or 'default_topics' if a different order is required
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
# -------------------------------------------------------------
# get_topic_ functions add more to auto-include in about topics
# -------------------------------------------------------------
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package punk::args::moduledoc::iocp
} \n]
}
proc get_topic_License {} {
return "BSD"
}
proc get_topic_Version {} {
return "$::punk::args::moduledoc::iocp::version"
}
proc get_topic_Contributors {} {
set authors {<unspecified>}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
# -------------------------------------------------------------
}
# we re-use the argument definition from punk::args::standard_about and override some items
set overrides [dict create]
dict set overrides @id -id "::punk::args::moduledoc::iocp::about"
dict set overrides @cmd -name "punk::args::moduledoc::iocp::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About punk::args::moduledoc::iocp
}] \n]
dict set overrides topic -choices [list {*}[punk::args::moduledoc::iocp::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [punk::args::moduledoc::iocp::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
#standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on
set argd [punk::args::parse $args withid ::punk::args::moduledoc::iocp::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::punk::args::moduledoc::iocp::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
# variable PUNKARGS
# variable PUNKARGS_aliases
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::args::moduledoc::iocp ::punk::args::moduledoc::iocp::argdoc
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::args::moduledoc::iocp [tcl::namespace::eval punk::args::moduledoc::iocp {
variable pkg punk::args::moduledoc::iocp
variable version
set version 999999.0a1.0
}]
return

3
src/modules/punk/args/moduledoc/iocp-buildversion.txt

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

11
src/modules/punk/args/moduledoc/tkcore-999999.0a1.0.tm

@ -368,7 +368,6 @@ tcl::namespace::eval punk::args::moduledoc::tkcore {
# -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS_aliases {::button ::tk::button}
punk::args::define {
@dynamic
@id -id ::tk::button
@cmd -name "Tk Builtin: tk::button"\
-summary\
@ -392,8 +391,7 @@ tcl::namespace::eval punk::args::moduledoc::tkcore {
@opts -type string -parsekey "" -group "STANDARD OPTIONS" -grouphelp\
""
}\
{
} {
${[punk::args::resolved_def -types opts (default)::punk::args::moduledoc::tkcore::tk_standardoptions\
-activebackground\
-activeforeground\
@ -419,8 +417,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore {
-textvariable\
-underline\
-wraplength\
]}}\
{
]}
} {
@opts -type string -parsekey "" -group "WIDGET-SPECIFIC OPTIONS" -grouphelp\
""
-command -type script -help\
@ -508,7 +506,8 @@ tcl::namespace::eval punk::args::moduledoc::tkcore {
dict set CLASS_BUTTON_CHOICEINFO $sub {{doctype native} {doctype punkargs}}
#override manual synopsis entry
#puts stderr "override manual synopsis entry with [punk::ns::synopsis "::package $sub"]"
dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"]
#dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::ns::synopsis "(widgetcommand)Class_Button $sub"]
dict set CLASS_BUTTON_CHOICELABELS $sub [punk::ansi::a+ normal][punk::args::synopsis "(widgetcommand)Class_Button $sub"]
}
}

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

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

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

@ -128,11 +128,12 @@ tcl::namespace::eval punk::cesu {
#binary scan $4 c 4
incr 1 ;#// Effectively adds 0x10000 to the codepoint ?
return [binary format ccca \
[expr {0xF0 | (($1 & 0xC) >> 2)}] \
[expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \
[expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \
$4]
return [binary format ccca {*}{
} [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{
} [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{
} [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{
} $4
]
}
#
@ -147,14 +148,15 @@ tcl::namespace::eval punk::cesu {
#binary scan $4 c 4
incr 1
return [binary format ccca \
[expr {0xF0 | (($1 & 0xC) >> 2)}] \
[expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] \
[expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] \
$4]
return [binary format ccca {*}{
} [expr {0xF0 | (($1 & 0xC) >> 2)}] {*}{
} [expr {0x80 | (($1 & 0x3) << 4) | (($2 & 0x3C) >> 2)}] {*}{
} [expr {0x80 | (($2 & 0x3) << 4) | ($3 & 0xF)}] {*}{
} $4
]
} else {
puts "Invalid sequence: $char"
puts "Invalid sequence: $char"
return $char
}
}

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

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

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

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

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

@ -44,7 +44,14 @@
#[list_begin itemized]
package require Tcl 8.6-
#----------------------------------------------------
#Although we need to be in an environment with Thread available to use punk::console,
# we don't want to require Thread as a hard dependency in the interp we're running in.
# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp.
#TODO.
package require Thread ;#tsv required to sync is_raw
#----------------------------------------------------
package require punk::ansi
package require punk::args
#*** !doctools
@ -282,7 +289,7 @@ namespace eval punk::console {
ignore the regex match 'ok' response
and keep going."
-return -type string -default payload -choices {payload dict} -choicelabels {
dict\
dict
"dict with keys prefix,response,payload,all"
} -help\
"Return format"
@ -290,12 +297,12 @@ namespace eval punk::console {
-console -default {stdin stdout} -type list -help\
"console/terminal (currently list of in/out channels) (todo - object?)"
-passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels {
none\
none
{ ANSI sent without any passthrough wrapping.
A terminal multiplexer such as tmux,screen,zellij may
not pass the request through to the underlying terminal(s)
This is the recommended/normal value for the option.}
tmux\
tmux
{ Wrap ANSI sequence with tmux passthrough sequence.
\x1bPtmux\;<originalsequence_with_escapes_doubled>\x1b\\
Note that a tmux session could be connected to multiple
@ -304,7 +311,7 @@ namespace eval punk::console {
Passthrough should generally be avoided except for debug/test
purposes.
}
auto\
auto
{ Use existence of ::env(TMUX) to detect tmux and
send tmux passthrough sequence.
Not recommended except for debug/test purposes.
@ -2779,6 +2786,7 @@ namespace eval punk::console {
This allows querying the current style and then re-setting it after temporarily changing it."
}]
}
proc cursor_style {args} {
set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style]
lassign [dict values $argd] leaders opts values

36
src/modules/punk/du-999999.0a1.0.tm

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

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

@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check {
if {"windows" ne $::tcl_platform(platform)} {
set bug 0
} else {
set tmpdir [file tempdir] ;#tcl 9+
set testfile [file join $tmpdir "bugtest"]
if {![catch {file tempdir} tmpdir]} {
#tcl 9+ has 'file tempdir'
set testfile [file join $tmpdir "bugtest"]
} else {
#fallback for older tcl versions - use env TEMP/TMP or current directory
set tmpdir ""
foreach e {TEMP TMP} {
if {[info exists ::env($e)] && [file isdirectory ::env($e)]} {
set tmpdir ::env($e)
break
}
}
if {$tmpdir eq ""} {
#no env vars - fallback to current directory
set tmpdir [pwd]
}
set testfile [file join $tmpdir "bugtest"]
}
set fd [open $testfile w]
puts $fd test
close $fd
set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*]
if {[file exists $testfile]} {
file delete $testfile
}
foreach r $globresult {
if {$r ne "bugtest"} {
set bug 1
@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat {
#*** !doctools
#[call [fun lpop] [arg listvar] [opt {index}]]
#[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop
upvar $lvar l
#upvar $lvar l
upvar 1 $lvar l
if {![llength $args]} {
set args [list end]
}
@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat {
#set newlist [lremove $newlist $tailidx]
#set newlist [lreplace $newlist $tailidx $tailidx]
set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx]
#don't use ledit here!
#we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin.
} else {
set sublist [lindex $newlist {*}$sublist_path]
#set sublist [lremove $sublist $tailidx]
@ -1990,6 +2011,7 @@ namespace eval punk::lib {
#for each of the above strings we should get a command recognised for the 'puts e*' items as well as the 'list' item, but not for the 'puts n' items since they are within curly braces and not subject to command substitution.
#---------------------------------
proc tclscript_info {script {nscontext ""}} {
package require parser
#if the script is ANSI highlighted - the square brackets within the ANSI will disrupt our parsing.
if {[punk::ansi::ta::detect $script]} {
#we will strip it - but be noisy on stderr since a) it's a bi inefficient to pass in ansi highlighted scripts.
@ -3393,16 +3415,51 @@ namespace eval punk::lib {
return [list $stdout $stderr 0]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] "
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
# set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
#} else {
# set RST [punk::ansi::a]
# #set sep " [a+ Web-seagreen]=[a] "
# set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
# #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
# #NOTE that \u2260 not suitable for non utf-8 terminals.
# set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
#}
#todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8.
# (safe detection methods for utf-8 support?)
#if colour is disabled we want to refresh this.
#therefore we use @dynamic
proc get_sep {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support
}
return $sep
}
proc get_sep_mismatch {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
#NOTE that \u2260 not suitable for non utf-8 terminals.
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] "
}
return $sep_mismatch
}
set argspec [string map [list %sep% $sep] {
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::pdict
@cmd -name pdict -help\
"Print dict keys,values to channel
@ -3412,7 +3469,7 @@ namespace eval punk::lib {
@opts -any 1
#default separator to provide similarity to tcl's parray function
-separator -default "%sep%"
-separator -default "${$DYN_SEP}"
-roottype -default "dict"
-substructure -default {}
-channel -default stdout -help\
@ -3423,33 +3480,36 @@ namespace eval punk::lib {
dictvar -type string -help "name of variable. Can be a dict, list or array"
patterns -type string -default "*" -multiple 1 -help {Multiple patterns can be specified as separate arguments.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
Each pattern consists of 1 or more segments separated by the hierarchy separator (forward slash)
The system uses similar patterns to the punk pipeline pattern-matching system.
The default assumed type is dict - but an array will automatically be extracted into key value pairs so will also work.
Segments are classified into list,dict and string operations.
Leading % indicates a string operation - e.g %# gives string length
A segment with a single @ is a list operation e.g @0 gives first list element, @1-3 gives the lrange from 1 to 3
(todo - change to indexset syntax @1..3 @1..end-1 etc)
A segment containing 2 @ symbols is a dict operation. e.g @@k1 retrieves the value for dict key 'k1'
The operation type indicator is not always necessary if lower segments in the hierarchy are of the same type as the previous one.
e.g1 pdict env */%#
the pattern starts with default type dict, so * retrieves all keys & values,
the next hierarchy switches to a string operation to get the length of each value.
e.g2 pdict env W* S*
Here we supply 2 patterns, each in default dict mode - to display keys and values where the keys match the glob patterns
e.g3 pdict punk_testd */*
This displays 2 levels of the dict hierarchy.
Note that if the sublevel can't actually be interpreted as a dictionary (odd number of elements or not a list at all)
- then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator.
e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1
Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent
The second level segment in each pattern switches to a dict operation to retrieve the value by key.
When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level.
}
}]
#puts stderr "$argspec"
set argd [punk::args::parse $args withdef $argspec]
}
proc pdict {args} {
package require punk::args
variable has_punk_ansi
set argd [punk::args::parse $args withid ::punk::lib::pdict]
set opts [dict get $argd opts]
set dvar [dict get $argd values dictvar]
set patterns [dict get $argd values patterns]
@ -3467,32 +3527,11 @@ namespace eval punk::lib {
showdict {*}$opts $dvalue {*}$patterns
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
#set sep " [a+ Web-seagreen]=[a] "
variable has_punk_ansi
if {!$has_punk_ansi} {
set RST ""
set sep " = "
#set sep_mismatch " mismatch "
set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
set RST [punk::ansi::a]
set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
}
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
namespace eval argdoc {
variable PUNKARGS
set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::showdict
@cmd -name punk::lib::showdict -help "display dictionary keys and values"
#todo - table tableobject
@ -3502,10 +3541,8 @@ namespace eval punk::lib {
"Trim whitespace off rhs of each line.
This can help prevent a single long line that wraps in terminal from making
every line wrap due to long rhs padding."
-separator -default {%sep%} -help\
"Separator column between keys and values"
-separator_mismatch -default {%sep_mismatch%} -help\
"Separator to use when patterns mismatch"
-separator -default "${$DYN_SEP}" -help "Separator column between keys and values"
-separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch"
-roottype -default "dict" -help\
"list,dict,string"
-ansibase_keys -default "" -help\
@ -3524,7 +3561,23 @@ namespace eval punk::lib {
"dict or list value"
patterns -default "*" -type string -multiple 1 -help\
"key or key glob pattern"
}]]
}]
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
set RST [punk::ansi::a]
set argd [punk::args::parse $args withid ::punk::lib::showdict]
#for punk::lib - we want to reduce pkg dependencies.
# - so we won't even use the tcllib debug pkg here
@ -4879,6 +4932,23 @@ namespace eval punk::lib {
the range will come out the same, so the result needs to be treated as a 1-based
set of indices when performing further operations.
"
-return -type string -default indices -choices {indices pairs} -choicecolumns 1 -choicelabels {
indices
" return a list of all indices in the order specified by the indexset,
with duplicates if specified by the indexset.
So for example
indexset_resolve 6 3..0,2,4,end
would return 3 2 1 0 2 4 5"
pairs
" return a list of index pairs representing the start and end of each range,
which may be increasing or decreasing, or just a single index
(where start and end are the same).
So for example
indexset_resolve -return pairs 6 3..0,2,4,end
would return {3 0} {2 2} {4 5}
indexset_resolve -return pairs 7 3..0,2,4,end
would return {3 0} {2 2} {4 4} {6 6}"
}
@values -min 2 -max 3
numitems -type integer
indexset -type indexset -help "comma delimited specification for indices to return"
@ -4892,32 +4962,66 @@ namespace eval punk::lib {
# for the unhappy path - the punk::args::parse is fine to generate the usage/error information.
# --------------------------------------------------
if {[llength $args] < 2} {
#too few args - use parser to generate error message
punk::args::resolve $args withid ::punk::lib::indexset_resolve
}
set indexset [lindex $args end]
set numitems [lindex $args end-1]
set indexset [lindex $args end]
if {![string is integer -strict $numitems] || ![is_indexset $indexset]} {
#use parser on unhappy path only
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
#assert we have 2 or more args
set optlist [lrange $args 0 end-2]
if {[llength $optlist] % 2 != 0} {
#options should come in pairs
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype "indices" ;#default
set base 0 ;#default
if {[llength $args] > 2} {
#if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
if {[llength $args] != 4} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set optname [lindex $args 0]
set optval [lindex $args 1]
set fulloptname [tcl::prefix::match -error "" -base $optname]
if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
dict for {opt val} $optlist {
set fulloptname [tcl::prefix::match -error "" {-base -return} $opt]
switch -exact -- $fulloptname {
-return {
set fullval [tcl::prefix::match -error "" {indices pairs} $val]
if {$fullval ni {indices pairs}} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set returntype $fullval
}
-base {
if {![string is integer -strict $val]} {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
set base $val
}
default {
set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
}
}
set base $optval
}
#set base 0 ;#default
#if {[llength $args] > 2} {
# #if more than just numitems and indexset - we expect only -base <int> ie 4 args in total
# if {[llength $args] != 4} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set optname [lindex $args 0]
# set optval [lindex $args 1]
# set fulloptname [tcl::prefix::match -error "" -base $optname]
# if {$fulloptname ne "-base" || ![string is integer -strict $optval]} {
# set errmsg [punk::args::usage -scheme error ::punk::lib::indexset_resolve]
# uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $errmsg]
# }
# set base $optval
#}
# --------------------------------------------------
@ -5104,8 +5208,84 @@ namespace eval punk::lib {
}
}
}
if {$returntype eq "pairs"} {
return [indices_to_pairs $index_list]
}
return $index_list
}
proc indices_to_pairs {indices} {
#convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices which can be increasing or decreasing.
#if the direction of the run changes - we end the previous run and start a new one
if {[llength $indices] == 0} {
return [list]
}
set pairs [list]
set start [lindex $indices 0]
set prev $start
set direction 0 ;#0 = unknown, 1 = increasing, -1 = decreasing
for {set i 1} {$i < [llength $indices]} {incr i} {
set idx [lindex $indices $i]
if {![string is integer -strict $idx]} {
error "non-integer index '$idx' in indices list"
}
if {$idx == $prev + 1} {
#increase since prev
if {$direction == 0} {
set direction 1
} elseif {$direction == -1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
} else {
#still increasing
}
set prev $idx
} elseif {$idx == $prev - 1} {
#decrease since prev
if {$direction == 0} {
set direction -1
} elseif {$direction == 1} {
#direction changed - end previous run and start new one
lappend pairs [list $start $prev]
set start $idx
set direction 0
}
set prev $idx
} else {
#run ended - add pair to list
lappend pairs [list $start $prev]
set start $idx
set prev $idx
set direction 0
}
}
# add final run
lappend pairs [list $start $prev]
return $pairs
}
#proc indices_to_pairs {indices} {
# #convert a list of indices to a list of pairs representing the start and end of contiguous runs of indices
# set pairs [list]
# set start [lindex $indices 0]
# set prev $start
# for {set i 1} {$i < [llength $indices]} {} {
# set idx [lindex $indices $i]
# if {$idx == $prev + 1} {
# #still in a run
# set prev $idx
# } else {
# #run ended - add pair to list
# lappend pairs [list $start $prev]
# set start $idx
# set prev $idx
# }
# }
# # add final run
# lappend pairs [list $start $prev]
# return $pairs
#}
# showdict uses lindex_resolve results -Inf & Inf to determine whether index is out of bounds on lower vs upper side
#This doesn't need the list itself - just the length suffices.
punk::args::define {
@ -7779,6 +7959,77 @@ namespace eval punk::lib {
}
}
#sugar
#exclusive end - more intuitive for some cases and more consistent with other languages
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::lib::FOR
@cmd -name punk::lib::FOR\
-summary\
"BASIC style integer 'for loop'"\
-help\
"Sugar syntax for a common looping pattern.
The loop variable takes on values from start to end (exclusive) in increments of step.
If step is not specified, it defaults to 1 or -1 depending on the relative values of start and end.
This is a common looping pattern that isn't directly supported by Tcl's built in control structures,
and this syntax is more concise for convenient interactive usage. For example:
FOR i 0 10 {puts $i}
will print the numbers 0 to 9.
This wrapper necessarily has some slight overhead compared to builtin Tcl for, foreach and while loops,
so may not be suitable for performance critical inner loops.
See also: https://wiki.tcl-lang.org/page/Simple+shorthand+%27for%27+loop
"
@values -min 3 -max 4
varname -type string -help "loop variable name"
start -type integer -help "initial value for loop variable"
end -type integer -help "end value for loop variable (exclusive)"
step -type integer -optional 1 -help "step value for loop variable (defaults to 1 or -1 depending on start and end values)"
script -type script -help "script to execute for each loop iteration"
}]
}
proc FOR { var args } {
switch -- [llength $args] {
3 {
# FOR x start end {}
lassign $args start end script
set step [ expr {$start > $end ? - 1 : 1} ]
}
4 {
# FOR x start end step {}
lassign $args start end step script
}
default {
error "FOR: wrong # args, should be: FOR varName startValue endValue ?stepValue? script"
}
}
if {![string is integer -strict $start] || ![string is integer -strict $end] || ![string is integer -strict $step]} {
error "FOR: start,end and step values must be integers"
}
upvar $var loopVar
set loopVar [expr {$start - $step}]
#to support 'continue' we have to increment the loopVar prior to the script evaluation, within the loop condition
if {$start < $end} {
if {$step <= 0} {
error "FOR: step value must be positive when start < end"
}
while {[incr loopVar $step] < $end} {
uplevel $script
}
} else {
if {$step >= 0} {
error "FOR: step value must be negative when start > end"
}
while {[incr loopVar $step] > $end} {
uplevel $script
}
}
}
#review - there are various type of uuid - we should use something consistent across platforms
#twapi is used on windows because it's about 5 times faster - but is this more important than consistency?
#twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway

32
src/modules/punk/mix/cli-999999.0a1.0.tm

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

16
src/modules/punk/mix/commandset/module-999999.0a1.0.tm

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

30
src/modules/punk/mix/util-999999.0a1.0.tm

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

38
src/modules/punk/nav/fs-999999.0a1.0.tm

@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
if {$linktarget_file_type eq "directory"} {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {
@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs {
link {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
set linktarget [file readlink $cdtarget]
if {$linktarget_file_type eq "directory"} {
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {

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

@ -2167,8 +2167,13 @@ y" {return quirkykeyscript}
puts stdout "leaving $target"
puts stdout "call $commandstring\x1b\[m"
puts stdout "result:"
puts stdout $result
if {$code == 0} {
puts stdout "result:"
puts stdout $result
} else {
puts stdout "error message:"
puts stdout $::errorInfo
}
puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset
set cmdtype [dict get $linedict $target cmdtype]
@ -2647,6 +2652,8 @@ y" {return quirkykeyscript}
upvar ::punk::ns::linedict linedict
set ::punk::ns::linedict [::tcl::dict::create]
set body_cache [tcl::dict::create]
set resolved_targets [list]
foreach tgt $targets {
set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]]
@ -4179,7 +4186,7 @@ y" {return quirkykeyscript}
#this *very odd* construct is to avoid using the namespace argument of apply. (handling of *weird/inadvisable* namespaces)
#we use an uplevel from within the apply which runs in the global namespace, (but called via nseval from within targetns) and a result var for 'info default' in the current punk::ns namespace.
nseval $targetns [list apply [list {procname argname} {
set has_default [uplevel 1 [list info default $procname $argname ::punk::ns::corp_defvar]]
set has_default [uplevel 1 [list ::info default $procname $argname ::punk::ns::corp_defvar]]
if {$has_default} {
set answer [dict create exists 1 default $::punk::ns::corp_defvar]
} else {
@ -5018,6 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
#puts "punk::ns::cmdtraverse punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -6618,18 +6626,19 @@ y" {return quirkykeyscript}
separately calling 'info args <proc>' 'info body <proc>'
etc.
The body may display with an additional
comment inserted to display information such as the
comment inserted above the proc line to display information such as the
namespace origin. Such a comment begins with #corp#.
Returns a list: proc <procname> <arglist> <body>
(as long as any syntax highlighter is written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
Returns a string: proc <procname> <arglist> <body>
If the output is to be used as a script to regenerate a
procedure, '-syntax none' should be used to avoid ANSI
colours, or the resulting arglist and body should be
run through 'ansistrip'.
(any syntax highlighter should be written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
"
@opts
#todo - make definition @dynamic - load highlighters as functions?
@ -6647,7 +6656,13 @@ y" {return quirkykeyscript}
"Whether to replace tabs in the body with spaces or a visible Unicode symbol."
-ranges -type indexset -default "0..end" -help\
"comma delimited set of line ranges.
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1."
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1.
For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body.
The special index 0 is used to specify the line before the first line of the body,
which is where the #corp# info comment is placed if it exists.
So the default range 0..end includes the info comment and all lines of the body.
Specifying -ranges 1..5,0 would include the info comment at the end of the output.
"
-syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\
-choicelabels {
none
@ -6690,11 +6705,6 @@ y" {return quirkykeyscript}
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 infoheader "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#we want to handle edge cases of commands such as "" or :x
#various builtins such as 'namespace which' won't work
@ -6753,13 +6763,29 @@ y" {return quirkykeyscript}
return [list alias {*}$alias]
}
}
if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {
append infoheader \n "${indent}#corp# namespace origin $origin"
}
if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
append infoheader \n
#--------------------------------------------------------------------------
if {[info exists ::auto_index($path)]} {
#set infoheader "${indent}#corp# auto_index $::auto_index($path)"
set infoheader "#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'"
#if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {}
if {$origin ne $targetcmd} {
#append infoheader "${indent}#corp# namespace origin $origin"
append infoheader "#corp# namespace origin $origin"
}
if {$infoheader ne "" && $syntax eq "basic"} {
set infoheader [ansiwrap green $infoheader]
}
#if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
# append infoheader \n
#}
#--------------------------------------------------------------------------
set body ""
#set bodytext [info body $origin]
#relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname
@ -6851,47 +6877,126 @@ y" {return quirkykeyscript}
}
}
if {$ranges ni {"0..end" ".." "0.."}} {
set lines [split $body \n]
set linecount [llength $lines]
set lines [split $body \n]
set linecount [llength $lines]
#------------------------------------------------------------------------------------------------
#When we resolve our 1-based indexset - the zero index used to specify the info comment is lost.
#we need to search for it manually and add it back in if it's in the specified ranges.
set rangelist [split $ranges ,]
set info_positions [list]
set lnum 0
foreach range $rangelist {
lassign [split $range ..] start _ end
set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range]
if {$start eq "0"} {
lappend info_positions $lnum
}
incr lnum [llength $r_indices]
#if {$end eq "0"} {
# #ignore
#}
}
#puts "info_positions: $info_positions"
#------------------------------------------------------------------------------------------------
set body ""
if {[lindex $info_positions 0] == 0 && $infoheader ne ""} {
append body "$infoheader" \n
}
if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} {
set w [string length $linecount]
set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges]
set body ""
set outputlines [llength $indices]
if {$do_ln} {
set n 0
foreach idx $indices {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
if {$idx == 1} {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}"
} else {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
} else {
foreach idx $indices {
append body [lindex $lines $idx-1] \n
if {$idx == 1} {
set ln1 "proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body [lindex $lines end] "\}"
} else {
append body [lindex $lines $idx-1] \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
}
#no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match
if {[string index $body end] eq "\n"} {
set body [string range $body 0 end-1]
}
} else {
#range was specified in a standard way to mean 'all lines'
set outputlines $linecount
if {$do_ln} {
set linebody ""
set n 0
set lines [split $body \n]
set linecount [llength $lines]
set w [string length $linecount]
foreach ln $lines {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
set n 2
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$lnc[format %${w}s $n]$lnr $ln"
incr n
append linebody "$lnc[format %${w}s $n]$lnr $ln" \n
}
set body [string range $linebody 0 end-1]
#set body $linebody
if {$linecount > 1} {
append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
} else {
set ln1 "proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$ln"
}
if {$linecount > 1} {
append linebody \n "[lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
}
}
if {$is_highlighted} {
#ansi colourised items in list format may not always have desired string representation (list escaping can occur)
#return as a string - which may not be a proper Tcl list!
return "proc $resolved {$argl} {\n$infoheader$body\n}"
} else {
list proc $resolved $argl $infoheader$body
#ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end?
if {[lindex $info_positions end] > 0} {
if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} {
append body "\n$infoheader"
}
}
return $body
}
@ -7086,6 +7191,7 @@ y" {return quirkykeyscript}
nstest eval {package require punk::ns}
set ns ""
if {![catch {nstest eval [list punk::ns::pkguse $pkg_unqualified]} errMsg]} {
#review
set script [string map [list %p% $pkg_unqualified] {dict get $::punk::ns::pkguse_package_to_namespace %p%}]
set ns [nstest eval $script]
} else {

23
src/modules/punk/packagepreference-999999.0a1.0.tm

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

206
src/modules/punk/repl-999999.0a1.0.tm

@ -165,11 +165,11 @@ namespace eval punk::repl {
variable frametype
set frametype ascii; #conservative default
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$testcharwidth == 1} {
set frametype light
}
}
#if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
# if {$testcharwidth == 1} {
# set frametype light
# }
#}
variable debug_repl 0
variable signal_control_c 0
@ -432,7 +432,7 @@ proc repl::start {inchan args} {
variable codethread
#review
if {$codethread eq ""} {
error "start - no codethread. call init first. (options -safe 0|1)"
error "start - no codethread. call init first. (options -type 0|1)"
}
variable commandstr
@ -478,7 +478,7 @@ proc repl::start {inchan args} {
#set ::punk::repl::codethread::running 1
#the interp in which commands such as d/ run
#we need to namespace eval for the -safe interp which may not have the packages loaded (or be able to) but still needs default values
#we need to namespace eval for the -type interp which may not have the packages loaded (or be able to) but still needs default values
#punk::repl::codethread::running is required whether safe or not.
interp eval code {
namespace eval ::punk::repl::codethread {}
@ -690,7 +690,7 @@ proc repl::reopen_stdinX {} {
}
#add to sliding buffer of last x chars emmitted to screen by repl
# add to sliding buffer of last x chars emmitted to screen by repl
#(we could maintain only one char - more kept merely for debug assistance)
#will not detect emissions from exec with stdout redirected and presumably some extensions etc
proc repl::screen_last_char_add {c what {why ""}} {
@ -1961,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
set is_vt52 0
}
variable codethread
variable loopinstance
incr loopinstance
@ -2091,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
set chunk "\b\x7f\b\x7f"
} elseif {$chunk eq "\x1c"} {
#ctrl-bslash
#This is commonly used in terminals as a 'harder' ctrl-c.
#try to brutally terminate process
#attempt to leave terminal in a reasonable state
mode line ;#may be aliased to ::repl::interphelpers::mode
after 250 {exit 42}
punk::console::mode line
#for now - exit with small delay for tidyup
after 1000 {exit 43}
return
} elseif {$chunk eq "\x1a"} {
#for now - exit with small delay for tidyup
#ctrl-z
#::punk::repl::handler_console_control "ctrl-z_via_rawloop"
if {[catch {punk::console::mode line}]} {
#REVIEW
interp eval code {punk::console::mode line}
#JMN
#set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}]
set iname $::punk::repl::codethread::replthread_interp
#only the highest level subshell has an interp name of empty string (lower levels are named 'code')
if {$iname eq ""} {
punk::console::mode line
}
after 1000 {exit 43}
after 250 [list thread::send $codethread [list interp eval code {quit 42}]]
return
}
@ -2118,8 +2123,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#if {$chunk eq "\x1b\[C"} {
#}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_off
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
$editbuf add_chunk $chunk
@ -2230,8 +2237,10 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
lappend input_chunks_waiting($inputchan) $waiting
}
}
#----------------------------------------------------------------------------------------------------------------------------------------------------------
punk::console::cursor_on
flush stdout
#----------------------------------------------------------------------------------------------------------------------------------------------------------
if {$editbuf_linenum_submitted == 0} {
#(there is no line 0 - lines start at 1)
@ -2259,9 +2268,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap1 POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error1 in repl_handler: $repl_error"
rputs stderr "error1 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "erroropts: $erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2397,6 +2406,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#set commandstr "set ::punk::repl::debug_repl"
set commandstr ""
}
if {$::punk::repl::debug_repl > 100} {
proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] {
set p %p%
@ -2416,12 +2428,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs debugreport $clearance$p[string map [list \n \n$p] $msg]
}]
set info ""
append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n"
append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n"
set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]]
append info "lastrunchunks\n"
append info "chunks: [llength $lastrunchunks]\n"
append info "namespace: $::punk::nav::ns::ns_current"
append info "chunks : [llength $lastrunchunks]\n"
#JMN
set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]]
append info "codethread namespace: $codethread_ns\n"
append info "stdinlines : [llength $stdinlines] lines\n"
foreach ln $stdinlines {
append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n"
}
append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n"
#append info "namespace: $::punk::nav::ns::ns_current"
debug_repl_emit $info
} else {
proc debug_repl_emit {msg} {return}
@ -2466,11 +2486,16 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
# lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]]
#}
variable codethread
variable codethread_cond
variable codethread_mutex
lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#JJJJ JMN test
#REVIEW
#lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}]
#-------------------------------
#thread::transfer $codethread stderr
#chan configure stdout -buffering none
@ -2895,8 +2920,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
if {[llength $waiting]} {
set c [lindex $waiting end]
} else {
#set c " "
set c \u240a
set c \u240a ;#unicode linefeed symbol.
}
doprompt ">$c "
}
@ -2925,9 +2949,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs stderr "trap POSIX '$e' eopts:'$eopts"
flush stderr
} on error {repl_error erropts} {
rputs stderr "error in repl_handler: $repl_error"
rputs stderr "error2 in repl_process_data: $repl_error"
rputs stderr "-------------"
rputs stderr "$::errorInfo"
rputs stderr "$erropts"
rputs stderr "-------------"
set stdinreader [chan event $inputchan readable]
if {![string length $stdinreader]} {
@ -2972,10 +2996,10 @@ namespace eval repl {
variable codethread_cond
variable codethread_mutex
set opts [list -force 0 -safe 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
set opts [list -force 0 -type 0 -safelog 0 -paths {} -callback_interp $default_callback_interp]
foreach {k v} $args {
switch -- $k {
-force - -safe - -safelog - -paths - -callback_interp {
-force - -type - -safelog - -paths - -callback_interp {
dict set opts $k $v
}
default {
@ -2984,7 +3008,7 @@ namespace eval repl {
}
}
set opt_force [dict get $opts -force]
set opt_safe [dict get $opts -safe]
set opt_repltype [dict get $opts -type]
set opt_safelog [dict get $opts -safelog]
if {$opt_safelog eq "0"} {
set opt_safelog ""
@ -3008,16 +3032,17 @@ namespace eval repl {
set codethread_mutex [thread::mutex create]
set scriptmap [list %args% [list $opts] \
%argv0% [list $::argv0] \
%argv% [list $::argv] \
%argc% [list $::argc] \
%replthread% [thread::id] \
%replthread_cond% $codethread_cond \
%replthread_interp% [list $opt_callback_interp] \
%tmlist% [list [tcl::tm::list]] \
%autopath% [list $::auto_path] \
%lib_epoch% [list $::punk::libunknown::epoch]\
set scriptmap [list %args% [list $opts] {*}{
} %argv0% [list $::argv0] {*}{
} %argv% [list $::argv] {*}{
} %argc% [list $::argc] {*}{
} %replthread% [thread::id] {*}{
} %replthread_cond% $codethread_cond {*}{
} %replthread_interp% [list $opt_callback_interp] {*}{
} %tmlist% [list [tcl::tm::list]] {*}{
} %autopath% [list $::auto_path] {*}{
} %lib_epoch% [list $::punk::libunknown::epoch] {*}{
}
]
#scriptmap applied at end to satisfy silly editor highlighting.
set init_script {
@ -3098,46 +3123,50 @@ namespace eval repl {
package require punk::args
#package require Thread
if {[catch {package require thread} errM]} {
puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
#if {[catch {package require thread} errM]} {
# puts stdout ">>repl::init initscript lib load fail on package require thread\n$errM"
if {[catch {package require Thread} errM2]} {
puts stdout ">>repl::init initscript lib load fail on package require Thread\n$errM2"
puts stdout ">>repl::init auto_path : $::auto_path"
puts stdout ">>repl::init tcl::tm::list: [tcl::tm::list]"
}
}
#}
namespace eval ::punk::repl::codethread {}
# -------------------------------------------------------------------------------------------------------------------------------------------------------
#-----
#review - icomm as a possible way to talk to thread outside of the code interp.
#thread::send msgs arrive at a specific interp based on initial setup - review for when/whether androwish thread enhancements are made to allow
#thread::send to caller defined interp targets (reference?)
#snit required for icomm
if {[catch {package require snit} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
}
if {[catch {package require punk::icomm} errM]} {
#puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
}
#if {[catch {package require snit} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---snit $errM"
#}
#if {[catch {package require punk::icomm} errM]} {
# #puts stdout "punk::repl::initscript: lib load fail ---icomm $errM"
#}
#-----
namespace eval ::punk::repl::codethread {}
#todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
catch {package require tcl::chan::fifo2}
if {[catch {
#first use can raise error being a version number e.g 0.1.0 - why?
lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
} errMsg]} {
puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
} else {
#experimental?
#puts stdout "transferring chan $replside to thread %replthread%"
#flush stdout
#if {[catch {
# #after 0 [list thread::transfer %replthread% $replside]
#} errMsg]} {
# #puts stdout "---thread::transfer error: $errMsg"
#}
}
#----------------------------------------------------------------------------------------------------------------------
# todo - review. According to fifo2 docs Memchan involves one less thread (may offer better performance/resource use)
# catch {package require tcl::chan::fifo2}
# if {[catch {
# #first use can raise error being a version number e.g 0.1.0 - why?
# lassign [tcl::chan::fifo2] ::punk::repl::codethread::repltalk replside
# } errMsg]} {
# puts stdout "punk::repl::initscript tcl::chan::fifo2 error: $errMsg"
# } else {
# #experimental?
# #puts stdout "transferring chan $replside to thread %replthread%"
# #flush stdout
# # if {[catch {
# # #after 0 [list thread::transfer %replthread% $replside]
# # } errMsg]} {
# # #puts stdout "---thread::transfer error: $errMsg"
# # }
# }
#----------------------------------------------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------------------------------------------------------------------
package require punk::console
package require punk::repl::codethread
@ -3318,7 +3347,7 @@ namespace eval repl {
set ts_start [clock seconds]
set replresult [interp eval code {
package require punk::repl
repl::init -safe punk
repl::init -type punk
repl::start stdin
}]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3328,7 +3357,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe safe {*}$args]
interp eval code [list repl::init -type safe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3338,7 +3367,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
set codethread [interp eval code [list repl::init -safe safebase {*}$args]]
set codethread [interp eval code [list repl::init -type safebase {*}$args]]
puts stdout "safebase codethread:$codethread"
set replresult [interp eval code [list repl::start stdin]]
@ -3349,7 +3378,7 @@ namespace eval repl {
interp eval code {
package require punk::repl
}
interp eval code [list repl::init -safe punksafe {*}$args]
interp eval code [list repl::init -type punksafe {*}$args]
set replresult [interp eval code [list repl::start stdin]]
return [list replresult $replresult elapsed [expr {[clock seconds]-$ts_start}]]
@ -3361,14 +3390,14 @@ namespace eval repl {
#flush stdout
set args %args%
set safe [dict get $args -safe]
set repltype [dict get $args -type]
set safelog [dict get $args -safelog]
set paths [list]
if {[dict exists $args -paths]} {
set paths [dict get $args -paths]
}
switch -- $safe {
switch -- $repltype {
safe {
interp create -safe -- code
code eval [list namespace eval ::punk::libunknown {}]
@ -3429,7 +3458,7 @@ namespace eval repl {
#todo a specific punk::libunknown 'package unknown' handler for safe interps
#pull in code via calls to source cached code?
switch -- $safe {
switch -- $repltype {
safe {
if {[llength $paths]} {
package require punk::island
@ -3782,8 +3811,27 @@ namespace eval repl {
}
punk - 0 {
#------------------------------------------------------------------------
#Test - experimental 2026
if {"stdout" in [chan names]} {
interp share {} stdout code
} else {
interp share {} [shellfilter::stack::item_tophandle stdout] code
}
if {"stderr" in [chan names]} {
interp share {} stderr code
} else {
interp share {} [shellfilter::stack::item_tophandle stderr] code
}
foreach nm {shellspyout shellspyerr} {
set thandle [shellfilter::stack::item_tophandle $nm]
if {$thandle ne ""} {
interp share {} $thandle code
}
}
#------------------------------------------------------------------------
interp eval code {
#safe !=1 and safe !=2, tmlist: %tmlist%
#repltype !=1 and repltype !=2, tmlist: %tmlist%
set ::argv0 %argv0%
set ::argv %argv%
set ::argc %argc%
@ -3963,8 +4011,8 @@ namespace eval repl {
thread::id
}
set init_script [string map $scriptmap $init_script]
#REVIEW - the same initscript sent for all values of $safe and it switches on values of $safe provided in %args%
#we already know $safe in this thread when generating the script - so why send the large script to the thread to then switch on that?
#REVIEW - the same initscript sent for all values of $repltype and it switches on values of $repltype provided in %args%
#we already know $repltype in this thread when generating the script - so why send the large script to the thread to then switch on that?
#thread::send $codethread $init_script
if {![catch {
@ -3979,7 +4027,7 @@ namespace eval repl {
error $errMsg
}
}
#init - don't auto init - require init with possible options e.g -safe
#init - don't auto init - require init with possible options e.g -type
}
package provide punk::repl [namespace eval punk::repl {
variable version

20
src/modules/punkcheck-0.1.0.tm

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

470
src/modules/shellfilter-999999.0a1.0.tm

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

2
src/modules/shellfilter-buildversion.txt

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

177
src/modules/shellthread-999999.0a1.0.tm

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

1
src/modules/test/punk/#modpod-ansi-999999.0a1.0/ansi-0.1.1_testsuites/ansi/ansimerge.test

@ -1,4 +1,3 @@
package require tcltest
namespace eval ::testspace {

42
src/modules/test/punk/#modpod-ns-999999.0a1.0/ns-0.1.0_testsuites/ns/corp.test

@ -87,4 +87,46 @@ namespace eval ::testspace {
-result [list\
1
]
test corp_single_line_function {Test that punk::ns::corp returns only a single line for a single line proc} {*}{
} -setup $common -body {
proc spud3 {} {return single-line function}
set body [punk::ns::corp -syntax none spud3]
lappend result [llength [split $body \n]]
} {*}{
} -cleanup {
rename spud3 ""
} {*}{
} -result [list {*}{
1
}]
test corp_linecount_match {Test that punk::ns::corp returns same number of lines} {*}{
} -setup $common -body {
#7 lines including proc line and closing brace, with 2 empty lines 2 comment lines
proc spud4 {a {b default}} {
#multiline function with some comments and empty lines
#test etc.
return spud4
}
set body [punk::ns::corp -syntax none spud4]
lappend result [llength [split $body \n]]
#now test that when restricted to 3 lines it returns 3 lines
#(no extra trailing newline allowed)
set body3 [punk::ns::corp -syntax none -ranges 1..3 spud4]
lappend result [llength [split $body3 \n]]
} {*}{
} -cleanup {
rename spud4 ""
} {*}{
} -result [list {*}{
7
3
}]
}

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

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

93
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.7.4.tm

@ -401,13 +401,20 @@ tcl::namespace::eval overtype {
set opt_console [tcl::dict::get $opts -console]
#--------------------------------------------------------------------------
#TODO
#REVIEW - punk::console package may not be loaded
set cursor_style_overtype {3 underline-blink}
set cursor_style_insert {5 beam-blink}
if {$opt_insert_mode} {
punk::console::cursor_style -console $opt_console $cursor_style_insert
set initial_cursor_style $cursor_style_insert
} else {
set initial_cursor_style $cursor_style_overtype
}
catch {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#--------------------------------------------------------------------------
# ----------------------------
# -experimental dev flag to set flags etc
@ -695,22 +702,23 @@ tcl::namespace::eval overtype {
#review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary -
#but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l
set renderopts [list -experimental $opt_experimental\
-cp437 $opt_cp437\
-info 1\
-crm_mode [tcl::dict::get $vtstate crm_mode]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-reverse_mode [tcl::dict::get $vtstate reverse_mode]\
-cursor_restore_attributes $cursor_saved_attributes\
-transparent $opt_transparent\
-width [tcl::dict::get $vtstate renderwidth]\
-exposed1 $opt_exposed1\
-exposed2 $opt_exposed2\
-expand_right $opt_expand_right\
-cursor_column $col\
-cursor_row $row\
-overtext_type $overtext_type\
set renderopts [list -experimental $opt_experimental {*}{
} -cp437 $opt_cp437 {*}{
} -info 1 {*}{
} -crm_mode [tcl::dict::get $vtstate crm_mode] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -reverse_mode [tcl::dict::get $vtstate reverse_mode] {*}{
} -cursor_restore_attributes $cursor_saved_attributes {*}{
} -transparent $opt_transparent {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -exposed1 $opt_exposed1 {*}{
} -exposed2 $opt_exposed2 {*}{
} -expand_right $opt_expand_right {*}{
} -cursor_column $col {*}{
} -cursor_row $row {*}{
} -overtext_type $overtext_type {*}{
}
]
set rinfo [renderline {*}$renderopts $undertext $overtext]
@ -940,14 +948,15 @@ tcl::namespace::eval overtype {
puts stdout ">>>renderspace<<<[a+ red bold]overflow_right during restore_cursor[a]"
set sub_info [overtype::renderline\
-info 1\
-width [tcl::dict::get $vtstate renderwidth]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-expand_right [tcl::dict::get $opts -expand_right]\
""\
$overflow_right\
set sub_info [overtype::renderline {*}{
} -info 1 {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -expand_right [tcl::dict::get $opts -expand_right] {*}{
} "" {*}{
} $overflow_right {*}{
}
]
set foldline [tcl::dict::get $sub_info result]
tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..?
@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype {
}
#JMN
if {[tcl::dict::get $vtstate insert_mode]} {
puts "setting cursor to insert style"
punk::console::cursor_style -console $opt_console $cursor_style_insert
} else {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#REVIEW - we don't want to emit cursor_style ANSI unless it changes.
#if {[tcl::dict::get $vtstate insert_mode]} {
# puts "setting cursor to insert style"
# punk::console::cursor_style -console $opt_console $cursor_style_insert
#} else {
# punk::console::cursor_style -console $opt_console $cursor_style_overtype
#}
#puts "renderedrow_max: $renderedrow_max"
#check for null lines below renderedrow_max (and at tail) and trim.
@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype {
#broken:
#todo - renderline -overflow is invalid.
# we need renderline to support -expand_left ??
set rinfo [renderline\
-info 1\
-insert_mode 0\
-transparent $opt_transparent\
-exposed1 $opt_exposed1 -exposed2 $opt_exposed2\
-overflow $opt_overflow\
-startcolumn [expr {1 + $startoffset}]\
$undertext $overtext]
set rinfo [renderline {*}{
} -info 1 {*}{
} -insert_mode 0 {*}{
} -transparent $opt_transparent {*}{
} -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 {*}{
} -overflow $opt_overflow {*}{
} -startcolumn [expr {1 + $startoffset}] {*}{
} $undertext $overtext {*}{
}
]
set replay_codes [tcl::dict::get $rinfo replay_codes]
set rendered [tcl::dict::get $rinfo result]
if {!$opt_overflow} {
@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype {
-crm_mode -default 0 -type boolean
-autowrap_mode -default 1 -type boolean
-reverse_mode -default 0 -type boolean
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\
-choicelabels\
{
1 "return a dict with raw fields"
2 "return a dict using ansistring VIEW"

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

@ -283,14 +283,6 @@ namespace eval punk {
#set path "[file dirname [info nameofexecutable]];.;"
set path "[file dirname [info nameofexecutable]];"
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
# ------------------------
#Note that unlike an ordinary Tcl array - the linked ::env behaves differently.
@ -307,6 +299,15 @@ namespace eval punk {
}
# ------------------------
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
#change2
if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
set lookfor [list $name]
@ -5492,9 +5493,10 @@ namespace eval punk {
#ctrl-c propagation also needs to be considered
set teehandle punksh
uplevel 1 [list ::catch \
[list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \
::tcl::UnknownResult ::tcl::UnknownOptions]
uplevel 1 [list ::catch {*}{
} [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{
} ::tcl::UnknownResult ::tcl::UnknownOptions
]
if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} {
dict set ::tcl::UnknownOptions -code error
@ -8428,19 +8430,44 @@ namespace eval punk {
set I [punk::ansi::a+ italic]
set NI [punk::ansi::a+ noitalic]
set sizedict [punk::console::get_size]
set cols [dict get $sizedict columns]
set rows [dict get $sizedict rows]
#todo - provide a mechanism to configure the default frametype everywhere and describe it in this help.
set frametype ascii ;#conservative default.
#if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all.
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$cols <= 80} {
# Be conservative with frame types on narrow terminals for help.
# an 80x30 terminal is more likely to be an older style terminal and may not have unicode support.
# unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme).
set frametype ascii
} else {
if {$testcharwidth == 1} {
set frametype light ;#unicode box-drawing chars.
}
}
}
# -------------------------------------------------------
set logoblock ""
if {[catch {
package require patternpunk
#lappend chunks [list stderr [>punk . rhs]]
append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
}]} {
append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
}
set title "[a+ brightgreen] Help System: "
set cmdinfo [list]
lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"]
set t [textblock::class::table new -minwidth 51 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8468,6 +8495,7 @@ namespace eval punk {
lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"]
lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8491,6 +8519,7 @@ namespace eval punk {
lappend cmdinfo [list "nn/" "" "go up one namespace"]
lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8513,6 +8542,7 @@ namespace eval punk {
lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"]
lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8537,6 +8567,7 @@ namespace eval punk {
lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8606,6 +8637,7 @@ namespace eval punk {
set usetable 1
if {$usetable} {
set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]]
$t configure -frametype $frametype
if {"windows" eq $::tcl_platform(platform)} {
#If any env vars have been set to empty string - this is considered a deletion of the variable on windows.
#The Tcl ::env array is linked to the underlying process view of the environment
@ -8634,6 +8666,7 @@ namespace eval punk {
$t destroy
set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]]
$t configure -frametype $frametype
foreach {v vinfo} $otherenv_config {
if {[info exists ::env($v)]} {
set env_val [set ::env($v)]
@ -8851,6 +8884,7 @@ namespace eval punk {
}]
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Topic"]
$t add_column
foreach {k v} $topics {

16
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm

@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi {
@cmd -name punk::ansi::sauce -summary\
"SAUCE info from file"\
-help\
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data."
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data.
Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format
that was commonly used in old ANSI art files to store information about the file, such as
title, author, group, date, and comments.
It may also have fields to specify the number of columns and rows in the ANSI art,
as well as flags for specific display attributes.
It may also be used on other types of files such as bitmap, vector, audio, binarytext,
xbin, archive and executable files.
It is a 128-byte block of data that is typically appended to the end of a file.
https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm"
-encoding -default iso8859-1 -type string -help\
"The default iso8859-1 is equivalent to binary ans should
work in the usual case.
@ -6044,6 +6056,7 @@ be as if this was off - ie lone CR.
#[para]These functions will emit the code - but read it in from stdin so that it doesn't display, and then return the row and column as a colon-delimited string or list respectively.
#[para]The punk::ansi::cursor_pos function is used by punk::console::get_cursor_pos and punk::console::get_cursor_pos_list
return \033\[6n
#same as 'tput u7' which just emits CSI 6n to stdout
}
proc cursor_pos_extended {} {
@ -6053,6 +6066,7 @@ be as if this was off - ie lone CR.
}
#DECFRA - Fill rectangular area
#REVIEW - vt100 accepts decimal values 132-126 and 160-255 ("in the current GL or GR in-use table")
#some modern terminals accept and display characters outside this range - but this needs investigation.

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

@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce {
variable PUNKARGS
variable PUNKARGS_aliases
#https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm
lappend PUNKARGS [list {
@id -id "(package)punk::ansi::sauce"
@package -name "punk::ansi::sauce" -help\

200
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm

@ -44,7 +44,14 @@
#[list_begin itemized]
package require Tcl 8.6-
#----------------------------------------------------
#Although we need to be in an environment with Thread available to use punk::console,
# we don't want to require Thread as a hard dependency in the interp we're running in.
# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp.
#TODO.
package require Thread ;#tsv required to sync is_raw
#----------------------------------------------------
package require punk::ansi
package require punk::args
#*** !doctools
@ -282,7 +289,7 @@ namespace eval punk::console {
ignore the regex match 'ok' response
and keep going."
-return -type string -default payload -choices {payload dict} -choicelabels {
dict\
dict
"dict with keys prefix,response,payload,all"
} -help\
"Return format"
@ -290,12 +297,12 @@ namespace eval punk::console {
-console -default {stdin stdout} -type list -help\
"console/terminal (currently list of in/out channels) (todo - object?)"
-passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels {
none\
none
{ ANSI sent without any passthrough wrapping.
A terminal multiplexer such as tmux,screen,zellij may
not pass the request through to the underlying terminal(s)
This is the recommended/normal value for the option.}
tmux\
tmux
{ Wrap ANSI sequence with tmux passthrough sequence.
\x1bPtmux\;<originalsequence_with_escapes_doubled>\x1b\\
Note that a tmux session could be connected to multiple
@ -304,7 +311,7 @@ namespace eval punk::console {
Passthrough should generally be avoided except for debug/test
purposes.
}
auto\
auto
{ Use existence of ::env(TMUX) to detect tmux and
send tmux passthrough sequence.
Not recommended except for debug/test purposes.
@ -1012,6 +1019,7 @@ namespace eval punk::console {
#e.g \033\[46;1R
set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload
#This is all 'tput u7' does (emits CSI 6n on stdout)
set request "\033\[6n"
set payload [punk::console::internal::get_ansi_response_payload -console $inoutchannels $request $capturingregex]
#some terminals fail to respond properly to \x1b\[6n but do respond to \x1b\[?6n and vice-versa :/
@ -1021,6 +1029,9 @@ namespace eval punk::console {
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\[\\)$}
@ -1295,7 +1306,7 @@ namespace eval punk::console {
default {set keyboard_name "unknown"}
}
return [dict create {*} {
return [dict create {*}{
} class $class_name {*}{
} version $version {*}{
} keyboard $keyboard_name {*}{
@ -1522,17 +1533,114 @@ namespace eval punk::console {
}
#todo - determine cursor on/off state before the call to restore properly.
variable get_size_mechanism
set get_size_mechanism [dict create]
proc get_size {{inoutchannels {stdin stdout}}} {
lassign $inoutchannels in out
#we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810
#chan eof is faster whether chan exists or not than
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size eof on output channel $out ([info level 1])"
set tried_mechlist [list]
#fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism
#unknown which platforms support this.
if {![catch {get_size_using_chanconfigure $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist "chanconfigure"
variable is_vt52
if {$is_vt52} {
#vt52 doesn't support cursor save/restore or cursor position reports.
if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist "tput"
error "can't get console size. Tried mechanisms: $mechlist"
}
variable get_size_mechanism ;#dict keyed on terminal ident. (currently just list of inoutchannels but may be something else in future such as terminal object or ident string)
if {![dict exists $get_size_mechanism $inoutchannels]} {
set try_order [list]
#call each mechanism once to see if it works - and to ensure we don't include initial run in our timings.
#we will also use the results for our initial return of the size.
set successful_mechs [list]
set sizedict [dict create]
if {![catch {get_size_using_cursorrestore $inoutchannels} result]} {
lappend successful_mechs "cursorrestore"
if {![dict size $sizedict]} {
set sizedict $result
}
}
if {![catch {get_size_using_cursormove $inoutchannels} result]} {
lappend successful_mechs "cursormove"
if {![dict size $sizedict]} {
set sizedict $result
}
}
if {![catch {get_size_using_tput $inoutchannels} result] } {
lappend successful_mechs "tput"
if {![dict size $sizedict]} {
set sizedict $result
}
}
set timings [list]
set t_ms 250 ;#default timerate is 1000ms - we are trying at least 3 mechanisms so 1000ms is a bit long for this test as it can slow down the first call to get_size significantly.
foreach sm $successful_mechs {
catch {
set timing_result [timerate {get_size_using_$sm $inoutchannels} $t_ms]
set micros [expr {int([lindex $timing_result 0])}]
lappend timings [list $micros $sm]
}
}
set sorted [lsort -integer -index 0 $timings]
if {[llength $sorted] > 0} {
set try_order [lmap t $sorted {lindex $t 1}]
} else {
set try_order [list]
}
dict set get_size_mechanism $inoutchannels $try_order
return $sizedict
}
foreach mech [dict get $get_size_mechanism $inoutchannels] {
if {![catch {get_size_using_$mech $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist $mech
}
#if {![catch {get_size_using_cursorrestore $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "cursorrestore"
#if {![catch {get_size_using_cursormove $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "cursormove"
#if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "tput"
error "can't get console size. Tried mechanisms: $tried_mechlist"
}
proc get_size_using_chanconfigure {{inoutchannels {stdin stdout}}} {
set out [lindex $inoutchannels 1]
set outconf [chan configure $out]
if {[dict exists $outconf -winsize]} {
#this mechanism is much faster than ansi cursor movements
#REVIEW check if any x-platform anomalies with this method?
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
error "chan configure method of getting console size not supported or failed to get valid size info"
#we don't need to care about the input channel if chan configure on the output can give us the info.
#short circuit ansi cursor movement method if chan configure supports the -winsize value
set outconf [chan configure $out]
@ -1542,43 +1650,45 @@ namespace eval punk::console {
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [list columns $cols rows $lines]
return [dict create columns $cols rows $lines]
}
#continue on to ansi mechanism if we didn't get 2 ints
}
if {[catch {chan eof $in} is_eof]} {
error "punk::console::get_size input channel $in seems to be closed ([info level 1])"
}
proc get_size_using_tput {{inoutchannels {stdin stdout}}} {
set tputcmd [auto_execok tput]
if {$tputcmd eq ""} {
error "tput command not found - cannot use tput method to get console size"
}
lassign [exec {*}$tputcmd lines cols] lines cols
return [dict create columns $cols rows $lines]
}
proc get_size_using_cursormove {{inoutchannels {stdin stdout}}} {
set out [lindex $inoutchannels 1]
#we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810
#chan eof is faster whether chan exists or not than
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size_using_cursormove output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size eof on input channel $in ([info level 1])"
error "punk::console::get_size_using_cursormove eof on output channel $out ([info level 1])"
}
}
#keep out of catch - no point in even trying a restore move if we can't get start position - just fail here.
#no vt52 equiv? may as well strip all vt52 from here?
lassign [get_cursor_pos_list $inoutchannels] start_row start_col
variable is_vt52
if {!$is_vt52} {
set movefunc "punk::ansi::move"
set func_coff "punk::ansi::cursor_off"
set func_con "punk::ansi::cursor_on"
} else {
set movefunc "punk::ansi::vt52move"
set func_coff "punk::ansi::vt52cursor_off"
set func_con "punk::ansi::vt52cursor_on"
}
if {[catch {
#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 $out [$func_coff][$movefunc 2000 2000]
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list $inoutchannels] lines cols
puts -nonewline $out [$movefunc $start_row $start_col][$func_con];flush stdout
set result [list columns $cols rows $lines]
puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::ansi::cursor_on];flush stdout
set result [dict create columns $cols rows $lines]
} errM]} {
puts -nonewline $out [$movefunc $start_row $start_col]
puts -nonewline $out [$func_con]
puts -nonewline $out [punk::ansi::move $start_row $start_col]
puts -nonewline $out [punk::ansi::cursor_on]
error "$errM"
} else {
return $result
@ -1586,14 +1696,16 @@ namespace eval punk::console {
}
#faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore
proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} {
proc get_size_using_cursorrestore {{inoutchannels {stdin stdout}}} {
lassign $inoutchannels in out
#we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly
set outconf [chan configure $out]
if {[dict exists $outconf -winsize]} {
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [list columns $cols rows $lines]
#don't use shortcut mechanisms - this function is intended to specificall use the cursor_save/restore method
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size_using_cursorrestore output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size_using_cursorrestore eof on output channel $out ([info level 1])"
}
}
@ -1603,7 +1715,7 @@ namespace eval punk::console {
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list $inoutchannels] lines cols
puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out
set result [list columns $cols rows $lines]
set result [dict create columns $cols rows $lines]
} errM]} {
puts -nonewline $out [punk::ansi::cursor_restore_dec]
puts -nonewline $out [punk::ansi::cursor_on]
@ -1612,10 +1724,15 @@ namespace eval punk::console {
return $result
}
}
proc get_dimensions {{inoutchannels {stdin stdout}}} {
lassign [get_size $inoutchannels] _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
@ -2669,6 +2786,7 @@ namespace eval punk::console {
This allows querying the current style and then re-setting it after temporarily changing it."
}]
}
proc cursor_style {args} {
set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style]
lassign [dict values $argd] leaders opts values

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

@ -1641,7 +1641,7 @@ namespace eval punk::du {
#convert time from windows (100ns units since jan 1, 1601) to Tcl time (seconds since Jan 1, 1970)
#We lose some precision by not passing the boolean to the large_system_time_to_secs_since_1970 function which returns fractional seconds
#but we need to maintain compatibility with other platforms and other tcl functions so if we want to return more precise times we will need another flag and/or result dict
dict set alltimes $fullname [dict create {*} {
dict set alltimes $fullname [dict create {*}{
} c [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo ctime]] {*}{
} a [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo atime]] {*}{
} m [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo mtime]] {*}{
@ -2114,15 +2114,15 @@ namespace eval punk::du {
}
proc du_dirlisting_tclvfs {folderpath args} {
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

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

@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check {
if {"windows" ne $::tcl_platform(platform)} {
set bug 0
} else {
set tmpdir [file tempdir] ;#tcl 9+
set testfile [file join $tmpdir "bugtest"]
if {![catch {file tempdir} tmpdir]} {
#tcl 9+ has 'file tempdir'
set testfile [file join $tmpdir "bugtest"]
} else {
#fallback for older tcl versions - use env TEMP/TMP or current directory
set tmpdir ""
foreach e {TEMP TMP} {
if {[info exists ::env($e)] && [file isdirectory ::env($e)]} {
set tmpdir ::env($e)
break
}
}
if {$tmpdir eq ""} {
#no env vars - fallback to current directory
set tmpdir [pwd]
}
set testfile [file join $tmpdir "bugtest"]
}
set fd [open $testfile w]
puts $fd test
close $fd
set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*]
if {[file exists $testfile]} {
file delete $testfile
}
foreach r $globresult {
if {$r ne "bugtest"} {
set bug 1
@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat {
#*** !doctools
#[call [fun lpop] [arg listvar] [opt {index}]]
#[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop
upvar $lvar l
#upvar $lvar l
upvar 1 $lvar l
if {![llength $args]} {
set args [list end]
}
@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat {
#set newlist [lremove $newlist $tailidx]
#set newlist [lreplace $newlist $tailidx $tailidx]
set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx]
#don't use ledit here!
#we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin.
} else {
set sublist [lindex $newlist {*}$sublist_path]
#set sublist [lremove $sublist $tailidx]
@ -459,26 +480,46 @@ tcl::namespace::eval punk::lib::compat {
}
}
set lidx [punk::lib::lindex_resolve [llength $l] $last]
switch -exact -- $lidx {
-Inf {
#index below lower bound
set post [lrange $l 0 end]
}
Inf {
#index above upper bound
set post [list]
}
default {
if {$lidx < $fidx} {
#from ledit man page:
#If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
set post [lrange $l $fidx end]
} else {
#set post [lrange $l $last+1 end]
if {$lidx < $fidx} {
#from ledit man page:
#If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
set post [lrange $l $fidx end]
} else {
#set post [lrange $l $last+1 end]
switch -exact -- $lidx {
-Inf {
#index below lower bound
set post [lrange $l 0 end]
}
Inf {
#index above upper bound
set post [list]
}
default {
set post [lrange $l $lidx+1 end]
}
}
}
#switch -exact -- $lidx {
# -Inf {
# #index below lower bound
# set post [lrange $l 0 end]
# }
# Inf {
# #index above upper bound
# set post [list]
# }
# default {
# if {$lidx < $fidx} {
# #from ledit man page:
# #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
# set post [lrange $l $fidx end]
# } else {
# #set post [lrange $l $last+1 end]
# set post [lrange $l $lidx+1 end]
# }
# }
#}
set l [list {*}$pre {*}$args {*}$post]
}
@ -2481,7 +2522,7 @@ namespace eval punk::lib {
#no parse tree - This is likely for an empty argument with expansion e.g {*}{ }
#This construct occurs when using {*} in place of line continuation for long lists or dicts, e.g
#dict create {*}{
# } key1 $dynamic {*} {
# } key1 $dynamic {*}{
# key2 value2
#}
#review - the 'empty' argument will still have an entry in cmdlineranges - as although 'empty' in terms of how it expands it may be whitespace across multiple lines.
@ -3447,32 +3488,53 @@ namespace eval punk::lib {
showdict {*}$opts $dvalue {*}$patterns
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
#set sep " [a+ Web-seagreen]=[a] "
variable has_punk_ansi
if {!$has_punk_ansi} {
set RST ""
set sep " = "
#set sep_mismatch " mismatch "
set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
set RST [punk::ansi::a]
set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
# set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
#} else {
# set RST [punk::ansi::a]
# #set sep " [a+ Web-seagreen]=[a] "
# set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
# #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
# #NOTE that \u2260 not suitable for non utf-8 terminals.
# set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
#}
#todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8.
# (safe detection methods for utf-8 support?)
#if colour is disabled we want to refresh this.
#therefore we use @dynamic
proc get_sep {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support
}
return $sep
}
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
proc get_sep_mismatch {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
#NOTE that \u2260 not suitable for non utf-8 terminals.
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] "
}
return $sep_mismatch
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::showdict
@cmd -name punk::lib::showdict -help "display dictionary keys and values"
#todo - table tableobject
@ -3482,10 +3544,8 @@ namespace eval punk::lib {
"Trim whitespace off rhs of each line.
This can help prevent a single long line that wraps in terminal from making
every line wrap due to long rhs padding."
-separator -default {%sep%} -help\
"Separator column between keys and values"
-separator_mismatch -default {%sep_mismatch%} -help\
"Separator to use when patterns mismatch"
-separator -default "${$DYN_SEP}" -help "Separator column between keys and values"
-separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch"
-roottype -default "dict" -help\
"list,dict,string"
-ansibase_keys -default "" -help\
@ -3504,7 +3564,23 @@ namespace eval punk::lib {
"dict or list value"
patterns -default "*" -type string -multiple 1 -help\
"key or key glob pattern"
}]]
}]
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
set RST [punk::ansi::a]
set argd [punk::args::parse $args withid ::punk::lib::showdict]
#for punk::lib - we want to reduce pkg dependencies.
# - so we won't even use the tcllib debug pkg here

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

@ -566,6 +566,7 @@ namespace eval punk::mix::cli {
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
set installer [punkcheck::installtrack new $installername $punkcheck_file]
#set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel
$installer set_source_target $srcdir $basedir
set event [$installer start_event $config]
# -- ---
@ -601,9 +602,9 @@ namespace eval punk::mix::cli {
}
#debug for issues on non-windows platforms.
if {$::tcl_platform(platform) ne "windows"} {
set is_interesting 1
}
#if {$::tcl_platform(platform) ne "windows"} {
# set is_interesting 1
#}
if {$is_interesting} {
puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath"
@ -662,6 +663,7 @@ namespace eval punk::mix::cli {
# -max_depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
$build_installer set_source_target $current_source_dir/$modpath $buildfolder
set build_event [$build_installer start_event $config]
# -- ---

38
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm

@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
if {$linktarget_file_type eq "directory"} {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {
@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs {
link {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
set linktarget [file readlink $cdtarget]
if {$linktarget_file_type eq "directory"} {
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {

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

@ -2167,8 +2167,13 @@ y" {return quirkykeyscript}
puts stdout "leaving $target"
puts stdout "call $commandstring\x1b\[m"
puts stdout "result:"
puts stdout $result
if {$code == 0} {
puts stdout "result:"
puts stdout $result
} else {
puts stdout "error message:"
puts stdout $::errorInfo
}
puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset
set cmdtype [dict get $linedict $target cmdtype]
@ -2647,6 +2652,8 @@ y" {return quirkykeyscript}
upvar ::punk::ns::linedict linedict
set ::punk::ns::linedict [::tcl::dict::create]
set body_cache [tcl::dict::create]
set resolved_targets [list]
foreach tgt $targets {
set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]]
@ -5018,6 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -6618,18 +6626,19 @@ y" {return quirkykeyscript}
separately calling 'info args <proc>' 'info body <proc>'
etc.
The body may display with an additional
comment inserted to display information such as the
comment inserted above the proc line to display information such as the
namespace origin. Such a comment begins with #corp#.
Returns a list: proc <procname> <arglist> <body>
(as long as any syntax highlighter is written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
Returns a string: proc <procname> <arglist> <body>
If the output is to be used as a script to regenerate a
procedure, '-syntax none' should be used to avoid ANSI
colours, or the resulting arglist and body should be
run through 'ansistrip'.
(any syntax highlighter should be written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
"
@opts
#todo - make definition @dynamic - load highlighters as functions?
@ -6647,7 +6656,13 @@ y" {return quirkykeyscript}
"Whether to replace tabs in the body with spaces or a visible Unicode symbol."
-ranges -type indexset -default "0..end" -help\
"comma delimited set of line ranges.
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1."
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1.
For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body.
The special index 0 is used to specify the line before the first line of the body,
which is where the #corp# info comment is placed if it exists.
So the default range 0..end includes the info comment and all lines of the body.
Specifying -ranges 1..5,0 would include the info comment at the end of the output.
"
-syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\
-choicelabels {
none
@ -6690,11 +6705,6 @@ y" {return quirkykeyscript}
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 infoheader "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#we want to handle edge cases of commands such as "" or :x
#various builtins such as 'namespace which' won't work
@ -6753,13 +6763,29 @@ y" {return quirkykeyscript}
return [list alias {*}$alias]
}
}
if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {
append infoheader \n "${indent}#corp# namespace origin $origin"
}
if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
append infoheader \n
#--------------------------------------------------------------------------
if {[info exists ::auto_index($path)]} {
#set infoheader "${indent}#corp# auto_index $::auto_index($path)"
set infoheader "#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'"
#if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {}
if {$origin ne $targetcmd} {
#append infoheader "${indent}#corp# namespace origin $origin"
append infoheader "#corp# namespace origin $origin"
}
if {$infoheader ne "" && $syntax eq "basic"} {
set infoheader [ansiwrap green $infoheader]
}
#if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
# append infoheader \n
#}
#--------------------------------------------------------------------------
set body ""
#set bodytext [info body $origin]
#relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname
@ -6851,47 +6877,126 @@ y" {return quirkykeyscript}
}
}
if {$ranges ni {"0..end" ".." "0.."}} {
set lines [split $body \n]
set linecount [llength $lines]
set lines [split $body \n]
set linecount [llength $lines]
#------------------------------------------------------------------------------------------------
#When we resolve our 1-based indexset - the zero index used to specify the info comment is lost.
#we need to search for it manually and add it back in if it's in the specified ranges.
set rangelist [split $ranges ,]
set info_positions [list]
set lnum 0
foreach range $rangelist {
lassign [split $range ..] start _ end
set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range]
if {$start eq "0"} {
lappend info_positions $lnum
}
incr lnum [llength $r_indices]
#if {$end eq "0"} {
# #ignore
#}
}
#puts "info_positions: $info_positions"
#------------------------------------------------------------------------------------------------
set body ""
if {[lindex $info_positions 0] == 0 && $infoheader ne ""} {
append body "$infoheader" \n
}
if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} {
set w [string length $linecount]
set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges]
set body ""
set outputlines [llength $indices]
if {$do_ln} {
set n 0
foreach idx $indices {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
if {$idx == 1} {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}"
} else {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
} else {
foreach idx $indices {
append body [lindex $lines $idx-1] \n
if {$idx == 1} {
set ln1 "proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body [lindex $lines end] "\}"
} else {
append body [lindex $lines $idx-1] \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
}
#no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match
if {[string index $body end] eq "\n"} {
set body [string range $body 0 end-1]
}
} else {
#range was specified in a standard way to mean 'all lines'
set outputlines $linecount
if {$do_ln} {
set linebody ""
set n 0
set lines [split $body \n]
set linecount [llength $lines]
set w [string length $linecount]
foreach ln $lines {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
set n 2
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$lnc[format %${w}s $n]$lnr $ln"
incr n
append linebody "$lnc[format %${w}s $n]$lnr $ln" \n
}
set body [string range $linebody 0 end-1]
#set body $linebody
if {$linecount > 1} {
append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
} else {
set ln1 "proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$ln"
}
if {$linecount > 1} {
append linebody \n "[lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
}
}
if {$is_highlighted} {
#ansi colourised items in list format may not always have desired string representation (list escaping can occur)
#return as a string - which may not be a proper Tcl list!
return "proc $resolved {$argl} {\n$infoheader$body\n}"
} else {
list proc $resolved $argl $infoheader$body
#ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end?
if {[lindex $info_positions end] > 0} {
if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} {
append body "\n$infoheader"
}
}
return $body
}

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

@ -21,6 +21,7 @@ unset stdin_info
# -----------------------------------
#-------------------------------------------------------------------------------------
if {[package provide punk::libunknown] eq ""} {
#maintenance - also in src/vfs/_config/punk_main.tcl
@ -162,6 +163,13 @@ namespace eval punk::repl {
#todo - key on shell/subshell
tsv::set repl runchunks-0 [list] ;#last_run_display
variable frametype
set frametype ascii; #conservative default
#if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
# if {$testcharwidth == 1} {
# set frametype light
# }
#}
variable debug_repl 0
variable signal_control_c 0
@ -991,6 +999,8 @@ namespace eval punk::repl::class {
dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered]
}
method add_chunk {chunk} {
upvar ::punk::repl::frametype frametype
#we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info
lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok
dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw]
@ -1065,7 +1075,7 @@ namespace eval punk::repl::class {
append debug \n [showdict $dinfo]
append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col"
package require textblock
set debug [textblock::frame -checkargs 0 -buildcache 0 $debug]
set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug]
if {![punk::console::vt52]} {
catch {punk::console::move_emitblock_return $debug_first_row 1 $debug}
} else {
@ -1148,7 +1158,7 @@ namespace eval punk::repl::class {
set debug "add_chunk$i"
append debug \n $mergedinfo
append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]"
set debug [textblock::frame -checkargs 0 -buildcache 0 $debug]
set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug]
#catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug}
set result [dict get $mergedinfo result]
@ -1816,6 +1826,8 @@ proc punk::repl::console_debugview {editbuf consolewidth args} {
}
package require textblock
variable debug_repl
variable frametype
if {$debug_repl <= 0} {
return [dict create width 0 height 0 topleft {}]
}
@ -1850,11 +1862,11 @@ proc punk::repl::console_debugview {editbuf consolewidth args} {
}
set debug_height [expr {[llength $lines]+2}] ;#framed height
} errM]} {
set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM]
set debug_height [textblock::height $info]
} else {
#treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0
set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info]
}
set debug_width [textblock::widthtopline $info]
@ -1881,6 +1893,8 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
package require textblock
upvar ::repl::editbuf_list editbuf_list
variable frametype
set defaults {-row 10 -rightmargin 0}
set opts [dict merge $defaults $args]
set opt_row [dict get $opts -row]
@ -1894,14 +1908,14 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
set info [punk::lib::list_as_lines $lines]
}
} editbuf_error]} {
set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"]
} else {
set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]"
append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]"
set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]"
set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]"
set info [a+ green bold]$row1\n$row2[a]\n$info
set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info]
}
set editbuf_width [textblock::widthtopline $info]
set spacepatch [textblock::block $editbuf_width 2 " "]
@ -1915,8 +1929,10 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
return [dict create width $editbuf_width]
}
proc punk::repl::console_controlnotification {message consolewidth consoleheight args} {
package require textblock
variable frametype
set defaults {-bottommargin 0 -rightmargin 0}
set opts [dict merge $defaults $args]
set opt_bottommargin [dict get $opts -bottommargin]
@ -1924,8 +1940,14 @@ proc punk::repl::console_controlnotification {message consolewidth consoleheight
set messagelines [split $message \n]
set message [lindex $messagelines 0] ;#only allow single line
set info "[a+ bold red]$message[a]"
set hlt [dict get [textblock::framedef light] hlt]
set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1]
if {$frametype eq "ascii"} {
set box [textblock::frame -type ascii -checkargs 0 -boxmap [list tlc + trc +] -title $message -height 1]
} else {
set hlt [dict get [textblock::framedef light] hlt]
set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1]
}
set notification_width [textblock::widthtopline $info]
set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}]
set row [expr {$consoleheight - $opt_bottommargin}]
@ -1939,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
set is_vt52 0
}
variable codethread
variable loopinstance
incr loopinstance
@ -2069,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
set chunk "\b\x7f\b\x7f"
} elseif {$chunk eq "\x1c"} {
#ctrl-bslash
#This is commonly used in terminals as a 'harder' ctrl-c.
#try to brutally terminate process
#attempt to leave terminal in a reasonable state
mode line ;#may be aliased to ::repl::interphelpers::mode
after 250 {exit 42}
punk::console::mode line
#for now - exit with small delay for tidyup
after 1000 {exit 43}
return
} elseif {$chunk eq "\x1a"} {
#for now - exit with small delay for tidyup
#ctrl-z
#::punk::repl::handler_console_control "ctrl-z_via_rawloop"
if {[catch {punk::console::mode line}]} {
#REVIEW
interp eval code {punk::console::mode line}
#JMN
#set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}]
set iname $::punk::repl::codethread::replthread_interp
#only the highest level subshell has an interp name of empty string (lower levels are named 'code')
if {$iname eq ""} {
punk::console::mode line
}
after 1000 {exit 43}
after 250 [list thread::send $codethread [list interp eval code {quit 42}]]
return
}
@ -2375,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#set commandstr "set ::punk::repl::debug_repl"
set commandstr ""
}
if {$::punk::repl::debug_repl > 100} {
proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] {
set p %p%
@ -2394,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs debugreport $clearance$p[string map [list \n \n$p] $msg]
}]
set info ""
append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n"
append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n"
set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]]
append info "lastrunchunks\n"
append info "chunks: [llength $lastrunchunks]\n"
append info "namespace: $::punk::nav::ns::ns_current"
append info "chunks : [llength $lastrunchunks]\n"
#JMN
set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]]
append info "codethread namespace: $codethread_ns\n"
append info "stdinlines : [llength $stdinlines] lines\n"
foreach ln $stdinlines {
append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n"
}
append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n"
#append info "namespace: $::punk::nav::ns::ns_current"
debug_repl_emit $info
} else {
proc debug_repl_emit {msg} {return}
@ -2444,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
# lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]]
#}
variable codethread
variable codethread_cond
variable codethread_mutex
@ -2873,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
if {[llength $waiting]} {
set c [lindex $waiting end]
} else {
#set c " "
set c \u240a
set c \u240a ;#unicode linefeed symbol.
}
doprompt ">$c "
}
@ -2986,16 +3022,17 @@ namespace eval repl {
set codethread_mutex [thread::mutex create]
set scriptmap [list %args% [list $opts] \
%argv0% [list $::argv0] \
%argv% [list $::argv] \
%argc% [list $::argc] \
%replthread% [thread::id] \
%replthread_cond% $codethread_cond \
%replthread_interp% [list $opt_callback_interp] \
%tmlist% [list [tcl::tm::list]] \
%autopath% [list $::auto_path] \
%lib_epoch% [list $::punk::libunknown::epoch]\
set scriptmap [list %args% [list $opts] {*}{
} %argv0% [list $::argv0] {*}{
} %argv% [list $::argv] {*}{
} %argc% [list $::argc] {*}{
} %replthread% [thread::id] {*}{
} %replthread_cond% $codethread_cond {*}{
} %replthread_interp% [list $opt_callback_interp] {*}{
} %tmlist% [list [tcl::tm::list]] {*}{
} %autopath% [list $::auto_path] {*}{
} %lib_epoch% [list $::punk::libunknown::epoch] {*}{
}
]
#scriptmap applied at end to satisfy silly editor highlighting.
set init_script {

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

@ -81,16 +81,18 @@ namespace eval punkcheck {
}
return $record_list
}
proc save_records_to_file {recordlist punkcheck_file {trigger {}}} {
if {$trigger ne ""} {
puts stderr "\x1b\[36mSaving [llength $recordlist] records to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m"
}
proc save_records_to_file {recordlist punkcheck_file {trigger {}} {debugchannel ""}} {
set newtdl [punk::tdl::prettyprint $recordlist]
set linecount [llength [split $newtdl \n]]
if {$debugchannel ne "" && $trigger ne ""} {
puts $debugchannel "\x1b\[36mSaving [llength $recordlist] records as $linecount lines to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m"
}
#puts stdout $newtdl
set fd [open $punkcheck_file w]
chan configure $fd -translation binary
puts -nonewline $fd $newtdl
flush $fd
close $fd
return [list recordcount [llength $recordlist] linecount $linecount]
}
@ -170,8 +172,10 @@ namespace eval punkcheck {
variable o_path_cksum_cache
variable o_fileset_record
variable o_installer ;#parent object
variable o_debugchannel
constructor {installer rel_sourceroot rel_targetroot args} {
set o_installer $installer
set o_debugchannel [$installer get_debugchannel]
set o_operation_start_ts ""
set o_path_cksum_cache [dict create]
set o_operation ""
@ -321,7 +325,7 @@ namespace eval punkcheck {
set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list]
set o_fileset_record [dict get $extractioninfo record]
set record_list [dict get $extractioninfo recordset]
set record_list [dict get $extractioninfo recordset] ;#if fileset wasn't present, same as original record_list, otherwise full recordset with the fileset record removed, ready for reinsertion.
set isnew [dict get $extractioninfo isnew]
set oldposition [dict get $extractioninfo oldposition]
unset extractioninfo
@ -530,7 +534,9 @@ namespace eval punkcheck {
variable o_record_list
variable o_active_event
variable o_events
constructor {installername punkcheck_file} {
variable o_debugchannel
constructor {installername punkcheck_file {debugchannel ""}} {
set o_debugchannel $debugchannel
set o_active_event ""
set o_name $installername
@ -539,6 +545,8 @@ namespace eval punkcheck {
set o_targetroot ""
set o_rel_sourceroot ""
set o_rel_targetroot ""
set o_record_list [list]
#todo - validate punkcheck file location further??
set punkcheck_folder [file dirname $o_checkfile]
if {![file isdirectory $punkcheck_folder]} {
@ -546,6 +554,55 @@ namespace eval punkcheck {
}
my load_all_records
if {![llength $o_record_list] && $o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[32mNo existing records found in punkcheck file '$o_checkfile' for installer '$installername'. Starting with empty record list.\x1b\[m"
} else {
#verify no duplicate installer records for this installer.
#JMN
set sanity_dict [dict create]
set insane ""
foreach rec $o_record_list {
if {[dict get $rec tag] eq "INSTALLER"} {
set name [dict get $rec -name]
if {[dict exists $sanity_dict $name]} {
#todo - warn - duplicate record for same targetlist - shouldn't happen as we should be using get_file_record to find existing records
if {$o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[31mpunkcheck installtrack - multiple INSTALLER records with same name '$name'\x1b\[m"
}
set insane "$name"
break
}
dict set sanity_dict $name {}
}
}
if {$insane ne ""} {
set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'."
append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file,"
append msg \n " or a previous installtrack instance that did not complete properly."
append msg \n " Do you want to DELETE the .punkcheck file?"
append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs."
append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls."
append msg \n " If not confirmed, an error will be raised - likely aborting the current operation."
append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'."
set answer [punk::lib::askuser $msg]
if {[string tolower $answer] ne "yes"} {
error "Failing due to sanity check failure. User did not confirm with 'yes'."
}
if {[file exists $o_checkfile] && [file isfile $o_checkfile]} {
file delete $o_checkfile
}
if {[file exists $o_checkfile]} {
error "Failed to delete punkcheck file '$o_checkfile' after sanity check failure. Please investigate and resolve the issue before proceeding."
}
set o_record_list [list]
} else {
if {$o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[32mSanity check passed: no duplicate INSTALLER records found for installer '$installername' in punkcheck file '$o_checkfile'.\x1b\[m"
}
}
unset sanity_dict
}
set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list]
set existing_header_posn [dict get $resultinfo position]
if {$existing_header_posn == -1} {
@ -580,6 +637,9 @@ namespace eval punkcheck {
method get_checkfile {} {
return $o_checkfile
}
method get_debugchannel {} {
return $o_debugchannel
}
#call set_source_target before calling start_event/end_event
#each event can have different source->target pairs - but may often have same, so set on installtrack as defaults. Only persisted in event records.
@ -2164,10 +2224,10 @@ namespace eval punkcheck {
return [dict create changed $changed unchanged $unchanged]
}
#assume only one for name - use first encountered
#assume only one for name - use first encountered?
proc get_installer_record {name record_list} {
set posn 0
set found_posn -1
set found_posns [list]
set record ""
#puts ">>>> checking [llength $record_list] punkcheck records"
foreach rec $record_list {
@ -2175,12 +2235,20 @@ namespace eval punkcheck {
if {[dict get $rec -name] eq $name} {
set found_posn $posn
set record $rec
break
lappend found_posns $posn
}
}
incr posn
}
return [list position $found_posn record $record]
if {[llength $found_posns] > 1} {
error "punkcheck::recordlist::get_installer_record - multiple installer records with name '$name' found at positions $found_posns"
} elseif {[llength $found_posns] == 0} {
return [list position -1 record ""]
} else {
#single record found
return [list position [lindex $found_posn 0] record $record]
}
}
proc new_installer_record {name args} {
@ -2374,7 +2442,8 @@ namespace eval punkcheck {
set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}]
} else {
#set recordset [lreplace $recordset[unset recordset] $existing_posn $existing_posn]
set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn]
#set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn]
ledit recordset $existing_posn $existing_posn
set isnew 0
set fileset_record [dict get $fetch_record_result record]
}

14
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm

@ -2728,13 +2728,13 @@ 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
switch -regexp -- $lastitem\
{^>[/[:alpha:]]+} {
set lastitem "> [string range $lastitem 1 end]"
}\
{^>>[/[:alpha:]]+} {
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}}

816
src/bootsupport/modules/shellfilter-0.2.tm → src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.2.tm

File diff suppressed because it is too large Load Diff

93
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/overtype-1.7.4.tm

@ -401,13 +401,20 @@ tcl::namespace::eval overtype {
set opt_console [tcl::dict::get $opts -console]
#--------------------------------------------------------------------------
#TODO
#REVIEW - punk::console package may not be loaded
set cursor_style_overtype {3 underline-blink}
set cursor_style_insert {5 beam-blink}
if {$opt_insert_mode} {
punk::console::cursor_style -console $opt_console $cursor_style_insert
set initial_cursor_style $cursor_style_insert
} else {
set initial_cursor_style $cursor_style_overtype
}
catch {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#--------------------------------------------------------------------------
# ----------------------------
# -experimental dev flag to set flags etc
@ -695,22 +702,23 @@ tcl::namespace::eval overtype {
#review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary -
#but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l
set renderopts [list -experimental $opt_experimental\
-cp437 $opt_cp437\
-info 1\
-crm_mode [tcl::dict::get $vtstate crm_mode]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-reverse_mode [tcl::dict::get $vtstate reverse_mode]\
-cursor_restore_attributes $cursor_saved_attributes\
-transparent $opt_transparent\
-width [tcl::dict::get $vtstate renderwidth]\
-exposed1 $opt_exposed1\
-exposed2 $opt_exposed2\
-expand_right $opt_expand_right\
-cursor_column $col\
-cursor_row $row\
-overtext_type $overtext_type\
set renderopts [list -experimental $opt_experimental {*}{
} -cp437 $opt_cp437 {*}{
} -info 1 {*}{
} -crm_mode [tcl::dict::get $vtstate crm_mode] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -reverse_mode [tcl::dict::get $vtstate reverse_mode] {*}{
} -cursor_restore_attributes $cursor_saved_attributes {*}{
} -transparent $opt_transparent {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -exposed1 $opt_exposed1 {*}{
} -exposed2 $opt_exposed2 {*}{
} -expand_right $opt_expand_right {*}{
} -cursor_column $col {*}{
} -cursor_row $row {*}{
} -overtext_type $overtext_type {*}{
}
]
set rinfo [renderline {*}$renderopts $undertext $overtext]
@ -940,14 +948,15 @@ tcl::namespace::eval overtype {
puts stdout ">>>renderspace<<<[a+ red bold]overflow_right during restore_cursor[a]"
set sub_info [overtype::renderline\
-info 1\
-width [tcl::dict::get $vtstate renderwidth]\
-insert_mode [tcl::dict::get $vtstate insert_mode]\
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
-expand_right [tcl::dict::get $opts -expand_right]\
""\
$overflow_right\
set sub_info [overtype::renderline {*}{
} -info 1 {*}{
} -width [tcl::dict::get $vtstate renderwidth] {*}{
} -insert_mode [tcl::dict::get $vtstate insert_mode] {*}{
} -autowrap_mode [tcl::dict::get $vtstate autowrap_mode] {*}{
} -expand_right [tcl::dict::get $opts -expand_right] {*}{
} "" {*}{
} $overflow_right {*}{
}
]
set foldline [tcl::dict::get $sub_info result]
tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..?
@ -1589,12 +1598,13 @@ tcl::namespace::eval overtype {
}
#JMN
if {[tcl::dict::get $vtstate insert_mode]} {
puts "setting cursor to insert style"
punk::console::cursor_style -console $opt_console $cursor_style_insert
} else {
punk::console::cursor_style -console $opt_console $cursor_style_overtype
}
#REVIEW - we don't want to emit cursor_style ANSI unless it changes.
#if {[tcl::dict::get $vtstate insert_mode]} {
# puts "setting cursor to insert style"
# punk::console::cursor_style -console $opt_console $cursor_style_insert
#} else {
# punk::console::cursor_style -console $opt_console $cursor_style_overtype
#}
#puts "renderedrow_max: $renderedrow_max"
#check for null lines below renderedrow_max (and at tail) and trim.
@ -1902,14 +1912,16 @@ tcl::namespace::eval overtype {
#broken:
#todo - renderline -overflow is invalid.
# we need renderline to support -expand_left ??
set rinfo [renderline\
-info 1\
-insert_mode 0\
-transparent $opt_transparent\
-exposed1 $opt_exposed1 -exposed2 $opt_exposed2\
-overflow $opt_overflow\
-startcolumn [expr {1 + $startoffset}]\
$undertext $overtext]
set rinfo [renderline {*}{
} -info 1 {*}{
} -insert_mode 0 {*}{
} -transparent $opt_transparent {*}{
} -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 {*}{
} -overflow $opt_overflow {*}{
} -startcolumn [expr {1 + $startoffset}] {*}{
} $undertext $overtext {*}{
}
]
set replay_codes [tcl::dict::get $rinfo replay_codes]
set rendered [tcl::dict::get $rinfo result]
if {!$opt_overflow} {
@ -2212,7 +2224,8 @@ tcl::namespace::eval overtype {
-crm_mode -default 0 -type boolean
-autowrap_mode -default 1 -type boolean
-reverse_mode -default 0 -type boolean
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0} -choicelabels\
-info -default 0 -type integer -choicecolumns 2 -choices {1 9 2 10 3 11 4 12 0}\
-choicelabels\
{
1 "return a dict with raw fields"
2 "return a dict using ansistring VIEW"

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

@ -283,14 +283,6 @@ namespace eval punk {
#set path "[file dirname [info nameofexecutable]];.;"
set path "[file dirname [info nameofexecutable]];"
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
# ------------------------
#Note that unlike an ordinary Tcl array - the linked ::env behaves differently.
@ -307,6 +299,15 @@ namespace eval punk {
}
# ------------------------
if {[info exists env(SystemRoot)]} {
set windir $env(SystemRoot)
} elseif {[info exists env(WINDIR)]} {
set windir $env(WINDIR)
}
if {[info exists windir]} {
append path "$windir/system32;$windir/system;$windir;"
}
#change2
if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
set lookfor [list $name]
@ -5492,9 +5493,10 @@ namespace eval punk {
#ctrl-c propagation also needs to be considered
set teehandle punksh
uplevel 1 [list ::catch \
[list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \
::tcl::UnknownResult ::tcl::UnknownOptions]
uplevel 1 [list ::catch {*}{
} [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] {*}{
} ::tcl::UnknownResult ::tcl::UnknownOptions
]
if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} {
dict set ::tcl::UnknownOptions -code error
@ -8428,19 +8430,44 @@ namespace eval punk {
set I [punk::ansi::a+ italic]
set NI [punk::ansi::a+ noitalic]
set sizedict [punk::console::get_size]
set cols [dict get $sizedict columns]
set rows [dict get $sizedict rows]
#todo - provide a mechanism to configure the default frametype everywhere and describe it in this help.
set frametype ascii ;#conservative default.
#if the test char width fails - it's likely we're on a very old terminal that doesn't support unicode at all.
if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
if {$cols <= 80} {
# Be conservative with frame types on narrow terminals for help.
# an 80x30 terminal is more likely to be an older style terminal and may not have unicode support.
# unicode on a non-unicode terminal is a bad experience - with the frame chars showing as garbage (e.g 3 chars per grapheme).
set frametype ascii
} else {
if {$testcharwidth == 1} {
set frametype light ;#unicode box-drawing chars.
}
}
}
# -------------------------------------------------------
set logoblock ""
if {[catch {
package require patternpunk
#lappend chunks [list stderr [>punk . rhs]]
append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -width 29 -checkargs 0 [>punk . banner -title "" -left Tcl -right [package provide Tcl]]]
}]} {
append logoblock [textblock::frame -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
append logoblock [textblock::frame -type $frametype -title "Punk Shell [package provide punk]" -subtitle "TCL [package provide Tcl]" -width 29 -height 10 -checkargs 0 ""]
}
set title "[a+ brightgreen] Help System: "
set cmdinfo [list]
lappend cmdinfo [list help "?${I}topic${NI}?" "This help.\nTo see available subitems type:\nhelp topics\n\nFor an unrecognised ${I}topic${NI}\nhelp will look for basic\ninfo for it as a command.\n"]
set t [textblock::class::table new -minwidth 51 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8468,6 +8495,7 @@ namespace eval punk {
lappend cmdinfo [list newdir "${I}subdir${NI}..." "make new dir or dirs and show status"]
lappend cmdinfo [list fcat "${I}file ?file?...${NI}" "cat file(s)"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8491,6 +8519,7 @@ namespace eval punk {
lappend cmdinfo [list "nn/" "" "go up one namespace"]
lappend cmdinfo [list "newns" "${I}ns${NI}" "make child namespace and switch to it"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8513,6 +8542,7 @@ namespace eval punk {
lappend cmdinfo [list eg "${I}cmd${NI} ?${I}subcommand${NI}...?" "Show example from manpage"]
lappend cmdinfo [list corp "${I}proc${NI}" "View proc body and arguments with basic highlighting"]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8537,6 +8567,7 @@ namespace eval punk {
lappend cmdinfo [list a "?${I}colourcode${NI}...?" "Return ANSI codes (with leading reset)\n e.g puts \"\[a+ purple\]purple\[a Green\]normal on green\[a\]\"\n [a+ purple]purple[a Green]normal on green[a] "]
set t [textblock::class::table new -minwidth 80 -show_seps 0]
$t configure -frametype $frametype
foreach row $cmdinfo {
$t add_row $row
}
@ -8606,6 +8637,7 @@ namespace eval punk {
set usetable 1
if {$usetable} {
set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]]
$t configure -frametype $frametype
if {"windows" eq $::tcl_platform(platform)} {
#If any env vars have been set to empty string - this is considered a deletion of the variable on windows.
#The Tcl ::env array is linked to the underlying process view of the environment
@ -8634,6 +8666,7 @@ namespace eval punk {
$t destroy
set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]]
$t configure -frametype $frametype
foreach {v vinfo} $otherenv_config {
if {[info exists ::env($v)]} {
set env_val [set ::env($v)]
@ -8851,6 +8884,7 @@ namespace eval punk {
}]
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Topic"]
$t add_column
foreach {k v} $topics {

16
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm

@ -596,7 +596,19 @@ tcl::namespace::eval punk::ansi {
@cmd -name punk::ansi::sauce -summary\
"SAUCE info from file"\
-help\
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data."
"Wrapper for punk::ansi::sauce::from_file to display SAUCE block data.
Standard Architecture for Universal Comment Extensions (SAUCE) is a metadata format
that was commonly used in old ANSI art files to store information about the file, such as
title, author, group, date, and comments.
It may also have fields to specify the number of columns and rows in the ANSI art,
as well as flags for specific display attributes.
It may also be used on other types of files such as bitmap, vector, audio, binarytext,
xbin, archive and executable files.
It is a 128-byte block of data that is typically appended to the end of a file.
https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm"
-encoding -default iso8859-1 -type string -help\
"The default iso8859-1 is equivalent to binary ans should
work in the usual case.
@ -6044,6 +6056,7 @@ be as if this was off - ie lone CR.
#[para]These functions will emit the code - but read it in from stdin so that it doesn't display, and then return the row and column as a colon-delimited string or list respectively.
#[para]The punk::ansi::cursor_pos function is used by punk::console::get_cursor_pos and punk::console::get_cursor_pos_list
return \033\[6n
#same as 'tput u7' which just emits CSI 6n to stdout
}
proc cursor_pos_extended {} {
@ -6053,6 +6066,7 @@ be as if this was off - ie lone CR.
}
#DECFRA - Fill rectangular area
#REVIEW - vt100 accepts decimal values 132-126 and 160-255 ("in the current GL or GR in-use table")
#some modern terminals accept and display characters outside this range - but this needs investigation.

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

@ -520,6 +520,7 @@ tcl::namespace::eval punk::ansi::sauce {
variable PUNKARGS
variable PUNKARGS_aliases
#https://web.archive.org/web/20260510043818/https://www.acid.org/info/sauce/sauce.htm
lappend PUNKARGS [list {
@id -id "(package)punk::ansi::sauce"
@package -name "punk::ansi::sauce" -help\

200
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm

@ -44,7 +44,14 @@
#[list_begin itemized]
package require Tcl 8.6-
#----------------------------------------------------
#Although we need to be in an environment with Thread available to use punk::console,
# we don't want to require Thread as a hard dependency in the interp we're running in.
# We should be able to provide wrappers such that thread features we need can be used via aliases into the current interp.
#TODO.
package require Thread ;#tsv required to sync is_raw
#----------------------------------------------------
package require punk::ansi
package require punk::args
#*** !doctools
@ -282,7 +289,7 @@ namespace eval punk::console {
ignore the regex match 'ok' response
and keep going."
-return -type string -default payload -choices {payload dict} -choicelabels {
dict\
dict
"dict with keys prefix,response,payload,all"
} -help\
"Return format"
@ -290,12 +297,12 @@ namespace eval punk::console {
-console -default {stdin stdout} -type list -help\
"console/terminal (currently list of in/out channels) (todo - object?)"
-passthrough -default "none" -choices {none tmux auto} -choicecolumns 1 -choicelabels {
none\
none
{ ANSI sent without any passthrough wrapping.
A terminal multiplexer such as tmux,screen,zellij may
not pass the request through to the underlying terminal(s)
This is the recommended/normal value for the option.}
tmux\
tmux
{ Wrap ANSI sequence with tmux passthrough sequence.
\x1bPtmux\;<originalsequence_with_escapes_doubled>\x1b\\
Note that a tmux session could be connected to multiple
@ -304,7 +311,7 @@ namespace eval punk::console {
Passthrough should generally be avoided except for debug/test
purposes.
}
auto\
auto
{ Use existence of ::env(TMUX) to detect tmux and
send tmux passthrough sequence.
Not recommended except for debug/test purposes.
@ -1012,6 +1019,7 @@ namespace eval punk::console {
#e.g \033\[46;1R
set capturingregex {(.*)(\x1b\[([0-9]+;[0-9]+)R)$} ;#must capture prefix,entire-response,response-payload
#This is all 'tput u7' does (emits CSI 6n on stdout)
set request "\033\[6n"
set payload [punk::console::internal::get_ansi_response_payload -console $inoutchannels $request $capturingregex]
#some terminals fail to respond properly to \x1b\[6n but do respond to \x1b\[?6n and vice-versa :/
@ -1021,6 +1029,9 @@ namespace eval punk::console {
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\[\\)$}
@ -1295,7 +1306,7 @@ namespace eval punk::console {
default {set keyboard_name "unknown"}
}
return [dict create {*} {
return [dict create {*}{
} class $class_name {*}{
} version $version {*}{
} keyboard $keyboard_name {*}{
@ -1522,17 +1533,114 @@ namespace eval punk::console {
}
#todo - determine cursor on/off state before the call to restore properly.
variable get_size_mechanism
set get_size_mechanism [dict create]
proc get_size {{inoutchannels {stdin stdout}}} {
lassign $inoutchannels in out
#we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810
#chan eof is faster whether chan exists or not than
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size eof on output channel $out ([info level 1])"
set tried_mechlist [list]
#fastest mechanism if available - use Tcl's inbuilt -winsize key from chan configure if available - this is much faster than any ANSI mechanism
#unknown which platforms support this.
if {![catch {get_size_using_chanconfigure $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist "chanconfigure"
variable is_vt52
if {$is_vt52} {
#vt52 doesn't support cursor save/restore or cursor position reports.
if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist "tput"
error "can't get console size. Tried mechanisms: $mechlist"
}
variable get_size_mechanism ;#dict keyed on terminal ident. (currently just list of inoutchannels but may be something else in future such as terminal object or ident string)
if {![dict exists $get_size_mechanism $inoutchannels]} {
set try_order [list]
#call each mechanism once to see if it works - and to ensure we don't include initial run in our timings.
#we will also use the results for our initial return of the size.
set successful_mechs [list]
set sizedict [dict create]
if {![catch {get_size_using_cursorrestore $inoutchannels} result]} {
lappend successful_mechs "cursorrestore"
if {![dict size $sizedict]} {
set sizedict $result
}
}
if {![catch {get_size_using_cursormove $inoutchannels} result]} {
lappend successful_mechs "cursormove"
if {![dict size $sizedict]} {
set sizedict $result
}
}
if {![catch {get_size_using_tput $inoutchannels} result] } {
lappend successful_mechs "tput"
if {![dict size $sizedict]} {
set sizedict $result
}
}
set timings [list]
set t_ms 250 ;#default timerate is 1000ms - we are trying at least 3 mechanisms so 1000ms is a bit long for this test as it can slow down the first call to get_size significantly.
foreach sm $successful_mechs {
catch {
set timing_result [timerate {get_size_using_$sm $inoutchannels} $t_ms]
set micros [expr {int([lindex $timing_result 0])}]
lappend timings [list $micros $sm]
}
}
set sorted [lsort -integer -index 0 $timings]
if {[llength $sorted] > 0} {
set try_order [lmap t $sorted {lindex $t 1}]
} else {
set try_order [list]
}
dict set get_size_mechanism $inoutchannels $try_order
return $sizedict
}
foreach mech [dict get $get_size_mechanism $inoutchannels] {
if {![catch {get_size_using_$mech $inoutchannels} sizedict]} {
return $sizedict
}
lappend tried_mechlist $mech
}
#if {![catch {get_size_using_cursorrestore $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "cursorrestore"
#if {![catch {get_size_using_cursormove $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "cursormove"
#if {![catch {get_size_using_tput $inoutchannels} sizedict]} {
# return $sizedict
#}
#lappend tried_mechlist "tput"
error "can't get console size. Tried mechanisms: $tried_mechlist"
}
proc get_size_using_chanconfigure {{inoutchannels {stdin stdout}}} {
set out [lindex $inoutchannels 1]
set outconf [chan configure $out]
if {[dict exists $outconf -winsize]} {
#this mechanism is much faster than ansi cursor movements
#REVIEW check if any x-platform anomalies with this method?
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [dict create columns $cols rows $lines]
}
}
error "chan configure method of getting console size not supported or failed to get valid size info"
#we don't need to care about the input channel if chan configure on the output can give us the info.
#short circuit ansi cursor movement method if chan configure supports the -winsize value
set outconf [chan configure $out]
@ -1542,43 +1650,45 @@ namespace eval punk::console {
#can -winsize key exist but contain erroneous info? We will check that we get 2 ints at least
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [list columns $cols rows $lines]
return [dict create columns $cols rows $lines]
}
#continue on to ansi mechanism if we didn't get 2 ints
}
if {[catch {chan eof $in} is_eof]} {
error "punk::console::get_size input channel $in seems to be closed ([info level 1])"
}
proc get_size_using_tput {{inoutchannels {stdin stdout}}} {
set tputcmd [auto_execok tput]
if {$tputcmd eq ""} {
error "tput command not found - cannot use tput method to get console size"
}
lassign [exec {*}$tputcmd lines cols] lines cols
return [dict create columns $cols rows $lines]
}
proc get_size_using_cursormove {{inoutchannels {stdin stdout}}} {
set out [lindex $inoutchannels 1]
#we can't reliably use [chan names] for stdin,stdout. There could be stacked channels and they may have a names such as file22fb27fe810
#chan eof is faster whether chan exists or not than
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size_using_cursormove output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size eof on input channel $in ([info level 1])"
error "punk::console::get_size_using_cursormove eof on output channel $out ([info level 1])"
}
}
#keep out of catch - no point in even trying a restore move if we can't get start position - just fail here.
#no vt52 equiv? may as well strip all vt52 from here?
lassign [get_cursor_pos_list $inoutchannels] start_row start_col
variable is_vt52
if {!$is_vt52} {
set movefunc "punk::ansi::move"
set func_coff "punk::ansi::cursor_off"
set func_con "punk::ansi::cursor_on"
} else {
set movefunc "punk::ansi::vt52move"
set func_coff "punk::ansi::vt52cursor_off"
set func_con "punk::ansi::vt52cursor_on"
}
if {[catch {
#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 $out [$func_coff][$movefunc 2000 2000]
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list $inoutchannels] lines cols
puts -nonewline $out [$movefunc $start_row $start_col][$func_con];flush stdout
set result [list columns $cols rows $lines]
puts -nonewline $out [punk::ansi::move $start_row $start_col][punk::ansi::cursor_on];flush stdout
set result [dict create columns $cols rows $lines]
} errM]} {
puts -nonewline $out [$movefunc $start_row $start_col]
puts -nonewline $out [$func_con]
puts -nonewline $out [punk::ansi::move $start_row $start_col]
puts -nonewline $out [punk::ansi::cursor_on]
error "$errM"
} else {
return $result
@ -1586,14 +1696,16 @@ namespace eval punk::console {
}
#faster than get_size when it is using ansi mechanism - but uses cursor_save - which we may want to avoid if calling during another operation which uses cursor save/restore
proc get_size_cursorrestore {{inoutchannels {stdin stdout}}} {
proc get_size_using_cursorrestore {{inoutchannels {stdin stdout}}} {
lassign $inoutchannels in out
#we use the same shortcircuit mechanism as get_size to avoid ansi at all if the output channel will give us the info directly
set outconf [chan configure $out]
if {[dict exists $outconf -winsize]} {
lassign [dict get $outconf -winsize] cols lines
if {[string is integer -strict $cols] && [string is integer -strict $lines]} {
return [list columns $cols rows $lines]
#don't use shortcut mechanisms - this function is intended to specificall use the cursor_save/restore method
if {[catch {chan eof $out} is_eof]} {
error "punk::console::get_size_using_cursorrestore output channel $out seems to be closed ([info level 1])"
} else {
if {$is_eof} {
error "punk::console::get_size_using_cursorrestore eof on output channel $out ([info level 1])"
}
}
@ -1603,7 +1715,7 @@ namespace eval punk::console {
puts -nonewline $out [punk::ansi::cursor_off][punk::ansi::cursor_save_dec][punk::ansi::move 2000 2000]
lassign [get_cursor_pos_list $inoutchannels] lines cols
puts -nonewline $out [punk::ansi::cursor_restore][punk::console::cursor_on];flush $out
set result [list columns $cols rows $lines]
set result [dict create columns $cols rows $lines]
} errM]} {
puts -nonewline $out [punk::ansi::cursor_restore_dec]
puts -nonewline $out [punk::ansi::cursor_on]
@ -1612,10 +1724,15 @@ namespace eval punk::console {
return $result
}
}
proc get_dimensions {{inoutchannels {stdin stdout}}} {
lassign [get_size $inoutchannels] _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
@ -2669,6 +2786,7 @@ namespace eval punk::console {
This allows querying the current style and then re-setting it after temporarily changing it."
}]
}
proc cursor_style {args} {
set argd [punk::args::parse $args -cache 1 withid ::punk::console::cursor_style]
lassign [dict values $argd] leaders opts values

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

@ -1641,7 +1641,7 @@ namespace eval punk::du {
#convert time from windows (100ns units since jan 1, 1601) to Tcl time (seconds since Jan 1, 1970)
#We lose some precision by not passing the boolean to the large_system_time_to_secs_since_1970 function which returns fractional seconds
#but we need to maintain compatibility with other platforms and other tcl functions so if we want to return more precise times we will need another flag and/or result dict
dict set alltimes $fullname [dict create {*} {
dict set alltimes $fullname [dict create {*}{
} c [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo ctime]] {*}{
} a [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo atime]] {*}{
} m [twapi::large_system_time_to_secs_since_1970 [dict get $iteminfo mtime]] {*}{
@ -2114,15 +2114,15 @@ namespace eval punk::du {
}
proc du_dirlisting_tclvfs {folderpath args} {
set defaults [dict
-glob *\
-filedebug 0\
-patterndebug 0\
-link_info 1\
-with_sizes 0\
-with_times 0\
-types {}\
]
set defaults [dict create {*}{
-glob *
-filedebug 0
-patterndebug 0
-link_info 1
-with_sizes 0
-with_times 0
-types {}
}]
set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
set opt_glob [dict get $opts -glob]

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

@ -138,12 +138,32 @@ tcl::namespace::eval punk::lib::check {
if {"windows" ne $::tcl_platform(platform)} {
set bug 0
} else {
set tmpdir [file tempdir] ;#tcl 9+
set testfile [file join $tmpdir "bugtest"]
if {![catch {file tempdir} tmpdir]} {
#tcl 9+ has 'file tempdir'
set testfile [file join $tmpdir "bugtest"]
} else {
#fallback for older tcl versions - use env TEMP/TMP or current directory
set tmpdir ""
foreach e {TEMP TMP} {
if {[info exists ::env($e)] && [file isdirectory ::env($e)]} {
set tmpdir ::env($e)
break
}
}
if {$tmpdir eq ""} {
#no env vars - fallback to current directory
set tmpdir [pwd]
}
set testfile [file join $tmpdir "bugtest"]
}
set fd [open $testfile w]
puts $fd test
close $fd
set globresult [glob -nocomplain -directory $tmpdir -types f -tail BUGTEST {BUGTES{T}} {[B]UGTEST} {\BUGTEST} BUGTES? BUGTEST*]
if {[file exists $testfile]} {
file delete $testfile
}
foreach r $globresult {
if {$r ne "bugtest"} {
set bug 1
@ -398,7 +418,8 @@ tcl::namespace::eval punk::lib::compat {
#*** !doctools
#[call [fun lpop] [arg listvar] [opt {index}]]
#[para] Forwards compatible lpop for versions 8.6 or less to support equivalent 8.7 lpop
upvar $lvar l
#upvar $lvar l
upvar 1 $lvar l
if {![llength $args]} {
set args [list end]
}
@ -422,7 +443,7 @@ tcl::namespace::eval punk::lib::compat {
#set newlist [lremove $newlist $tailidx]
#set newlist [lreplace $newlist $tailidx $tailidx]
set newlist [lreplace $newlist[set newlist {}] $tailidx $tailidx]
#don't use ledit here!
#we avoid use of ledit here because if lpop is running as compat - ledit may also not be available as a builtin.
} else {
set sublist [lindex $newlist {*}$sublist_path]
#set sublist [lremove $sublist $tailidx]
@ -459,26 +480,46 @@ tcl::namespace::eval punk::lib::compat {
}
}
set lidx [punk::lib::lindex_resolve [llength $l] $last]
switch -exact -- $lidx {
-Inf {
#index below lower bound
set post [lrange $l 0 end]
}
Inf {
#index above upper bound
set post [list]
}
default {
if {$lidx < $fidx} {
#from ledit man page:
#If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
set post [lrange $l $fidx end]
} else {
#set post [lrange $l $last+1 end]
if {$lidx < $fidx} {
#from ledit man page:
#If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
set post [lrange $l $fidx end]
} else {
#set post [lrange $l $last+1 end]
switch -exact -- $lidx {
-Inf {
#index below lower bound
set post [lrange $l 0 end]
}
Inf {
#index above upper bound
set post [list]
}
default {
set post [lrange $l $lidx+1 end]
}
}
}
#switch -exact -- $lidx {
# -Inf {
# #index below lower bound
# set post [lrange $l 0 end]
# }
# Inf {
# #index above upper bound
# set post [list]
# }
# default {
# if {$lidx < $fidx} {
# #from ledit man page:
# #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted.
# set post [lrange $l $fidx end]
# } else {
# #set post [lrange $l $last+1 end]
# set post [lrange $l $lidx+1 end]
# }
# }
#}
set l [list {*}$pre {*}$args {*}$post]
}
@ -2481,7 +2522,7 @@ namespace eval punk::lib {
#no parse tree - This is likely for an empty argument with expansion e.g {*}{ }
#This construct occurs when using {*} in place of line continuation for long lists or dicts, e.g
#dict create {*}{
# } key1 $dynamic {*} {
# } key1 $dynamic {*}{
# key2 value2
#}
#review - the 'empty' argument will still have an entry in cmdlineranges - as although 'empty' in terms of how it expands it may be whitespace across multiple lines.
@ -3447,32 +3488,53 @@ namespace eval punk::lib {
showdict {*}$opts $dvalue {*}$patterns
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
#set sep " [a+ Web-seagreen]=[a] "
variable has_punk_ansi
if {!$has_punk_ansi} {
set RST ""
set sep " = "
#set sep_mismatch " mismatch "
set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
set RST [punk::ansi::a]
set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
namespace eval argdoc {
variable PUNKARGS
upvar ::punk::lib::has_punk_ansi has_punk_ansi
#if {!$has_punk_ansi} {
# set RST ""
# set sep " = "
# set sep_ \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
#} else {
# set RST [punk::ansi::a]
# #set sep " [a+ Web-seagreen]=[a] "
# set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support
# #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
# #NOTE that \u2260 not suitable for non utf-8 terminals.
# set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST "
#}
#todo - consider ascii == and != instead of unicode when terminal doesn't support utf-8.
# (safe detection methods for utf-8 support?)
#if colour is disabled we want to refresh this.
#therefore we use @dynamic
proc get_sep {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep " = "
} else {
#set sep " [a+ Web-seagreen]=[a] "
set sep " [punk::ansi::a+ Green]=[punk::ansi::a] " ;#stick to basic default colours for wider terminal support
}
return $sep
}
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
proc get_sep_mismatch {} {
upvar ::punk::lib::has_punk_ansi has_punk_ansi
if {!$has_punk_ansi} {
set sep_mismatch \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol)
} else {
#set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST "
#NOTE that \u2260 not suitable for non utf-8 terminals.
set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260[punk::ansi::a] "
}
return $sep_mismatch
}
set DYN_SEP {${[get_sep]}}
set DYN_SEP_MISMATCH {${[get_sep_mismatch]}}
set argd [punk::args::parse $args withdef [string map [list %sep% $sep %sep_mismatch% $sep_mismatch] {
lappend PUNKARGS [list {
@dynamic
@id -id ::punk::lib::showdict
@cmd -name punk::lib::showdict -help "display dictionary keys and values"
#todo - table tableobject
@ -3482,10 +3544,8 @@ namespace eval punk::lib {
"Trim whitespace off rhs of each line.
This can help prevent a single long line that wraps in terminal from making
every line wrap due to long rhs padding."
-separator -default {%sep%} -help\
"Separator column between keys and values"
-separator_mismatch -default {%sep_mismatch%} -help\
"Separator to use when patterns mismatch"
-separator -default "${$DYN_SEP}" -help "Separator column between keys and values"
-separator_mismatch -default "${$DYN_SEP_MISMATCH}" -help "Separator to use when patterns mismatch"
-roottype -default "dict" -help\
"list,dict,string"
-ansibase_keys -default "" -help\
@ -3504,7 +3564,23 @@ namespace eval punk::lib {
"dict or list value"
patterns -default "*" -type string -multiple 1 -help\
"key or key glob pattern"
}]]
}]
}
#TODO - much.
#showdict needs to be able to show different branches which share a root path
#e.g show key a1/b* in its entirety along with a1/c* - (or even exact duplicates)
# - specify ansi colour per pattern so different branches can be highlighted?
# - ideally we want to be able to use all the dict & list patterns from the punk pipeline system eg @head @tail # (count) etc
# - The current version is incomplete but passably usable.
# - Copy proc and attempt rework so we can get back to this as a baseline for functionality
proc showdict {args} { ;# analogous to parray (except that it takes the dict as a value)
package require punk::pipe
#package require punk ;#we need pipeline pattern matching features
package require textblock
set RST [punk::ansi::a]
set argd [punk::args::parse $args withid ::punk::lib::showdict]
#for punk::lib - we want to reduce pkg dependencies.
# - so we won't even use the tcllib debug pkg here

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

@ -566,6 +566,7 @@ namespace eval punk::mix::cli {
#lassign [punkcheck::start_installer_event $punkcheck_file $installername $srcdir $basedir $config] _eventid punkcheck_eventid _recordset record_list
# -- ---
set installer [punkcheck::installtrack new $installername $punkcheck_file]
#set installer [punkcheck::installtrack new $installername $punkcheck_file stderr] ;#with debugchannel
$installer set_source_target $srcdir $basedir
set event [$installer start_event $config]
# -- ---
@ -601,9 +602,9 @@ namespace eval punk::mix::cli {
}
#debug for issues on non-windows platforms.
if {$::tcl_platform(platform) ne "windows"} {
set is_interesting 1
}
#if {$::tcl_platform(platform) ne "windows"} {
# set is_interesting 1
#}
if {$is_interesting} {
puts "build_modules_from_source_to_base >>> module $current_source_dir/$modpath"
@ -662,6 +663,7 @@ namespace eval punk::mix::cli {
# -max_depth -1 for no limit
set build_installername pods_in_$current_source_dir
set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck]
#set build_installer [punkcheck::installtrack new $build_installername $buildfolder/.punkcheck stderr] ;#with debugchannel
$build_installer set_source_target $current_source_dir/$modpath $buildfolder
set build_event [$build_installer start_event $config]
# -- ---

38
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm

@ -546,10 +546,21 @@ tcl::namespace::eval punk::nav::fs {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
if {$linktarget_file_type eq "directory"} {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {
@ -567,11 +578,22 @@ tcl::namespace::eval punk::nav::fs {
link {
file stat $cdtarget cdtargetinfo
set linktarget_file_type $cdtargetinfo(type)
set linktarget [file readlink $cdtarget]
if {$linktarget_file_type eq "directory"} {
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
if {[catch {file readlink $cdtarget} linktarget]} {
#if we can't read the link target - it may be a type of link Tcl doesn't understand, but the OS does.
#review - exact type of link?
#we can probably still cd to it - but the path will appear to be within the parent directory even though
#the actual target may be elsewhere on the filesystem.
#This may be the intention of such links anyway.
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
} else {
set linktarget [file readlink $cdtarget]
cd $linktarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
directory {

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

@ -2167,8 +2167,13 @@ y" {return quirkykeyscript}
puts stdout "leaving $target"
puts stdout "call $commandstring\x1b\[m"
puts stdout "result:"
puts stdout $result
if {$code == 0} {
puts stdout "result:"
puts stdout $result
} else {
puts stdout "error message:"
puts stdout $::errorInfo
}
puts stdout \x1b\[m ;#result may leave terminal with ansi SGR attributes in effect - emit a reset
set cmdtype [dict get $linedict $target cmdtype]
@ -2647,6 +2652,8 @@ y" {return quirkykeyscript}
upvar ::punk::ns::linedict linedict
set ::punk::ns::linedict [::tcl::dict::create]
set body_cache [tcl::dict::create]
set resolved_targets [list]
foreach tgt $targets {
set tgt_info [uplevel 1 [list ::punk::ns::cmdinfo {*}$tgt]]
@ -5018,6 +5025,7 @@ y" {return quirkykeyscript}
set queryargs [lrange $args $i end]
set resolvedargs [list]
set queryargs_untested $queryargs
puts "punk::args::id_exists $docid queryargs_untested: $queryargs"
} else {
#we cannot generate autodoc for any deeper (e.g ensemble/proc after undocumented parent)
#There is nothing to indicate the locations of subcommands - they could be anywhere.
@ -6618,18 +6626,19 @@ y" {return quirkykeyscript}
separately calling 'info args <proc>' 'info body <proc>'
etc.
The body may display with an additional
comment inserted to display information such as the
comment inserted above the proc line to display information such as the
namespace origin. Such a comment begins with #corp#.
Returns a list: proc <procname> <arglist> <body>
(as long as any syntax highlighter is written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
Returns a string: proc <procname> <arglist> <body>
If the output is to be used as a script to regenerate a
procedure, '-syntax none' should be used to avoid ANSI
colours, or the resulting arglist and body should be
run through 'ansistrip'.
(any syntax highlighter should be written to
avoid breaking the structure. e.g by avoiding the
insertion of ANSI between an escaping backslash and
its target character)
"
@opts
#todo - make definition @dynamic - load highlighters as functions?
@ -6647,7 +6656,13 @@ y" {return quirkykeyscript}
"Whether to replace tabs in the body with spaces or a visible Unicode symbol."
-ranges -type indexset -default "0..end" -help\
"comma delimited set of line ranges.
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1."
Restrict output to the specified line ranges of the body. Lines are numbered starting at 1.
For example, -ranges 1..5,10 would return lines 1 to 5 and line 10 of the body.
The special index 0 is used to specify the line before the first line of the body,
which is where the #corp# info comment is placed if it exists.
So the default range 0..end includes the info comment and all lines of the body.
Specifying -ranges 1..5,0 would include the info comment at the end of the output.
"
-syntax -type string -typesynopsis "none|basic" -default basic -choices {none basic}\
-choicelabels {
none
@ -6690,11 +6705,6 @@ y" {return quirkykeyscript}
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 infoheader "\n${indent}#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#we want to handle edge cases of commands such as "" or :x
#various builtins such as 'namespace which' won't work
@ -6753,13 +6763,29 @@ y" {return quirkykeyscript}
return [list alias {*}$alias]
}
}
if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {
append infoheader \n "${indent}#corp# namespace origin $origin"
}
if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
append infoheader \n
#--------------------------------------------------------------------------
if {[info exists ::auto_index($path)]} {
#set infoheader "${indent}#corp# auto_index $::auto_index($path)"
set infoheader "#corp# auto_index $::auto_index($path)"
} else {
set infoheader ""
}
#puts "targetcmd: '$targetcmd' iproc: '$iproc' origin: '$origin' resolved: '$resolved' targetns: '$targetns' name: '$name'"
#if {[nsprefix $targetcmd] ne [nsprefix [nsjoin ${targetns} $name]]} {}
if {$origin ne $targetcmd} {
#append infoheader "${indent}#corp# namespace origin $origin"
append infoheader "#corp# namespace origin $origin"
}
if {$infoheader ne "" && $syntax eq "basic"} {
set infoheader [ansiwrap green $infoheader]
}
#if {$infoheader ne "" && [string index $infoheader end] ne "\n"} {
# append infoheader \n
#}
#--------------------------------------------------------------------------
set body ""
#set bodytext [info body $origin]
#relevant test test::punk::ns SUITE ns corp.test corp_leadingcolon_functionname
@ -6851,47 +6877,126 @@ y" {return quirkykeyscript}
}
}
if {$ranges ni {"0..end" ".." "0.."}} {
set lines [split $body \n]
set linecount [llength $lines]
set lines [split $body \n]
set linecount [llength $lines]
#------------------------------------------------------------------------------------------------
#When we resolve our 1-based indexset - the zero index used to specify the info comment is lost.
#we need to search for it manually and add it back in if it's in the specified ranges.
set rangelist [split $ranges ,]
set info_positions [list]
set lnum 0
foreach range $rangelist {
lassign [split $range ..] start _ end
set r_indices [punk::lib::indexset_resolve -base 1 $linecount $range]
if {$start eq "0"} {
lappend info_positions $lnum
}
incr lnum [llength $r_indices]
#if {$end eq "0"} {
# #ignore
#}
}
#puts "info_positions: $info_positions"
#------------------------------------------------------------------------------------------------
set body ""
if {[lindex $info_positions 0] == 0 && $infoheader ne ""} {
append body "$infoheader" \n
}
if {$ranges ni [list "0..end" "1..end" ".." "0.." "1.." "..end"]} {
set w [string length $linecount]
set indices [punk::lib::indexset_resolve -base 1 $linecount $ranges]
set body ""
set outputlines [llength $indices]
if {$do_ln} {
set n 0
foreach idx $indices {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
if {$idx == 1} {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines end]" "\}"
} else {
append body "$lnc[format %${w}s $idx]$lnr [lindex $lines $idx-1]" \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
} else {
foreach idx $indices {
append body [lindex $lines $idx-1] \n
if {$idx == 1} {
set ln1 "proc $resolved [list $argl] \{"
append body "$ln1[lindex $lines 0]"
if {$linecount == 1} {
append body "\}"
} else {
append body "\n"
}
} elseif {$idx == $linecount} {
append body [lindex $lines end] "\}"
} else {
append body [lindex $lines $idx-1] \n
}
if {[set p [lsearch $info_positions $idx]] >= 0} {
append body $infoheader \n
set info_positions [lremove $info_positions $p]
}
}
}
#no superfluous trailing newline allowed. see test::punk::ns test: corp_linecount_match
if {[string index $body end] eq "\n"} {
set body [string range $body 0 end-1]
}
} else {
#range was specified in a standard way to mean 'all lines'
set outputlines $linecount
if {$do_ln} {
set linebody ""
set n 0
set lines [split $body \n]
set linecount [llength $lines]
set w [string length $linecount]
foreach ln $lines {
set ln1 "$lnc[format %${w}s 1]$lnr proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
set n 2
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$lnc[format %${w}s $n]$lnr $ln"
incr n
append linebody "$lnc[format %${w}s $n]$lnr $ln" \n
}
set body [string range $linebody 0 end-1]
#set body $linebody
if {$linecount > 1} {
append linebody \n "$lnc[format %${w}s $linecount]$lnr [lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
} else {
set ln1 "proc $resolved [list $argl] \{"
set linebody "$ln1[lindex $lines 0]"
foreach ln [lrange $lines 1 end-1] {
append linebody \n "$ln"
}
if {$linecount > 1} {
append linebody \n "[lindex $lines end]\}"
} else {
append linebody "\}"
}
append body $linebody
}
}
if {$is_highlighted} {
#ansi colourised items in list format may not always have desired string representation (list escaping can occur)
#return as a string - which may not be a proper Tcl list!
return "proc $resolved {$argl} {\n$infoheader$body\n}"
} else {
list proc $resolved $argl $infoheader$body
#ignore info header if it is in between for now? what is the usecase for it to display other than at the beginning or the end?
if {[lindex $info_positions end] > 0} {
if {[lindex $info_positions end] >= $outputlines && $infoheader ne ""} {
append body "\n$infoheader"
}
}
return $body
}

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

@ -21,6 +21,7 @@ unset stdin_info
# -----------------------------------
#-------------------------------------------------------------------------------------
if {[package provide punk::libunknown] eq ""} {
#maintenance - also in src/vfs/_config/punk_main.tcl
@ -162,6 +163,13 @@ namespace eval punk::repl {
#todo - key on shell/subshell
tsv::set repl runchunks-0 [list] ;#last_run_display
variable frametype
set frametype ascii; #conservative default
#if {![catch {punk::console::test_char_width \u00e9} testcharwidth]} {
# if {$testcharwidth == 1} {
# set frametype light
# }
#}
variable debug_repl 0
variable signal_control_c 0
@ -991,6 +999,8 @@ namespace eval punk::repl::class {
dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered]
}
method add_chunk {chunk} {
upvar ::punk::repl::frametype frametype
#we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info
lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok
dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw]
@ -1065,7 +1075,7 @@ namespace eval punk::repl::class {
append debug \n [showdict $dinfo]
append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col"
package require textblock
set debug [textblock::frame -checkargs 0 -buildcache 0 $debug]
set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug]
if {![punk::console::vt52]} {
catch {punk::console::move_emitblock_return $debug_first_row 1 $debug}
} else {
@ -1148,7 +1158,7 @@ namespace eval punk::repl::class {
set debug "add_chunk$i"
append debug \n $mergedinfo
append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]"
set debug [textblock::frame -checkargs 0 -buildcache 0 $debug]
set debug [textblock::frame -type $frametype -checkargs 0 -buildcache 0 $debug]
#catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug}
set result [dict get $mergedinfo result]
@ -1816,6 +1826,8 @@ proc punk::repl::console_debugview {editbuf consolewidth args} {
}
package require textblock
variable debug_repl
variable frametype
if {$debug_repl <= 0} {
return [dict create width 0 height 0 topleft {}]
}
@ -1850,11 +1862,11 @@ proc punk::repl::console_debugview {editbuf consolewidth args} {
}
set debug_height [expr {[llength $lines]+2}] ;#framed height
} errM]} {
set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error$RST" $errM]
set debug_height [textblock::height $info]
} else {
#treat as ephemeral (unreusable) frames due to varying width & height - therefore set -buildcache 0
set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ bold green] -title "[a cyan]debugview_raw$RST" $info]
}
set debug_width [textblock::widthtopline $info]
@ -1881,6 +1893,8 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
package require textblock
upvar ::repl::editbuf_list editbuf_list
variable frametype
set defaults {-row 10 -rightmargin 0}
set opts [dict merge $defaults $args]
set opt_row [dict get $opts -row]
@ -1894,14 +1908,14 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
set info [punk::lib::list_as_lines $lines]
}
} editbuf_error]} {
set info [textblock::frame -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"]
} else {
set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]"
append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]"
set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]"
set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]"
set info [a+ green bold]$row1\n$row2[a]\n$info
set info [textblock::frame -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info]
set info [textblock::frame -type $frametype -checkargs 0 -buildcache 0 -ansiborder [a+ green bold] -title $title $info]
}
set editbuf_width [textblock::widthtopline $info]
set spacepatch [textblock::block $editbuf_width 2 " "]
@ -1915,8 +1929,10 @@ proc punk::repl::console_editbufview {editbuf consolewidth args} {
return [dict create width $editbuf_width]
}
proc punk::repl::console_controlnotification {message consolewidth consoleheight args} {
package require textblock
variable frametype
set defaults {-bottommargin 0 -rightmargin 0}
set opts [dict merge $defaults $args]
set opt_bottommargin [dict get $opts -bottommargin]
@ -1924,8 +1940,14 @@ proc punk::repl::console_controlnotification {message consolewidth consoleheight
set messagelines [split $message \n]
set message [lindex $messagelines 0] ;#only allow single line
set info "[a+ bold red]$message[a]"
set hlt [dict get [textblock::framedef light] hlt]
set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1]
if {$frametype eq "ascii"} {
set box [textblock::frame -type ascii -checkargs 0 -boxmap [list tlc + trc +] -title $message -height 1]
} else {
set hlt [dict get [textblock::framedef light] hlt]
set box [textblock::frame -checkargs 0 -boxmap [list tlc $hlt trc $hlt] -title $message -height 1]
}
set notification_width [textblock::widthtopline $info]
set box_offset [expr {$consolewidth - $notification_width - $opt_rightmargin}]
set row [expr {$consoleheight - $opt_bottommargin}]
@ -1939,6 +1961,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
set is_vt52 0
}
variable codethread
variable loopinstance
incr loopinstance
@ -2069,20 +2092,24 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
set chunk "\b\x7f\b\x7f"
} elseif {$chunk eq "\x1c"} {
#ctrl-bslash
#This is commonly used in terminals as a 'harder' ctrl-c.
#try to brutally terminate process
#attempt to leave terminal in a reasonable state
mode line ;#may be aliased to ::repl::interphelpers::mode
after 250 {exit 42}
punk::console::mode line
#for now - exit with small delay for tidyup
after 1000 {exit 43}
return
} elseif {$chunk eq "\x1a"} {
#for now - exit with small delay for tidyup
#ctrl-z
#::punk::repl::handler_console_control "ctrl-z_via_rawloop"
if {[catch {punk::console::mode line}]} {
#REVIEW
interp eval code {punk::console::mode line}
#JMN
#set iname [thread::send $tid {set ::punk::repl::codethread::replthread_interp}]
set iname $::punk::repl::codethread::replthread_interp
#only the highest level subshell has an interp name of empty string (lower levels are named 'code')
if {$iname eq ""} {
punk::console::mode line
}
after 1000 {exit 43}
after 250 [list thread::send $codethread [list interp eval code {quit 42}]]
return
}
@ -2375,6 +2402,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
#set commandstr "set ::punk::repl::debug_repl"
set commandstr ""
}
if {$::punk::repl::debug_repl > 100} {
proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] {
set p %p%
@ -2394,12 +2424,20 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
rputs debugreport $clearance$p[string map [list \n \n$p] $msg]
}]
set info ""
append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n"
append info "repl loopinstance : $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n"
append info "commandstr : [punk::ansi::ansistring::VIEW $commandstr]\n"
set lastrunchunks [tsv::get repl runchunks-[tsv::get repl runid]]
append info "lastrunchunks\n"
append info "chunks: [llength $lastrunchunks]\n"
append info "namespace: $::punk::nav::ns::ns_current"
append info "chunks : [llength $lastrunchunks]\n"
#JMN
set codethread_ns [thread::send $codethread [list interp eval code [list set ::punk::nav::ns::ns_current]]]
append info "codethread namespace: $codethread_ns\n"
append info "stdinlines : [llength $stdinlines] lines\n"
foreach ln $stdinlines {
append info " line: [punk::ansi::ansistring::VIEW -lf 1 $ln]\n"
}
append info "chunk : [punk::ansi::ansistring::VIEW $chunk]\n"
#append info "namespace: $::punk::nav::ns::ns_current"
debug_repl_emit $info
} else {
proc debug_repl_emit {msg} {return}
@ -2444,7 +2482,6 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
# lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]]
#}
variable codethread
variable codethread_cond
variable codethread_mutex
@ -2873,8 +2910,7 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
if {[llength $waiting]} {
set c [lindex $waiting end]
} else {
#set c " "
set c \u240a
set c \u240a ;#unicode linefeed symbol.
}
doprompt ">$c "
}
@ -2986,16 +3022,17 @@ namespace eval repl {
set codethread_mutex [thread::mutex create]
set scriptmap [list %args% [list $opts] \
%argv0% [list $::argv0] \
%argv% [list $::argv] \
%argc% [list $::argc] \
%replthread% [thread::id] \
%replthread_cond% $codethread_cond \
%replthread_interp% [list $opt_callback_interp] \
%tmlist% [list [tcl::tm::list]] \
%autopath% [list $::auto_path] \
%lib_epoch% [list $::punk::libunknown::epoch]\
set scriptmap [list %args% [list $opts] {*}{
} %argv0% [list $::argv0] {*}{
} %argv% [list $::argv] {*}{
} %argc% [list $::argc] {*}{
} %replthread% [thread::id] {*}{
} %replthread_cond% $codethread_cond {*}{
} %replthread_interp% [list $opt_callback_interp] {*}{
} %tmlist% [list [tcl::tm::list]] {*}{
} %autopath% [list $::auto_path] {*}{
} %lib_epoch% [list $::punk::libunknown::epoch] {*}{
}
]
#scriptmap applied at end to satisfy silly editor highlighting.
set init_script {

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

@ -81,16 +81,18 @@ namespace eval punkcheck {
}
return $record_list
}
proc save_records_to_file {recordlist punkcheck_file {trigger {}}} {
if {$trigger ne ""} {
puts stderr "\x1b\[36mSaving [llength $recordlist] records to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m"
}
proc save_records_to_file {recordlist punkcheck_file {trigger {}} {debugchannel ""}} {
set newtdl [punk::tdl::prettyprint $recordlist]
set linecount [llength [split $newtdl \n]]
if {$debugchannel ne "" && $trigger ne ""} {
puts $debugchannel "\x1b\[36mSaving [llength $recordlist] records as $linecount lines to file '$punkcheck_file' trigger: \x1b\[32m$trigger\x1b\[m"
}
#puts stdout $newtdl
set fd [open $punkcheck_file w]
chan configure $fd -translation binary
puts -nonewline $fd $newtdl
flush $fd
close $fd
return [list recordcount [llength $recordlist] linecount $linecount]
}
@ -170,8 +172,10 @@ namespace eval punkcheck {
variable o_path_cksum_cache
variable o_fileset_record
variable o_installer ;#parent object
variable o_debugchannel
constructor {installer rel_sourceroot rel_targetroot args} {
set o_installer $installer
set o_debugchannel [$installer get_debugchannel]
set o_operation_start_ts ""
set o_path_cksum_cache [dict create]
set o_operation ""
@ -321,7 +325,7 @@ namespace eval punkcheck {
set extractioninfo [punkcheck::recordlist::extract_or_create_fileset_record $o_targets $record_list]
set o_fileset_record [dict get $extractioninfo record]
set record_list [dict get $extractioninfo recordset]
set record_list [dict get $extractioninfo recordset] ;#if fileset wasn't present, same as original record_list, otherwise full recordset with the fileset record removed, ready for reinsertion.
set isnew [dict get $extractioninfo isnew]
set oldposition [dict get $extractioninfo oldposition]
unset extractioninfo
@ -530,7 +534,9 @@ namespace eval punkcheck {
variable o_record_list
variable o_active_event
variable o_events
constructor {installername punkcheck_file} {
variable o_debugchannel
constructor {installername punkcheck_file {debugchannel ""}} {
set o_debugchannel $debugchannel
set o_active_event ""
set o_name $installername
@ -539,6 +545,8 @@ namespace eval punkcheck {
set o_targetroot ""
set o_rel_sourceroot ""
set o_rel_targetroot ""
set o_record_list [list]
#todo - validate punkcheck file location further??
set punkcheck_folder [file dirname $o_checkfile]
if {![file isdirectory $punkcheck_folder]} {
@ -546,6 +554,55 @@ namespace eval punkcheck {
}
my load_all_records
if {![llength $o_record_list] && $o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[32mNo existing records found in punkcheck file '$o_checkfile' for installer '$installername'. Starting with empty record list.\x1b\[m"
} else {
#verify no duplicate installer records for this installer.
#JMN
set sanity_dict [dict create]
set insane ""
foreach rec $o_record_list {
if {[dict get $rec tag] eq "INSTALLER"} {
set name [dict get $rec -name]
if {[dict exists $sanity_dict $name]} {
#todo - warn - duplicate record for same targetlist - shouldn't happen as we should be using get_file_record to find existing records
if {$o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[31mpunkcheck installtrack - multiple INSTALLER records with same name '$name'\x1b\[m"
}
set insane "$name"
break
}
dict set sanity_dict $name {}
}
}
if {$insane ne ""} {
set msg "Sanity check: punkcheck file '$o_checkfile' contains multiple records for INSTALLER -name '$insane'."
append msg \n "This may indicate a problem such as multiple concurrent installtrack instances using the same punkcheck file,"
append msg \n " or a previous installtrack instance that did not complete properly."
append msg \n " Do you want to DELETE the .punkcheck file?"
append msg \n " It is safe to delete .punkcheck files, at the cost of loss of history and checksums used to optimize installs."
append msg \n " They are a record of installation events and checksums used to avoid unnecessary reinstalls."
append msg \n " If not confirmed, an error will be raised - likely aborting the current operation."
append msg \n "confirm deletion and continue by regenerating the file, by typing the 3 letters: 'yes'."
set answer [punk::lib::askuser $msg]
if {[string tolower $answer] ne "yes"} {
error "Failing due to sanity check failure. User did not confirm with 'yes'."
}
if {[file exists $o_checkfile] && [file isfile $o_checkfile]} {
file delete $o_checkfile
}
if {[file exists $o_checkfile]} {
error "Failed to delete punkcheck file '$o_checkfile' after sanity check failure. Please investigate and resolve the issue before proceeding."
}
set o_record_list [list]
} else {
if {$o_debugchannel ne ""} {
puts $o_debugchannel "\x1b\[32mSanity check passed: no duplicate INSTALLER records found for installer '$installername' in punkcheck file '$o_checkfile'.\x1b\[m"
}
}
unset sanity_dict
}
set resultinfo [punkcheck::recordlist::get_installer_record $o_name $o_record_list]
set existing_header_posn [dict get $resultinfo position]
if {$existing_header_posn == -1} {
@ -580,6 +637,9 @@ namespace eval punkcheck {
method get_checkfile {} {
return $o_checkfile
}
method get_debugchannel {} {
return $o_debugchannel
}
#call set_source_target before calling start_event/end_event
#each event can have different source->target pairs - but may often have same, so set on installtrack as defaults. Only persisted in event records.
@ -2164,10 +2224,10 @@ namespace eval punkcheck {
return [dict create changed $changed unchanged $unchanged]
}
#assume only one for name - use first encountered
#assume only one for name - use first encountered?
proc get_installer_record {name record_list} {
set posn 0
set found_posn -1
set found_posns [list]
set record ""
#puts ">>>> checking [llength $record_list] punkcheck records"
foreach rec $record_list {
@ -2175,12 +2235,20 @@ namespace eval punkcheck {
if {[dict get $rec -name] eq $name} {
set found_posn $posn
set record $rec
break
lappend found_posns $posn
}
}
incr posn
}
return [list position $found_posn record $record]
if {[llength $found_posns] > 1} {
error "punkcheck::recordlist::get_installer_record - multiple installer records with name '$name' found at positions $found_posns"
} elseif {[llength $found_posns] == 0} {
return [list position -1 record ""]
} else {
#single record found
return [list position [lindex $found_posn 0] record $record]
}
}
proc new_installer_record {name args} {
@ -2374,7 +2442,8 @@ namespace eval punkcheck {
set fileset_record [dict create tag FILEINFO -targets $relative_target_paths body {}]
} else {
#set recordset [lreplace $recordset[unset recordset] $existing_posn $existing_posn]
set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn]
#set recordset [lreplace $recordset[set recordset {}] $existing_posn $existing_posn]
ledit recordset $existing_posn $existing_posn
set isnew 0
set fileset_record [dict get $fetch_record_result record]
}

14
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.1.tm

@ -2728,13 +2728,13 @@ 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
switch -regexp -- $lastitem\
{^>[/[:alpha:]]+} {
set lastitem "> [string range $lastitem 1 end]"
}\
{^>>[/[:alpha:]]+} {
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}}

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

File diff suppressed because it is too large Load Diff

8
src/runtime/mapvfs.config

@ -17,7 +17,13 @@
tclkit86bi.exe {punk8win.vfs punkbi kit}
#tclkit-win64-dyn.exe {punk86bawt.vfs punkbawt kit}
tclkit-win64-dyn.exe {punk86bawt.vfs punksys kit}
#------------------------------------------------------------------------
#broken 'lreplace' - but runtime beforehand is ok - thread library issue?
#tclkit-win64-dyn.exe {punk86bawt.vfs punksys kit}
#------------------------------------------------------------------------
#same kit with different .vfs is ok
tclkit-win64-dyn.exe {punk8win.vfs punksys kit}
#tclkit87a5.exe {punk86.vfs punk87} {punk.vfs punkmain}

BIN
src/vendorlib_tcl8/win32-x86_64/Memchan2.3/Memchan23.dll

Binary file not shown.

BIN
src/vendorlib_tcl8/win32-x86_64/Memchan2.3/libMemchanstub23.a

Binary file not shown.

2
src/vendorlib_tcl8/win32-x86_64/Memchan2.3/pkgIndex.tcl

@ -0,0 +1,2 @@
package ifneeded Memchan 2.3 \
[list load [file join $dir Memchan23.dll]]

67
src/vendormodules/include_modules.config

@ -1,41 +1,42 @@
#todo - change to include_modules.toml
#aim is to be programatically editable whilst retaining comments
set local_modules [list\
c:/repo/jn/tclmodules/fauxlink/modules fauxlink\
c:/repo/jn/tclmodules/gridplus/modules gridplus\
c:/repo/jn/tclmodules/modpod/modules modpod\
c:/repo/jn/tclmodules/packageTest/modules packagetest\
c:/repo/jn/tclmodules/tablelist/modules tablelist\
c:/repo/jn/tclmodules/tablelist/modules tablelist_tile\
c:/repo/jn/tclmodules/tomlish/modules tomlish\
c:/repo/jn/tclmodules/tomlish/modules test::tomlish\
c:/repo/jn/tclmodules/dictn/modules dictn\
c:/repo/jn/tclmodules/dollarcent/modules dollarcent\
c:/repo/jn/tclmodules/pattern/modules pattern\
c:/repo/jn/tclmodules/pattern/modules pattern2\
c:/repo/jn/tclmodules/pattern/modules patterncmd\
c:/repo/jn/tclmodules/pattern/modules patternlib\
c:/repo/jn/tclmodules/pattern/modules patterncipher\
c:/repo/jn/tclmodules/pattern/modules metaface\
c:/repo/jn/tclmodules/pattern/modules patternpredator1\
c:/repo/jn/tclmodules/pattern/modules patternpredator2\
c:/repo/jn/tclmodules/pattern/modules patterndispatcher\
c:/repo/jn/tclmodules/pattern/modules treeobj\
c:/repo/jn/tclmodules/pattern/modules pattern::ms\
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternBuilder\
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternInterface\
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternSystem\
c:/repo/jn/tclmodules/pattern/modules test::pattern\
c:/repo/jn/tclmodules/voo/modules voo\
c:/repo/jn/tarjar/modules tarjar\
]
set local_modules [list {*}{
c:/repo/jn/tclmodules/fauxlink/modules fauxlink
c:/repo/jn/tclmodules/gridplus/modules gridplus
c:/repo/jn/tclmodules/modpod/modules modpod
c:/repo/jn/tclmodules/packageTest/modules packagetest
c:/repo/jn/tclmodules/tablelist/modules tablelist
c:/repo/jn/tclmodules/tablelist/modules tablelist_tile
c:/repo/jn/tclmodules/tomlish/modules tomlish
c:/repo/jn/tclmodules/tomlish/modules test::tomlish
c:/repo/jn/tclmodules/dictn/modules dictn
c:/repo/jn/tclmodules/dollarcent/modules dollarcent
c:/repo/jn/tclmodules/pattern/modules pattern
c:/repo/jn/tclmodules/pattern/modules pattern2
c:/repo/jn/tclmodules/pattern/modules patterncmd
c:/repo/jn/tclmodules/pattern/modules patternlib
c:/repo/jn/tclmodules/pattern/modules patterncipher
c:/repo/jn/tclmodules/pattern/modules metaface
c:/repo/jn/tclmodules/pattern/modules patternpredator1
c:/repo/jn/tclmodules/pattern/modules patternpredator2
c:/repo/jn/tclmodules/pattern/modules patterndispatcher
c:/repo/jn/tclmodules/pattern/modules treeobj
c:/repo/jn/tclmodules/pattern/modules pattern::ms
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternBuilder
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternInterface
c:/repo/jn/tclmodules/pattern/modules pattern::IPatternSystem
c:/repo/jn/tclmodules/pattern/modules test::pattern
c:/repo/jn/tclmodules/voo/modules voo
c:/repo/jn/tarjar/modules tarjar
c:/repo/jn/tclmodules/sqids-tcl/modules sqids
}]
#moved overtype into punkshell project
# c:/repo/jn/tclmodules/overtype/modules overtype
set fossil_modules [dict create\
]
set fossil_modules [dict create {*}{
}]
set git_modules [dict create\
]
set git_modules [dict create {*}{
}]

931
src/vendormodules/sqids-0.3.1.tm

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

BIN
src/vendormodules_tcl9/Thread-3.0b3.tm

Binary file not shown.

BIN
src/vendormodules_tcl9/Thread/platform/win32_x86_64_tcl9-3.0b3.tm

Binary file not shown.

113
src/vfs/_config/punk_main.tcl

@ -23,6 +23,7 @@
# - and restrict package paths to those coming from a vfs (if not launched with 'dev' or 'os' first arg which allows external paths to remain)
apply { args {
set ::punkargv $args
set tclmajorv [lindex [split [info tclversion] .] 0]
namespace eval ::punkboot {
#This is somewhat ugly - but we don't want to do any 'package require' operations at this stage
@ -336,6 +337,7 @@ apply { args {
#for a total of 11 possible final orderings.
#(16 possible values for package_mode argument when you include the short-forms "",os,dev,os-dev,dev-os which always have 'internal' appended)
set test_package_mode [lindex $args 0]
#puts stderr "main.tcl test_package_mode: '$test_package_mode'"
switch -exact -- $test_package_mode {
internal -
@ -859,49 +861,80 @@ apply { args {
#--------------------------------------------------------
#Now that new 'package unknown' mechanism is in place - we can use package require
#assert arglist has had 'dev|os|os-dev etc' first arg removed if it was present.
if {[llength $arglist] == 1 && [lindex $arglist 0] eq "tclsh"} {
#called as <executable> dev tclsh or <executable> tclsh
#we would like to drop through to standard tclsh repl without launching another process
#tclMain.c doesn't allow it unless patched.
if {![info exists ::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true 0
} else {
if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL)
} else {
#tclsh,shell,shellspy
set subcommand [lindex $arglist 0]
switch -- $subcommand {
tclsh - shellspy - punk {
set subcommand_arglist [lrange $arglist 1 end]
}
default {
set subcommand punk
set subcommand_arglist $arglist
}
}
set ::argv $subcommand_arglist
set ::argc [llength $subcommand_arglist]
switch -- $subcommand {
tclsh {
#called as <executable> dev tclsh or <executable> tclsh
#we would like to drop through to standard tclsh repl without launching another process
#tclMain.c doesn't allow it unless patched.
if {![info exists ::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true 0
} else {
if {[string is boolean -strict $::env(TCLSH_PIPEREPL)]} {
set is_tclsh_piperepl_env_true $::env(TCLSH_PIPEREPL)
} else {
set is_tclsh_piperepl_env_true 0
}
}
if {!$is_tclsh_piperepl_env_true} {
puts stderr "tcl_interactive: $::tcl_interactive"
puts stderr "stdin: [chan configure stdin]"
puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean"
} else {
#according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired
#check if tclsh/punk has had the piperepl patch applied - in which case tclsh(istty) should exist
if {![info exists ::tclsh(istty)]} {
puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch"
}
}
set ::tcl_interactive 1
set ::tclsh(dorepl) 1
if {[llength $subcommand_arglist]} {
set normscript [file normalize [lindex $subcommand_arglist 0]]
info script $normscript
set ::argv0 $normscript
set ::argv [lrange $subcommand_arglist 1 end]
set ::argc [llength $::argv]
#we are in an apply context here - so we need to uplevel to get the source to work as expected
uplevel 1 [list source [lindex $subcommand_arglist 0]]
}
}
if {!$is_tclsh_piperepl_env_true} {
puts stderr "tcl_interactive: $::tcl_interactive"
puts stderr "stdin: [chan configure stdin]"
puts stderr "Environment variable TCLSH_PIPEREPL is not set or is false or is not a boolean"
} else {
#according to env TCLSH_PIPEREPL and our commandline argument - tclsh repl is desired
#check if tclsh/punk has had the piperepl patch applied - in which case tclsh(istty) should exist
if {![info exists ::tclsh(istty)]} {
puts stderr "error: the runtime doesn't appear to have been compiled with the piperepl patch"
}
}
set ::tcl_interactive 1
set ::tclsh(dorepl) 1
} elseif {[lindex $arglist 0] eq "shellspy"} {
#pass through to shellspy commandline processor
#puts stdout "main.tcl launching app-shellspy"
package require app-shellspy
} elseif {[llength $arglist]} {
package require app-punkshell
} else {
#punk shell
#todo logger ?
#puts stdout "main.tcl launching app-punk. pkg names count:[llength [package names]]"
#puts ">> $::auto_path"
#puts ">>> [tcl::tm::list]"
#puts ">>>> [package unknown]"
package require app-punk
#app-punk starts repl
#repl::start stdin -title "main.tcl"
shellspy {
#pass through to shellspy commandline processor
package require app-shellspy
}
punk {
if {[llength $subcommand_arglist]} {
#puts stdout "main.tcl launching app-punkshell with args: $subcommand_arglist"
package require app-punkshell
} else {
#punk interactive shell
package require app-punk
#app-punk starts repl
}
}
}
#if {[llength $arglist] == 1 && [lindex $arglist 0] eq "tclsh"} {
#} elseif {[lindex $arglist 0] eq "shellspy"} {
#} elseif {[llength $arglist]} {
#} else {
#}
}} {*}$::argv

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save