Browse Source

punk::winlnk and punk patternmatchin fixes

master
Julian Noble 2 days ago
parent
commit
78b8ad803e
  1. 72
      src/bootsupport/modules/punk-0.1.tm
  2. 5488
      src/bootsupport/modules/punk/lib-0.1.6.tm
  3. 391
      src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  4. 1014
      src/bootsupport/modules/punk/winlnk-0.1.1.tm
  5. 11
      src/bootsupport/modules/punk/winpath-0.1.0.tm
  6. 72
      src/modules/punk-0.1.tm
  7. 123
      src/modules/punk/lib-999999.0a1.0.tm
  8. 2
      src/modules/punk/lib-buildversion.txt
  9. 391
      src/modules/punk/nav/fs-999999.0a1.0.tm
  10. 202
      src/modules/punk/winlnk-999999.0a1.0.tm
  11. 2
      src/modules/punk/winlnk-buildversion.txt
  12. 11
      src/modules/punk/winpath-999999.0a1.0.tm
  13. 72
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm
  14. 5488
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  15. 391
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  16. 1014
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/winlnk-0.1.1.tm
  17. 11
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/winpath-0.1.0.tm
  18. 72
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm
  19. 5488
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.6.tm
  20. 391
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm
  21. 1014
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/winlnk-0.1.1.tm
  22. 11
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/winpath-0.1.0.tm
  23. 72
      src/vfs/_vfscommon.vfs/modules/punk-0.1.tm
  24. 5488
      src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm
  25. 391
      src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm
  26. 1014
      src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.1.tm
  27. 11
      src/vfs/_vfscommon.vfs/modules/punk/winpath-0.1.0.tm

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

@ -1422,7 +1422,7 @@ namespace eval punk {
}
if {[string is digit -strict [join $subindices ""]]} {
#review tip 551 (tcl9+?)
#review tip 551 (underscores in numerical literals) (tcl9+)
#puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
#pure numeric keylist - put straight to lindex
#
@ -2650,6 +2650,76 @@ namespace eval punk {
}
}]
}
} elseif {[punk::lib::is_indexset $index]} {
#review - a basic math statement such as 5-1 is also a valid member of an indexset
#see punk::lib::is_indexset and punk::lib::indexset_resolve
#single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
set is_range [expr {[string first ".." $index] >= 0}]
if {$get_not} {
if {$is_range} {
lappend INDEX_OPERATIONS list-range-not
} else {
lappend INDEX_OPERATIONS listindex-not
}
set assign_script {
set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] <idx>]]
}
} else {
if {$is_range} {
lappend INDEX_OPERATIONS list-range
} else {
lappend INDEX_OPERATIONS listindex
}
set assign_script {
set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] <idx>] {lindex $leveldata $i}]
}
}
if {$do_bounds_check} {
#bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
if {$is_range} {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
lassign [split <idx> ..] idx1 _ idx2
set v2 [punk::lib::lindex_resolve_basic $len $idx2]
if {isinf($v2)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
set v1 [punk::lib::lindex_resolve_basic $len $idx1]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
set v1 [punk::lib::lindex_resolve_basic $len <idx>]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
}
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
${$assign_script}
}
}]
}
set script [string map [list <idx> $index] $script]
} elseif {[string first "end" $index] >=0} {
if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {

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

File diff suppressed because it is too large Load Diff

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

@ -229,12 +229,16 @@ tcl::namespace::eval punk::nav::fs {
} else {
set stripbase 1
}
if {$v eq "/"} {
#hack
dict set matchinfo files {}
dict set matchinfo filesizes {}
}
set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo]
#we need to pass matchinfo that includes files even when only doing a directory listing (d/ /)
#This is because we want to display links/shortcuts that point to directories as directories.
#( ./ listing needs to show navigable items)
#if {$v eq "/"} {
# #dodgy hack that doesn't give proper display of all links/shortcuts that are pointing to directories.
# dict set matchinfo files {}
# dict set matchinfo filesizes {}
#}
set out [dirfiles_dict_as_lines -listing $v -stripbase $stripbase $matchinfo]
#set chunklist [list]
#lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"]
set result "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"
@ -258,10 +262,10 @@ tcl::namespace::eval punk::nav::fs {
#puts stdout "-->[ansistring VIEW $result]"
return $result
} else {
set atail [lassign $args a1]
set atail [lassign $args cdtarget]
if {[llength $args] == 1} {
set a1 [lindex $args 0]
switch -exact -- $a1 {
set cdtarget [lindex $args 0]
switch -exact -- $cdtarget {
. - ./ {
tailcall punk::nav::fs::d/
}
@ -286,43 +290,88 @@ tcl::namespace::eval punk::nav::fs {
}
} else {
cd $up1
#set VIRTUAL_CWD [file normalize $a1]
#set VIRTUAL_CWD [file normalize $cdtarget]
}
tailcall punk::nav::fs::d/ $v
}
}
if {![regexp {[*?]} $a1] && [file pathtype $a1] ne "relative"} {
set cdtarget_copy [punk::nav::fs::system::valcopy $cdtarget]
set cdtarget_copy [string map {\\ /} $cdtarget_copy]
if {[string range $cdtarget_copy 0 3] eq "//?/"} {
#handle dos device paths - convert to normal path for glob testing
set glob_test [string range $cdtarget_copy 3 end]
set cdtarget_is_glob [regexp {[*?]} $glob_test]
} else {
set cdtarget_is_glob [regexp {[*?]} $cdtarget]
}
if {!$cdtarget_is_glob} {
set cdtarget_file_type [file type $cdtarget]
#e.g may be a link - whilst the type returned in the 'file stat' info reflects the type of the link target
} else {
set cdtarget_file_type "glob"
}
if {!$cdtarget_is_glob && [file pathtype $cdtarget] ne "relative"} {
#non-relative non-glob
if { ![string match //zipfs:/* $a1]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD $a1
tailcall punk::nav::fs::d/ $v
if {![string match //zipfs:/* $cdtarget]} {
switch -- $cdtarget_file_type {
link {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
}
if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD [file normalize $a1]
tailcall punk::nav::fs::d/ $v
if {!$cdtarget_is_glob && ![string match //zipfs:/* $cdtarget] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
switch -- $cdtarget_file_type {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
#if {[file type $cdtarget] eq "directory"} {
# cd $cdtarget
# #set VIRTUAL_CWD [file normalize $cdtarget]
# tailcall punk::nav::fs::d/ $v
#}
}
if {![regexp {[*?]} $a1]} {
if {!$cdtarget_is_glob} {
#NON-Glob target
#review
if {[string match //zipfs:/* $a1]} {
if {[Zipfs_path_within_zipfs_mounts $a1]} {
commandstack::basecall cd $a1
if {[string match //zipfs:/* $cdtarget]} {
if {[Zipfs_path_within_zipfs_mounts $cdtarget]} {
commandstack::basecall cd $cdtarget
}
set VIRTUAL_CWD $a1
set curdir $a1
set VIRTUAL_CWD $cdtarget
set curdir $cdtarget
} else {
set target [punk::path::normjoin $VIRTUAL_CWD $a1]
set target [punk::path::normjoin $VIRTUAL_CWD $cdtarget]
if {[string match //zipfs:/* $VIRTUAL_CWD]} {
if {[Zipfs_path_within_zipfs_mounts $target]} {
commandstack::basecall cd $target
@ -521,20 +570,93 @@ tcl::namespace::eval punk::nav::fs {
return $result
}
punk::args::define {
@id -id ::punk::nav::fs::d/new
-nonportable -type none -help\
"Allow creation of directories which may not be portable across platforms.
Use with caution and only when you know what you are doing.
This allows creation of directories with names that may be invalid on some
platforms, or that may have special meanings on some platforms
(e.g reserved device names on windows).
If -nonportable is not supplied, then an error will be raised if any supplied
path is non-portable as defined by punk::winpath::illegalname_test.
Regardless of whether -nonportable is supplied or not, some characters are not
suitable for windows or most other platforms and will be rejected with an error.
An example of this is the null character (\0)."
@values -min 1 -max -1 -type string
path -type string -multiple 1 -help\
"Path(s) to create. Can be absolute or relative.
If any path is rejected due to -nonportable or other invalid characters,
or because a parent directory is not writable, then no directories will be created.
If a path already exists, then it will be left as-is and no error will be raised.
If despite passing the name tests or writability tests, a directory cannot be
created for some reason (e.g other filesystem error) then an error will be raised
and processing of any remaining paths will be aborted."
}
#todo - synchronize overall behaviour of d/new with that of n/new (for namespaces)
proc d/new {args} {
if {![llength $args]} {
error "usage: d/new <dir> \[<dir> ...\]"
}
set a1 [lindex $args 0]
set argd [punk::args::parse $args withid ::punk::nav::fs::d/new]
lassign [dict values $argd] leaders opts values received
set paths [dict get $values path]
set allow_nonportable [dict exists $received -nonportable]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
set fullpath_list [list]
set error_paths [list]
foreach p $paths {
if {!$allow_nonportable && [punk::winpath::illegalname_test $p]} {
#error "punk::nav::fs::d/new Path '$p' is not portable and may not be created without -nonportable option"
lappend error_paths [list $p "Path '$p' is not portable and may not be created without -nonportable option"]
continue
}
if {[string first \0 $p] != -1} {
#error "punk::nav::fs::d/new Path '$p' contains null character which is not allowed"
lappend error_paths [list $p "Path '$p' contains null character which is not allowed"]
continue
}
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
#e.g can return something like //?/C:/test/illegalpath. which is not a valid path for mkdir.
set fullpath [file join $path1 {*}[lrange $args 1 end]]
#Some subpaths of the supplied paths to create may already exist.
#we should test write permissions on the nearest existing parent of the supplied path to create, rather than just on the supplied path itself which may not exist at all.
set parent [file dirname $fullpath]
while {![file exists $parent]} {
set parent [file dirname $parent]
}
if {![file writable $parent]} {
#error "punk::nav::fs::d/new Cannot create directory '$fullpath' as parent '$parent' is not writable"
lappend error_paths [list $fullpath "Cannot create directory '$fullpath' as parent '$parent' is not writable"]
continue
}
lappend fullpath_list $fullpath
}
if {[llength $fullpath_list] != [llength $paths]} {
set path_error_display ""
foreach e $error_paths {
set p [lindex $e 0]
set m [lindex $e 1]
append path_error_display " Path: '$p' Error: $m\n"
}
error "punk::nav::fs::d/new One or more supplied paths were invalid or not writable:\n$path_error_display"
}
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
set num_created 0
set error_string ""
foreach fullpath $fullpath_list {
if {[catch {file mkdir $fullpath}]} {
set error_string "Failed to create directory '$fullpath' - processing of remaining paths aborted."
break
}
incr num_created
}
file mkdir $fullpath
d/ $fullpath
if {$error_string ne ""} {
error "punk::nav::fs::d/new $error_string\n$num_created directories out of [llength $fullpath_list] were created successfully before the error was encountered."
}
d/ $curdir
}
#todo use unknown to allow d/~c:/etc ??
@ -849,11 +971,11 @@ tcl::namespace::eval punk::nav::fs {
#file attr //cookit:/ returns {-vfs 1 -handle {}}
#we will treat it differently for now - use generic handler REVIEW
set in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
set is_in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
if {[llength [package provide vfs]]} {
foreach mount [vfs::filesystem info] {
if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
set is_in_vfs 1
break
}
}
@ -871,27 +993,27 @@ tcl::namespace::eval punk::nav::fs {
} else {
set next_opt_with_times [list -with_times $opt_with_times]
}
if {$in_vfs} {
if {$is_in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set in_zipfs 0
set in_cookit 1
set in_other_pseudovol 1
set invfs ""
switch -glob -- $location {
//zipfs:/* {
if {[info commands ::tcl::zipfs::mount] ne ""} {
set in_zipfs 1
set invfs zipfs
}
}
//cookit:/* {
set in_cookit 1
set invfs cookit
}
default {
#handle 'other/unknown' that mounts at a volume-like path //pseudovol:/
#(intentionally will not match a dos device path such as //?/c:/)
if {[regexp {//((?:(?!:|/).)+):/.*} $location _match pseudovol]} {
#pseudovol probably more than one char long
#we don't really expect something like //c:/ , but anyway, it's not the same as c:/ and for all we know someone could use that as a volume name?
set in_other_pseudovol 1 ;#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
set invfs pseudovol
} else {
#we could use 'file attr' here to test if {-vfs 1}
#but it's an extra filesystem hit on all normal paths too (which can be expensive on some systems)
@ -900,20 +1022,24 @@ tcl::namespace::eval punk::nav::fs {
}
}
if {$in_zipfs} {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_cookit} {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_other} {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
switch -- $invfs {
zipfs {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
cookit {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
pseudovol {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
default {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
}
}
@ -1018,11 +1144,13 @@ tcl::namespace::eval punk::nav::fs {
@id -id ::punk::nav::fs::dirfiles_dict_as_lines
-stripbase -default 0 -type boolean
-formatsizes -default 1 -type boolean
-listing -default "/" -choices {/ // //}
@values -min 1 -max -1 -type dict -unnamed true
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {args} {
set ts1 [clock milliseconds]
package require overtype
set argd [punk::args::parse $args withid ::punk::nav::fs::dirfiles_dict_as_lines]
lassign [dict values $argd] leaders opts vals
@ -1031,9 +1159,12 @@ tcl::namespace::eval punk::nav::fs {
# -- --- --- --- --- --- --- --- --- --- --- ---
set opt_stripbase [dict get $opts -stripbase]
set opt_stripbase [dict get $opts -stripbase]
set opt_formatsizes [dict get $opts -formatsizes]
set opt_listing [dict get $opts -listing]
# -- --- --- --- --- --- --- --- --- --- --- ---
#we still need to examine files for -listing / which means show only directories,
# because we want to display links/shortcuts that point to directories as directories
#if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied
set common_base ""
@ -1074,7 +1205,6 @@ tcl::namespace::eval punk::nav::fs {
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] {
set $fileset [list]
}
#set contents [lindex $list_of_dicts 0]
foreach contents $list_of_dicts {
lappend dirs {*}[dict get $contents dirs]
@ -1090,6 +1220,7 @@ tcl::namespace::eval punk::nav::fs {
lappend vfsmounts {*}[dict get $contents vfsmounts]
}
set fkeys [dict create] ;#avoid some file normalize calls..
if {$opt_stripbase && $common_base ne ""} {
set filetails [list]
@ -1224,27 +1355,41 @@ tcl::namespace::eval punk::nav::fs {
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
set ts2 [clock milliseconds]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::resolve $fname]
set target_type "file" ;#default/fallback
set shortcutinfo [punk::winlnk::resolve $fname]
if {[dict exists $shortcutinfo link_target]} {
set is_valid_lnk 1
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
set link_target_type [dict get $shortcutinfo target_type]
switch -- $link_target_type {
file {
set target_type "file"
}
directory - "local disk" {
set target_type "directory"
}
unknown {
#fall back to checking attributes and filesystem if we have a link_target but no target_type
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
} else {
#no link_target - probably an ordinary file - but there could have been some other error in reading the binary windows lnk format.
@ -1295,6 +1440,8 @@ tcl::namespace::eval punk::nav::fs {
}
unset finfo
puts stderr "dirfiles_dict_as_lines since ts2 [clock milliseconds] - $ts2 ms = [expr {[clock milliseconds] - $ts2}]"
puts stderr "dirfiles_dict_as_lines since start [clock milliseconds] - $ts1 ms = [expr {[clock milliseconds] - $ts1}]"
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -1304,58 +1451,82 @@ tcl::namespace::eval punk::nav::fs {
set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]]
set RST [punk::ansi::a]
if {$opt_listing eq "/"} {
#disply directories only (including items that were actually files that were links/shortcuts to directories)
set finfo_plus [list]
}
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold]
set f1 [punk::ansi::a+ white]
set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set d1 [punk::ansi::a+ cyan normal]
set d1_overrides [list]
#set d2 [punk::ansi::a+ defaultfg defaultbg normal]
set f1 [punk::ansi::a+ white normal]
set f1_overrides [list]
#set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set fdisp ""
if {[string length $d]} {
if {$d in $flaggedhidden} {
set d1 [punk::ansi::a+ cyan normal]
#set d1 [punk::ansi::a+ Term-grey50 normal]
lappend d1_overrides term-grey50
}
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red Yellow bold]
} else {
set d1 [punk::ansi::a+ green Purple bold]
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red White bold]
} else {
set d1 [punk::ansi::a+ green bold]
}
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red bold]
}
lappend d1_overrides Green
}
if {$d in $nonportable} {
#lappend d1_overrides underline undt-red ;#we use underlins to indicate symlinks and shortcuts, so we shouldn't use underlines here if possible.
lappend d1_overrides italic bold
}
#if {$d in $vfsmounts} {
# if {$d in $flaggedhidden} {
# #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indication of 'hidden' status - REVIEW
# #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
# #mark it differently for now.. (todo bug report?)
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red Yellow bold]
# } else {
# set d1 [punk::ansi::a+ green Purple bold]
# }
# } else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red White bold]
# } else {
# set d1 [punk::ansi::a+ green bold]
# }
# }
#} else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red bold]
# }
#}
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {[llength $d1_overrides]} {
set d1 [punk::ansi::a+ {*}$d1_overrides]
}
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
}
if {[llength $filerec]} {
set fname [dict get $filerec file]
set fdisp [dict get $filerec display]
if {$fname in $flaggedhidden} {
set f1 [punk::ansi::a+ Purple]
} else {
if {$fname in $nonportable} {
set f1 [punk::ansi::a+ red bold]
}
#set f1 [punk::ansi::a+ Term-grey50]
lappend f1_overrides term-grey50
}
if {$fname in $nonportable} {
lappend f1_overrides italic bold
}
if {[llength $f1_overrides]} {
set f1 [punk::ansi::a+ {*}$f1_overrides]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} else {
#either there are no files or opt_listing is / = show dirs only (some of which may have actually been files that were links/shortcuts to directories)
lappend displaylist [overtype::left $col1 $d1$d$RST]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
}
return [punk::lib::list_as_lines $displaylist]
@ -1469,6 +1640,12 @@ tcl::namespace::eval punk::nav::fs::system {
#[subsection {Namespace punk::nav::fs::system}]
#[para] Internal functions that are not part of the API
#utility function to copy values from one variable to another without sharing the reference.
#Useful for example to avoid some issues with possible shimmering of the underlying type of file paths.
proc valcopy {obj} {
append obj2 $obj {}
}
#ordinary emission of chunklist when no repl
proc emit_chunklist {chunklist} {
set result ""

1014
src/bootsupport/modules/punk/winlnk-0.1.1.tm

File diff suppressed because it is too large Load Diff

11
src/bootsupport/modules/punk/winpath-0.1.0.tm

@ -196,7 +196,8 @@ namespace eval punk::winpath {
#https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
#according to the above: Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
set reserved [list < > : \" / \\ | ? *]
#embedded nulls (\0) are also disallowed - but these are also disallowed on unix-like platforms.
set windows_reserved_names [list "CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6" "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7" "LPT8" "LPT9"]
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
@ -208,6 +209,14 @@ namespace eval punk::winpath {
#/./ /../ segments don't require protection - keep checking.
continue
}
if {[string toupper [file rootname $seg]] in $windows_reserved_names} {
#windows reserved names
#there are reports that these names aren't usable even with file extension - e.g that CON.txt is reserved and can't be created by some standard tools.
#In practice on windows 11 in 2026, cmd.exe,notepad,explorer and powershell seem to handle creation and access of CON.txt and PRN.txt etc without issue.
# the windows documentation reference above however still states that these names with an extension should be avoided.
#For this reason - we will still treat these as reserved and require protection with dos device syntax - even though in practice they seem to be usable without it.
return 1
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph

72
src/modules/punk-0.1.tm

@ -1422,7 +1422,7 @@ namespace eval punk {
}
if {[string is digit -strict [join $subindices ""]]} {
#review tip 551 (tcl9+?)
#review tip 551 (underscores in numerical literals) (tcl9+)
#puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
#pure numeric keylist - put straight to lindex
#
@ -2650,6 +2650,76 @@ namespace eval punk {
}
}]
}
} elseif {[punk::lib::is_indexset $index]} {
#review - a basic math statement such as 5-1 is also a valid member of an indexset
#see punk::lib::is_indexset and punk::lib::indexset_resolve
#single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
set is_range [expr {[string first ".." $index] >= 0}]
if {$get_not} {
if {$is_range} {
lappend INDEX_OPERATIONS list-range-not
} else {
lappend INDEX_OPERATIONS listindex-not
}
set assign_script {
set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] <idx>]]
}
} else {
if {$is_range} {
lappend INDEX_OPERATIONS list-range
} else {
lappend INDEX_OPERATIONS listindex
}
set assign_script {
set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] <idx>] {lindex $leveldata $i}]
}
}
if {$do_bounds_check} {
#bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
if {$is_range} {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
lassign [split <idx> ..] idx1 _ idx2
set v2 [punk::lib::lindex_resolve_basic $len $idx2]
if {isinf($v2)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
set v1 [punk::lib::lindex_resolve_basic $len $idx1]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
set v1 [punk::lib::lindex_resolve_basic $len <idx>]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
}
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
${$assign_script}
}
}]
}
set script [string map [list <idx> $index] $script]
} elseif {[string first "end" $index] >=0} {
if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {

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

@ -1895,6 +1895,10 @@ namespace eval punk::lib {
lappend keyset $p
lappend keyset_structure list
}
} elseif {[punk::lib::is_indexset $p]} {
set keys [punk::lib::indexset_resolve [llength $dval] $p]
lappend keyset {*}$keys
lappend keyset_structure {*}[lrepeat [llength $keys] list]
} elseif {[string match "?*-?*" $p]} {
#could be either - don't change type
#list indices with tcl8.7 underscores? be careful. Before 8.7 we could have used regexp \d on integers
@ -2184,60 +2188,81 @@ namespace eval punk::lib {
}
string {
set hidekey 1
if {$key eq "%string"} {
set hidekey 1
set thisval $dval
} elseif {$key eq "%ansiview"} {
set thisval [ansistring VIEW -lf 1 $dval]
} elseif {$key eq "%ansiviewstyle"} {
set thisval [ansistring VIEWSTYLE -lf 1 $dval]
} elseif {[string match *lpad-* $key]} {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + $extra}]
set thisval [textblock::pad $dval -which left -width $width]
} elseif {[string match *lpadstr-* $key]} {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}]
set thisval [textblock::pad $dval -which left -width $width -padchar $extra]
} elseif {[string match *rpad-* $key]} {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + $extra}]
set thisval [textblock::pad $dval -which right -width $width]
} elseif {[string match *rpadstr-* $key]} {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}]
set thisval [textblock::pad $dval -which right -width $width -padchar $extra]
} else {
if {[lindex $key 1] eq "query"} {
set qry [lindex $key 0]
} else {
set qry $key
switch -- $key {
"%string" {
set hidekey 1
set thisval $dval
}
set thisval $dval
if {[string index $key 0] ne "%"} {
set key %$key
"%ansiview" {
set thisval [ansistring VIEW -lf 1 $dval]
}
% thisval.= $key= $thisval
}
set nextpatterns [list]
#which pattern nest applies to this branch
set nextsub [dict get $pattern_next_substructure $pattern_nest]
if {[llength $pattern_nest_list]} {
set nest [lrange $pattern_nest_list 1 end]
lappend nextpatterns {*}[join $nest /]
"%ansiviewstyle" {
set thisval [ansistring VIEWSTYLE -lf 1 $dval]
}
#set nextopts [dict get $argd opts]
dict set nextopts -roottype $nextsub
dict set nextopts -channel none
default {
switch -glob -- $key {
*lpad-* {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + $extra}]
set thisval [textblock::pad $dval -which left -width $width]
}
%lpadstr-* {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}]
set thisval [textblock::pad $dval -which left -width $width -padchar $extra]
}
%rpad-* {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + $extra}]
set thisval [textblock::pad $dval -which right -width $width]
}
%rpadstr-* {
set hidekey 1
lassign [split $key -] _ extra
set width [expr {[textblock::width $dval] + [tcl::string::length $extra]}]
set thisval [textblock::pad $dval -which right -width $width -padchar $extra]
}
%split-* {
#split on one or more chars - review
set hidekey 1
lassign [split $key -] _ splitchars
set thisval [split $dval $splitchars]
}
default {
if {[lindex $key 1] eq "query"} {
set qry [lindex $key 0]
} else {
set qry $key
}
set thisval $dval
if {[string index $key 0] ne "%"} {
set key %$key
}
#pipeline
% thisval.= $key= $thisval
}
}
if {[llength $nextpatterns]} {
set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns]
}
}
set nextpatterns [list]
#which pattern nest applies to this branch
set nextsub [dict get $pattern_next_substructure $pattern_nest]
if {[llength $pattern_nest_list]} {
set nest [lrange $pattern_nest_list 1 end]
lappend nextpatterns {*}[join $nest /]
}
#set nextopts [dict get $argd opts]
dict set nextopts -roottype $nextsub
dict set nextopts -channel none
if {[llength $nextpatterns]} {
set thisval [showdict {*}$nextopts -- $thisval {*}$nextpatterns]
}
}
}

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

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

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

@ -229,12 +229,16 @@ tcl::namespace::eval punk::nav::fs {
} else {
set stripbase 1
}
if {$v eq "/"} {
#hack
dict set matchinfo files {}
dict set matchinfo filesizes {}
}
set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo]
#we need to pass matchinfo that includes files even when only doing a directory listing (d/ /)
#This is because we want to display links/shortcuts that point to directories as directories.
#( ./ listing needs to show navigable items)
#if {$v eq "/"} {
# #dodgy hack that doesn't give proper display of all links/shortcuts that are pointing to directories.
# dict set matchinfo files {}
# dict set matchinfo filesizes {}
#}
set out [dirfiles_dict_as_lines -listing $v -stripbase $stripbase $matchinfo]
#set chunklist [list]
#lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"]
set result "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"
@ -258,10 +262,10 @@ tcl::namespace::eval punk::nav::fs {
#puts stdout "-->[ansistring VIEW $result]"
return $result
} else {
set atail [lassign $args a1]
set atail [lassign $args cdtarget]
if {[llength $args] == 1} {
set a1 [lindex $args 0]
switch -exact -- $a1 {
set cdtarget [lindex $args 0]
switch -exact -- $cdtarget {
. - ./ {
tailcall punk::nav::fs::d/
}
@ -286,43 +290,88 @@ tcl::namespace::eval punk::nav::fs {
}
} else {
cd $up1
#set VIRTUAL_CWD [file normalize $a1]
#set VIRTUAL_CWD [file normalize $cdtarget]
}
tailcall punk::nav::fs::d/ $v
}
}
if {![regexp {[*?]} $a1] && [file pathtype $a1] ne "relative"} {
set cdtarget_copy [punk::nav::fs::system::valcopy $cdtarget]
set cdtarget_copy [string map {\\ /} $cdtarget_copy]
if {[string range $cdtarget_copy 0 3] eq "//?/"} {
#handle dos device paths - convert to normal path for glob testing
set glob_test [string range $cdtarget_copy 3 end]
set cdtarget_is_glob [regexp {[*?]} $glob_test]
} else {
set cdtarget_is_glob [regexp {[*?]} $cdtarget]
}
if {!$cdtarget_is_glob} {
set cdtarget_file_type [file type $cdtarget]
#e.g may be a link - whilst the type returned in the 'file stat' info reflects the type of the link target
} else {
set cdtarget_file_type "glob"
}
if {!$cdtarget_is_glob && [file pathtype $cdtarget] ne "relative"} {
#non-relative non-glob
if { ![string match //zipfs:/* $a1]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD $a1
tailcall punk::nav::fs::d/ $v
if {![string match //zipfs:/* $cdtarget]} {
switch -- $cdtarget_file_type {
link {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
}
if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD [file normalize $a1]
tailcall punk::nav::fs::d/ $v
if {!$cdtarget_is_glob && ![string match //zipfs:/* $cdtarget] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
switch -- $cdtarget_file_type {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
#if {[file type $cdtarget] eq "directory"} {
# cd $cdtarget
# #set VIRTUAL_CWD [file normalize $cdtarget]
# tailcall punk::nav::fs::d/ $v
#}
}
if {![regexp {[*?]} $a1]} {
if {!$cdtarget_is_glob} {
#NON-Glob target
#review
if {[string match //zipfs:/* $a1]} {
if {[Zipfs_path_within_zipfs_mounts $a1]} {
commandstack::basecall cd $a1
if {[string match //zipfs:/* $cdtarget]} {
if {[Zipfs_path_within_zipfs_mounts $cdtarget]} {
commandstack::basecall cd $cdtarget
}
set VIRTUAL_CWD $a1
set curdir $a1
set VIRTUAL_CWD $cdtarget
set curdir $cdtarget
} else {
set target [punk::path::normjoin $VIRTUAL_CWD $a1]
set target [punk::path::normjoin $VIRTUAL_CWD $cdtarget]
if {[string match //zipfs:/* $VIRTUAL_CWD]} {
if {[Zipfs_path_within_zipfs_mounts $target]} {
commandstack::basecall cd $target
@ -521,20 +570,93 @@ tcl::namespace::eval punk::nav::fs {
return $result
}
punk::args::define {
@id -id ::punk::nav::fs::d/new
-nonportable -type none -help\
"Allow creation of directories which may not be portable across platforms.
Use with caution and only when you know what you are doing.
This allows creation of directories with names that may be invalid on some
platforms, or that may have special meanings on some platforms
(e.g reserved device names on windows).
If -nonportable is not supplied, then an error will be raised if any supplied
path is non-portable as defined by punk::winpath::illegalname_test.
Regardless of whether -nonportable is supplied or not, some characters are not
suitable for windows or most other platforms and will be rejected with an error.
An example of this is the null character (\0)."
@values -min 1 -max -1 -type string
path -type string -multiple 1 -help\
"Path(s) to create. Can be absolute or relative.
If any path is rejected due to -nonportable or other invalid characters,
or because a parent directory is not writable, then no directories will be created.
If a path already exists, then it will be left as-is and no error will be raised.
If despite passing the name tests or writability tests, a directory cannot be
created for some reason (e.g other filesystem error) then an error will be raised
and processing of any remaining paths will be aborted."
}
#todo - synchronize overall behaviour of d/new with that of n/new (for namespaces)
proc d/new {args} {
if {![llength $args]} {
error "usage: d/new <dir> \[<dir> ...\]"
}
set a1 [lindex $args 0]
set argd [punk::args::parse $args withid ::punk::nav::fs::d/new]
lassign [dict values $argd] leaders opts values received
set paths [dict get $values path]
set allow_nonportable [dict exists $received -nonportable]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
set fullpath_list [list]
set error_paths [list]
foreach p $paths {
if {!$allow_nonportable && [punk::winpath::illegalname_test $p]} {
#error "punk::nav::fs::d/new Path '$p' is not portable and may not be created without -nonportable option"
lappend error_paths [list $p "Path '$p' is not portable and may not be created without -nonportable option"]
continue
}
if {[string first \0 $p] != -1} {
#error "punk::nav::fs::d/new Path '$p' contains null character which is not allowed"
lappend error_paths [list $p "Path '$p' contains null character which is not allowed"]
continue
}
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
#e.g can return something like //?/C:/test/illegalpath. which is not a valid path for mkdir.
set fullpath [file join $path1 {*}[lrange $args 1 end]]
#Some subpaths of the supplied paths to create may already exist.
#we should test write permissions on the nearest existing parent of the supplied path to create, rather than just on the supplied path itself which may not exist at all.
set parent [file dirname $fullpath]
while {![file exists $parent]} {
set parent [file dirname $parent]
}
if {![file writable $parent]} {
#error "punk::nav::fs::d/new Cannot create directory '$fullpath' as parent '$parent' is not writable"
lappend error_paths [list $fullpath "Cannot create directory '$fullpath' as parent '$parent' is not writable"]
continue
}
lappend fullpath_list $fullpath
}
if {[llength $fullpath_list] != [llength $paths]} {
set path_error_display ""
foreach e $error_paths {
set p [lindex $e 0]
set m [lindex $e 1]
append path_error_display " Path: '$p' Error: $m\n"
}
error "punk::nav::fs::d/new One or more supplied paths were invalid or not writable:\n$path_error_display"
}
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
set num_created 0
set error_string ""
foreach fullpath $fullpath_list {
if {[catch {file mkdir $fullpath}]} {
set error_string "Failed to create directory '$fullpath' - processing of remaining paths aborted."
break
}
incr num_created
}
file mkdir $fullpath
d/ $fullpath
if {$error_string ne ""} {
error "punk::nav::fs::d/new $error_string\n$num_created directories out of [llength $fullpath_list] were created successfully before the error was encountered."
}
d/ $curdir
}
#todo use unknown to allow d/~c:/etc ??
@ -849,11 +971,11 @@ tcl::namespace::eval punk::nav::fs {
#file attr //cookit:/ returns {-vfs 1 -handle {}}
#we will treat it differently for now - use generic handler REVIEW
set in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
set is_in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
if {[llength [package provide vfs]]} {
foreach mount [vfs::filesystem info] {
if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
set is_in_vfs 1
break
}
}
@ -871,27 +993,27 @@ tcl::namespace::eval punk::nav::fs {
} else {
set next_opt_with_times [list -with_times $opt_with_times]
}
if {$in_vfs} {
if {$is_in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set in_zipfs 0
set in_cookit 1
set in_other_pseudovol 1
set invfs ""
switch -glob -- $location {
//zipfs:/* {
if {[info commands ::tcl::zipfs::mount] ne ""} {
set in_zipfs 1
set invfs zipfs
}
}
//cookit:/* {
set in_cookit 1
set invfs cookit
}
default {
#handle 'other/unknown' that mounts at a volume-like path //pseudovol:/
#(intentionally will not match a dos device path such as //?/c:/)
if {[regexp {//((?:(?!:|/).)+):/.*} $location _match pseudovol]} {
#pseudovol probably more than one char long
#we don't really expect something like //c:/ , but anyway, it's not the same as c:/ and for all we know someone could use that as a volume name?
set in_other_pseudovol 1 ;#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
set invfs pseudovol
} else {
#we could use 'file attr' here to test if {-vfs 1}
#but it's an extra filesystem hit on all normal paths too (which can be expensive on some systems)
@ -900,20 +1022,24 @@ tcl::namespace::eval punk::nav::fs {
}
}
if {$in_zipfs} {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_cookit} {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_other} {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
switch -- $invfs {
zipfs {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
cookit {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
pseudovol {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
default {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
}
}
@ -1018,11 +1144,13 @@ tcl::namespace::eval punk::nav::fs {
@id -id ::punk::nav::fs::dirfiles_dict_as_lines
-stripbase -default 0 -type boolean
-formatsizes -default 1 -type boolean
-listing -default "/" -choices {/ // //}
@values -min 1 -max -1 -type dict -unnamed true
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {args} {
set ts1 [clock milliseconds]
package require overtype
set argd [punk::args::parse $args withid ::punk::nav::fs::dirfiles_dict_as_lines]
lassign [dict values $argd] leaders opts vals
@ -1031,9 +1159,12 @@ tcl::namespace::eval punk::nav::fs {
# -- --- --- --- --- --- --- --- --- --- --- ---
set opt_stripbase [dict get $opts -stripbase]
set opt_stripbase [dict get $opts -stripbase]
set opt_formatsizes [dict get $opts -formatsizes]
set opt_listing [dict get $opts -listing]
# -- --- --- --- --- --- --- --- --- --- --- ---
#we still need to examine files for -listing / which means show only directories,
# because we want to display links/shortcuts that point to directories as directories
#if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied
set common_base ""
@ -1074,7 +1205,6 @@ tcl::namespace::eval punk::nav::fs {
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] {
set $fileset [list]
}
#set contents [lindex $list_of_dicts 0]
foreach contents $list_of_dicts {
lappend dirs {*}[dict get $contents dirs]
@ -1090,6 +1220,7 @@ tcl::namespace::eval punk::nav::fs {
lappend vfsmounts {*}[dict get $contents vfsmounts]
}
set fkeys [dict create] ;#avoid some file normalize calls..
if {$opt_stripbase && $common_base ne ""} {
set filetails [list]
@ -1224,27 +1355,41 @@ tcl::namespace::eval punk::nav::fs {
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
set ts2 [clock milliseconds]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::resolve $fname]
set target_type "file" ;#default/fallback
set shortcutinfo [punk::winlnk::resolve $fname]
if {[dict exists $shortcutinfo link_target]} {
set is_valid_lnk 1
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
set link_target_type [dict get $shortcutinfo target_type]
switch -- $link_target_type {
file {
set target_type "file"
}
directory - "local disk" {
set target_type "directory"
}
unknown {
#fall back to checking attributes and filesystem if we have a link_target but no target_type
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
} else {
#no link_target - probably an ordinary file - but there could have been some other error in reading the binary windows lnk format.
@ -1295,6 +1440,8 @@ tcl::namespace::eval punk::nav::fs {
}
unset finfo
puts stderr "dirfiles_dict_as_lines since ts2 [clock milliseconds] - $ts2 ms = [expr {[clock milliseconds] - $ts2}]"
puts stderr "dirfiles_dict_as_lines since start [clock milliseconds] - $ts1 ms = [expr {[clock milliseconds] - $ts1}]"
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -1304,58 +1451,82 @@ tcl::namespace::eval punk::nav::fs {
set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]]
set RST [punk::ansi::a]
if {$opt_listing eq "/"} {
#disply directories only (including items that were actually files that were links/shortcuts to directories)
set finfo_plus [list]
}
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold]
set f1 [punk::ansi::a+ white]
set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set d1 [punk::ansi::a+ cyan normal]
set d1_overrides [list]
#set d2 [punk::ansi::a+ defaultfg defaultbg normal]
set f1 [punk::ansi::a+ white normal]
set f1_overrides [list]
#set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set fdisp ""
if {[string length $d]} {
if {$d in $flaggedhidden} {
set d1 [punk::ansi::a+ cyan normal]
#set d1 [punk::ansi::a+ Term-grey50 normal]
lappend d1_overrides term-grey50
}
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red Yellow bold]
} else {
set d1 [punk::ansi::a+ green Purple bold]
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red White bold]
} else {
set d1 [punk::ansi::a+ green bold]
}
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red bold]
}
lappend d1_overrides Green
}
if {$d in $nonportable} {
#lappend d1_overrides underline undt-red ;#we use underlins to indicate symlinks and shortcuts, so we shouldn't use underlines here if possible.
lappend d1_overrides italic bold
}
#if {$d in $vfsmounts} {
# if {$d in $flaggedhidden} {
# #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indication of 'hidden' status - REVIEW
# #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
# #mark it differently for now.. (todo bug report?)
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red Yellow bold]
# } else {
# set d1 [punk::ansi::a+ green Purple bold]
# }
# } else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red White bold]
# } else {
# set d1 [punk::ansi::a+ green bold]
# }
# }
#} else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red bold]
# }
#}
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {[llength $d1_overrides]} {
set d1 [punk::ansi::a+ {*}$d1_overrides]
}
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
}
if {[llength $filerec]} {
set fname [dict get $filerec file]
set fdisp [dict get $filerec display]
if {$fname in $flaggedhidden} {
set f1 [punk::ansi::a+ Purple]
} else {
if {$fname in $nonportable} {
set f1 [punk::ansi::a+ red bold]
}
#set f1 [punk::ansi::a+ Term-grey50]
lappend f1_overrides term-grey50
}
if {$fname in $nonportable} {
lappend f1_overrides italic bold
}
if {[llength $f1_overrides]} {
set f1 [punk::ansi::a+ {*}$f1_overrides]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} else {
#either there are no files or opt_listing is / = show dirs only (some of which may have actually been files that were links/shortcuts to directories)
lappend displaylist [overtype::left $col1 $d1$d$RST]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
}
return [punk::lib::list_as_lines $displaylist]
@ -1469,6 +1640,12 @@ tcl::namespace::eval punk::nav::fs::system {
#[subsection {Namespace punk::nav::fs::system}]
#[para] Internal functions that are not part of the API
#utility function to copy values from one variable to another without sharing the reference.
#Useful for example to avoid some issues with possible shimmering of the underlying type of file paths.
proc valcopy {obj} {
append obj2 $obj {}
}
#ordinary emission of chunklist when no repl
proc emit_chunklist {chunklist} {
set result ""

202
src/modules/punk/winlnk-999999.0a1.0.tm

@ -115,7 +115,7 @@ tcl::namespace::eval punk::winlnk {
}
variable LinkFlags
set LinkFlags [dict create\
hasLinkTargetIDList 1\
HasLinkTargetIDList 1\
HasLinkInfo 2\
HasName 4\
HasRelativePath 8\
@ -477,6 +477,54 @@ tcl::namespace::eval punk::winlnk {
return 0
}
}
proc Get_LinkTargetIDList_content {contents} {
set idlist_size [Get_LinkTargetIDList_size $contents]
if {$idlist_size == 0} {
return ""
} else {
set idlist_content [string range $contents 78 [expr {78 + $idlist_size -1}]]
return $idlist_content
}
}
#some clues on the structure of the IDList content and how to parse it can be found in the analysis of CVE-2020-0729,
#which is a remote code execution vulnerability in Windows that can be exploited through specially crafted .lnk files that contain malicious IDList content.
#The analysis of this vulnerability provides insights into how the IDList content is structured and how it can be parsed to extract information about the link target and potentially execute code.
#https://www.zerodayinitiative.com/blog/2020/3/25/cve-2020-0729-remote-code-execution-through-lnk-files
proc Get_LinkTargetIDList_iteminfo {contents} {
set idlist_content [Get_LinkTargetIDList_content $contents]
set result {}
set offset 0
while {$offset < [string length $idlist_content]} {
if {[string length $idlist_content] - $offset < 2} break
set size_bytes [string range $idlist_content $offset [expr {$offset + 1}]] ;#size including these 2 bytes
binary scan $size_bytes su size
if {$size == 0} break
if {$size < 2} {
# Invalid size, abort
error "punk::winlnk::Get_LinkTargetIDList_iteminfo: Invalid ItemID size: $size at offset $offset"
}
if {$offset + $size > [string length $idlist_content]} {
# ItemID extends beyond content, stop parsing
puts stderr "punk::winlnk::Get_LinkTargetIDList_iteminfo: ItemID at offset $offset with size $size extends beyond content length, stopping parse"
break
}
set itemid [string range $idlist_content $offset [expr {$offset + $size - 1}]]
set itemid_bytes [string range $itemid 0 1]
binary scan $itemid_bytes su itemid_size
#in *general* byte 3 of the ItemID structure can be used to determine the type of the item
#(e.g. file, folder, network location, etc.) but this is not always reliable and can vary
#based on the specific structure of the ItemID and the context in which it is used
set itemid_type_byte [string index $itemid 2]
#puts stderr "ItemID size: $itemid_size, type byte: [format %02X [scan $itemid_type_byte %c]]"
set maybe_type [format %02X [scan $itemid_type_byte %c]]
lappend result [dict create size $itemid_size type $maybe_type rawcontent $itemid]
incr offset $size
}
return $result
}
proc Get_LinkInfo_content {contents} {
set idlist_size [Get_LinkTargetIDList_size $contents]
if {$idlist_size == 0} {
@ -497,11 +545,18 @@ tcl::namespace::eval punk::winlnk {
}
proc LinkInfo_get_fields {linkinfocontent} {
#TODO - finish parsing of LinkInfo - add support
#Link location information
#present if data flag HasLinkInfo exists.
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]
binary scan $bytes_linkinfoheadersize i headersize
set bytes_linkinfoflags [string range $linkinfocontent 8 11]
set r [binary scan $4bytes i flags] ;# i for little endian 32-bit signed int
set r [binary scan $bytes_linkinfoflags i flags] ;# i for little endian 32-bit signed int
#puts "linkinfoflags: $flags"
set localbasepath ""
@ -517,10 +572,10 @@ tcl::namespace::eval punk::winlnk {
#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
set bytes_volumeid_offset [string range $linkinfocontent 12 15]
set bytes_localbasepath_offset [string range $linkinfocontent 16 19]
set bytes_commonnetworkrelativelinkoffset [string range $linkinfocontent 20 23]
set bytes_commonpathsuffix_offset [string range $linkinfocontent 24 27]
binary scan $bytes_localbasepath_offset i bp_offset
if {$bp_offset > 0} {
@ -558,10 +613,11 @@ tcl::namespace::eval punk::winlnk {
}
return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix]
return [dict create localbasepath $localbasepath commonpathsuffix $commonpathsuffix note <incomplete>]
}
proc contents_get_info {contents} {
proc Contents_Get_Info {contents} {
#todo - return something like the perl lnk-parse-1.0.pl script?
@ -607,7 +663,9 @@ tcl::namespace::eval punk::winlnk {
set localbase_path ""
set suffix_path ""
set linkinfocontent [dict get $linkinfo_content_dict content]
set next_start [dict get $linkinfo_content_dict next_start] ;#location of section following LinkInfo (Location information) - this will be the Data Strings.
set link_target ""
set linkfields [dict create]
if {$linkinfocontent ne ""} {
set linkfields [LinkInfo_get_fields $linkinfocontent]
set localbase_path [dict get $linkfields localbasepath]
@ -662,10 +720,40 @@ tcl::namespace::eval punk::winlnk {
}
}
# ----------------------------------------------------------------------
#todo - get Data strings by parsing contents starting at $next_start
#stored in following order:
# description
# relative path
# working directory
# command line arguments
# icon location
#Data strings format:
# 2 bytes: number of characters in the string
# following: The string. ASCII or UTF-16 little-endian string
set datastring_dict [Contents_Get_DataStrings $contents $next_start]
# ----------------------------------------------------------------------
set file_attributes [Header_Get_FileAttributes $contents]
set linktargetidlist [Get_LinkTargetIDList_iteminfo $contents]
set target_type_info [Get_target_type $contents $file_attributes]
set target_type [dict get $target_type_info type]
set target_type_mech [dict get $target_type_info mechanism]
if {$target_type eq "unknown"} {
if {[file exists $link_target]} {
set target_type [file type $link_target]
set target_type_mech "filesystem"
}
}
set result [dict create\
link_target $link_target\
link_flags $flags_enabled\
file_attributes [Header_Get_FileAttributes $contents]\
file_attributes $file_attributes\
creation_time [Header_Get_CreationTime $contents]\
access_time [Header_Get_AccessTime $contents]\
write_time [Header_Get_WriteTime $contents]\
@ -673,8 +761,12 @@ tcl::namespace::eval punk::winlnk {
icon_index "<unimplemented>"\
showwnd "$showwnd"\
hotkey [Header_Get_HotKey $contents]\
relative_path "?"\
target_type $target_type\
target_type_mech $target_type_mech\
idlist $linktargetidlist\
linkinfo $linkfields\
]
#relative_path "?"
}
proc file_check_header {path} {
@ -707,7 +799,7 @@ tcl::namespace::eval punk::winlnk {
#[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]
return [Contents_Get_Info $c]
} else {
return [dict create error "lnk_header_check_failed"]
}
@ -728,8 +820,39 @@ tcl::namespace::eval punk::winlnk {
}
proc file_show_info {path} {
package require punk::lib
punk::lib::showdict [resolve $path] *
#punk::lib::showdict [resolve $path] */@*
set field_queries [dict create\
link_target link_target\
link_flags link_flags/@*\
file_attributes file_attributes\
creation_time creation_time\
access_time access_time\
write_time write_time\
target_length target_length\
icon_index icon_index\
showwnd showwnd\
hotkey hotkey\
target_type target_type\
idlist idlist/@*/@*.@*\
linkinfo linkinfo/@*.@*\
]
set info [resolve $path]
if {[dict exists $info error]} {
return "Error: [dict get $info error]"
} else {
set querystring ""
foreach field [dict keys $info] {
if {[dict exists $field_queries $field]} {
append querystring "[dict get $field_queries $field] "
} else {
append querystring "$field "
}
}
puts "querystring: $querystring"
return [punk::lib::showdict $info {*}$querystring]
}
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@ -758,6 +881,61 @@ tcl::namespace::eval punk::winlnk {
}
}
proc target_type {path} {
set content [Get_contents $path]
if {![Contents_check_header $content]} {
error "lnk_header_check_failed"
}
set info [Contents_Get_Info $content]
return [dict get $info target_type]
}
proc Get_target_type {content file_attributes} {
#determine type based on info in the .lnk file, such as file attributes and link flags
if {"DIRECTORY" in $file_attributes} {
return [dict create type directory mechanism file_attributes]"
} elseif {"ARCHIVE" in $file_attributes} {
return [dict create type file mechanism file_attributes]
} else {
set iteminfo [Get_LinkTargetIDList_iteminfo $content]
if {[llength $iteminfo] > 0} {
set first_item [lindex $iteminfo 0]
set first_item_type [dict get $first_item type]
set saw_2f 0
switch -- $first_item_type {
"1F" {
#plain files and folders always seem to have a first item type of 1F
#so does "local disk"
set type_so_far "unknown"
#For a file, we may first see multiple items of type 32 (directory) as we go through the folder structure,
#and then finally an item of type 31 (file) at the end.
#For a network location, we may see an item of type 2F.
#So we need to loop through all the items and keep track of what we've seen so far.
foreach item $iteminfo {
set item_type [dict get $item type]
if {$item_type eq "31"} {
set type_so_far "directory"
} elseif {$item_type eq "32"} {
return [dict create type file mechanism idlist]
} elseif {$item_type eq "2F"} {
set saw_2f 1
}
}
if {$type_so_far eq "unknown" && $saw_2f} {
return [dict create type "local disk" mechanism idlist]
}
return [dict create type $type_so_far mechanism idlist]
}
}
return [dict create type "unknown" mechanism idlist]
} else {
return [dict create type "unknown" mechanism idlist]
}
}
}
#proc sample1 {p1 n args} {
# #*** !doctools
# #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]]

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

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

11
src/modules/punk/winpath-999999.0a1.0.tm

@ -196,7 +196,8 @@ namespace eval punk::winpath {
#https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
#according to the above: Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
set reserved [list < > : \" / \\ | ? *]
#embedded nulls (\0) are also disallowed - but these are also disallowed on unix-like platforms.
set windows_reserved_names [list "CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6" "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7" "LPT8" "LPT9"]
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
@ -208,6 +209,14 @@ namespace eval punk::winpath {
#/./ /../ segments don't require protection - keep checking.
continue
}
if {[string toupper [file rootname $seg]] in $windows_reserved_names} {
#windows reserved names
#there are reports that these names aren't usable even with file extension - e.g that CON.txt is reserved and can't be created by some standard tools.
#In practice on windows 11 in 2026, cmd.exe,notepad,explorer and powershell seem to handle creation and access of CON.txt and PRN.txt etc without issue.
# the windows documentation reference above however still states that these names with an extension should be avoided.
#For this reason - we will still treat these as reserved and require protection with dos device syntax - even though in practice they seem to be usable without it.
return 1
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph

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

@ -1422,7 +1422,7 @@ namespace eval punk {
}
if {[string is digit -strict [join $subindices ""]]} {
#review tip 551 (tcl9+?)
#review tip 551 (underscores in numerical literals) (tcl9+)
#puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
#pure numeric keylist - put straight to lindex
#
@ -2650,6 +2650,76 @@ namespace eval punk {
}
}]
}
} elseif {[punk::lib::is_indexset $index]} {
#review - a basic math statement such as 5-1 is also a valid member of an indexset
#see punk::lib::is_indexset and punk::lib::indexset_resolve
#single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
set is_range [expr {[string first ".." $index] >= 0}]
if {$get_not} {
if {$is_range} {
lappend INDEX_OPERATIONS list-range-not
} else {
lappend INDEX_OPERATIONS listindex-not
}
set assign_script {
set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] <idx>]]
}
} else {
if {$is_range} {
lappend INDEX_OPERATIONS list-range
} else {
lappend INDEX_OPERATIONS listindex
}
set assign_script {
set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] <idx>] {lindex $leveldata $i}]
}
}
if {$do_bounds_check} {
#bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
if {$is_range} {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
lassign [split <idx> ..] idx1 _ idx2
set v2 [punk::lib::lindex_resolve_basic $len $idx2]
if {isinf($v2)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
set v1 [punk::lib::lindex_resolve_basic $len $idx1]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
set v1 [punk::lib::lindex_resolve_basic $len <idx>]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
}
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
${$assign_script}
}
}]
}
set script [string map [list <idx> $index] $script]
} elseif {[string first "end" $index] >=0} {
if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {

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

File diff suppressed because it is too large Load Diff

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

@ -229,12 +229,16 @@ tcl::namespace::eval punk::nav::fs {
} else {
set stripbase 1
}
if {$v eq "/"} {
#hack
dict set matchinfo files {}
dict set matchinfo filesizes {}
}
set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo]
#we need to pass matchinfo that includes files even when only doing a directory listing (d/ /)
#This is because we want to display links/shortcuts that point to directories as directories.
#( ./ listing needs to show navigable items)
#if {$v eq "/"} {
# #dodgy hack that doesn't give proper display of all links/shortcuts that are pointing to directories.
# dict set matchinfo files {}
# dict set matchinfo filesizes {}
#}
set out [dirfiles_dict_as_lines -listing $v -stripbase $stripbase $matchinfo]
#set chunklist [list]
#lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"]
set result "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"
@ -258,10 +262,10 @@ tcl::namespace::eval punk::nav::fs {
#puts stdout "-->[ansistring VIEW $result]"
return $result
} else {
set atail [lassign $args a1]
set atail [lassign $args cdtarget]
if {[llength $args] == 1} {
set a1 [lindex $args 0]
switch -exact -- $a1 {
set cdtarget [lindex $args 0]
switch -exact -- $cdtarget {
. - ./ {
tailcall punk::nav::fs::d/
}
@ -286,43 +290,88 @@ tcl::namespace::eval punk::nav::fs {
}
} else {
cd $up1
#set VIRTUAL_CWD [file normalize $a1]
#set VIRTUAL_CWD [file normalize $cdtarget]
}
tailcall punk::nav::fs::d/ $v
}
}
if {![regexp {[*?]} $a1] && [file pathtype $a1] ne "relative"} {
set cdtarget_copy [punk::nav::fs::system::valcopy $cdtarget]
set cdtarget_copy [string map {\\ /} $cdtarget_copy]
if {[string range $cdtarget_copy 0 3] eq "//?/"} {
#handle dos device paths - convert to normal path for glob testing
set glob_test [string range $cdtarget_copy 3 end]
set cdtarget_is_glob [regexp {[*?]} $glob_test]
} else {
set cdtarget_is_glob [regexp {[*?]} $cdtarget]
}
if {!$cdtarget_is_glob} {
set cdtarget_file_type [file type $cdtarget]
#e.g may be a link - whilst the type returned in the 'file stat' info reflects the type of the link target
} else {
set cdtarget_file_type "glob"
}
if {!$cdtarget_is_glob && [file pathtype $cdtarget] ne "relative"} {
#non-relative non-glob
if { ![string match //zipfs:/* $a1]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD $a1
tailcall punk::nav::fs::d/ $v
if {![string match //zipfs:/* $cdtarget]} {
switch -- $cdtarget_file_type {
link {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
}
if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD [file normalize $a1]
tailcall punk::nav::fs::d/ $v
if {!$cdtarget_is_glob && ![string match //zipfs:/* $cdtarget] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
switch -- $cdtarget_file_type {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
#if {[file type $cdtarget] eq "directory"} {
# cd $cdtarget
# #set VIRTUAL_CWD [file normalize $cdtarget]
# tailcall punk::nav::fs::d/ $v
#}
}
if {![regexp {[*?]} $a1]} {
if {!$cdtarget_is_glob} {
#NON-Glob target
#review
if {[string match //zipfs:/* $a1]} {
if {[Zipfs_path_within_zipfs_mounts $a1]} {
commandstack::basecall cd $a1
if {[string match //zipfs:/* $cdtarget]} {
if {[Zipfs_path_within_zipfs_mounts $cdtarget]} {
commandstack::basecall cd $cdtarget
}
set VIRTUAL_CWD $a1
set curdir $a1
set VIRTUAL_CWD $cdtarget
set curdir $cdtarget
} else {
set target [punk::path::normjoin $VIRTUAL_CWD $a1]
set target [punk::path::normjoin $VIRTUAL_CWD $cdtarget]
if {[string match //zipfs:/* $VIRTUAL_CWD]} {
if {[Zipfs_path_within_zipfs_mounts $target]} {
commandstack::basecall cd $target
@ -521,20 +570,93 @@ tcl::namespace::eval punk::nav::fs {
return $result
}
punk::args::define {
@id -id ::punk::nav::fs::d/new
-nonportable -type none -help\
"Allow creation of directories which may not be portable across platforms.
Use with caution and only when you know what you are doing.
This allows creation of directories with names that may be invalid on some
platforms, or that may have special meanings on some platforms
(e.g reserved device names on windows).
If -nonportable is not supplied, then an error will be raised if any supplied
path is non-portable as defined by punk::winpath::illegalname_test.
Regardless of whether -nonportable is supplied or not, some characters are not
suitable for windows or most other platforms and will be rejected with an error.
An example of this is the null character (\0)."
@values -min 1 -max -1 -type string
path -type string -multiple 1 -help\
"Path(s) to create. Can be absolute or relative.
If any path is rejected due to -nonportable or other invalid characters,
or because a parent directory is not writable, then no directories will be created.
If a path already exists, then it will be left as-is and no error will be raised.
If despite passing the name tests or writability tests, a directory cannot be
created for some reason (e.g other filesystem error) then an error will be raised
and processing of any remaining paths will be aborted."
}
#todo - synchronize overall behaviour of d/new with that of n/new (for namespaces)
proc d/new {args} {
if {![llength $args]} {
error "usage: d/new <dir> \[<dir> ...\]"
}
set a1 [lindex $args 0]
set argd [punk::args::parse $args withid ::punk::nav::fs::d/new]
lassign [dict values $argd] leaders opts values received
set paths [dict get $values path]
set allow_nonportable [dict exists $received -nonportable]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
set fullpath_list [list]
set error_paths [list]
foreach p $paths {
if {!$allow_nonportable && [punk::winpath::illegalname_test $p]} {
#error "punk::nav::fs::d/new Path '$p' is not portable and may not be created without -nonportable option"
lappend error_paths [list $p "Path '$p' is not portable and may not be created without -nonportable option"]
continue
}
if {[string first \0 $p] != -1} {
#error "punk::nav::fs::d/new Path '$p' contains null character which is not allowed"
lappend error_paths [list $p "Path '$p' contains null character which is not allowed"]
continue
}
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
#e.g can return something like //?/C:/test/illegalpath. which is not a valid path for mkdir.
set fullpath [file join $path1 {*}[lrange $args 1 end]]
#Some subpaths of the supplied paths to create may already exist.
#we should test write permissions on the nearest existing parent of the supplied path to create, rather than just on the supplied path itself which may not exist at all.
set parent [file dirname $fullpath]
while {![file exists $parent]} {
set parent [file dirname $parent]
}
if {![file writable $parent]} {
#error "punk::nav::fs::d/new Cannot create directory '$fullpath' as parent '$parent' is not writable"
lappend error_paths [list $fullpath "Cannot create directory '$fullpath' as parent '$parent' is not writable"]
continue
}
lappend fullpath_list $fullpath
}
if {[llength $fullpath_list] != [llength $paths]} {
set path_error_display ""
foreach e $error_paths {
set p [lindex $e 0]
set m [lindex $e 1]
append path_error_display " Path: '$p' Error: $m\n"
}
error "punk::nav::fs::d/new One or more supplied paths were invalid or not writable:\n$path_error_display"
}
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
set num_created 0
set error_string ""
foreach fullpath $fullpath_list {
if {[catch {file mkdir $fullpath}]} {
set error_string "Failed to create directory '$fullpath' - processing of remaining paths aborted."
break
}
incr num_created
}
file mkdir $fullpath
d/ $fullpath
if {$error_string ne ""} {
error "punk::nav::fs::d/new $error_string\n$num_created directories out of [llength $fullpath_list] were created successfully before the error was encountered."
}
d/ $curdir
}
#todo use unknown to allow d/~c:/etc ??
@ -849,11 +971,11 @@ tcl::namespace::eval punk::nav::fs {
#file attr //cookit:/ returns {-vfs 1 -handle {}}
#we will treat it differently for now - use generic handler REVIEW
set in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
set is_in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
if {[llength [package provide vfs]]} {
foreach mount [vfs::filesystem info] {
if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
set is_in_vfs 1
break
}
}
@ -871,27 +993,27 @@ tcl::namespace::eval punk::nav::fs {
} else {
set next_opt_with_times [list -with_times $opt_with_times]
}
if {$in_vfs} {
if {$is_in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set in_zipfs 0
set in_cookit 1
set in_other_pseudovol 1
set invfs ""
switch -glob -- $location {
//zipfs:/* {
if {[info commands ::tcl::zipfs::mount] ne ""} {
set in_zipfs 1
set invfs zipfs
}
}
//cookit:/* {
set in_cookit 1
set invfs cookit
}
default {
#handle 'other/unknown' that mounts at a volume-like path //pseudovol:/
#(intentionally will not match a dos device path such as //?/c:/)
if {[regexp {//((?:(?!:|/).)+):/.*} $location _match pseudovol]} {
#pseudovol probably more than one char long
#we don't really expect something like //c:/ , but anyway, it's not the same as c:/ and for all we know someone could use that as a volume name?
set in_other_pseudovol 1 ;#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
set invfs pseudovol
} else {
#we could use 'file attr' here to test if {-vfs 1}
#but it's an extra filesystem hit on all normal paths too (which can be expensive on some systems)
@ -900,20 +1022,24 @@ tcl::namespace::eval punk::nav::fs {
}
}
if {$in_zipfs} {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_cookit} {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_other} {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
switch -- $invfs {
zipfs {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
cookit {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
pseudovol {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
default {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
}
}
@ -1018,11 +1144,13 @@ tcl::namespace::eval punk::nav::fs {
@id -id ::punk::nav::fs::dirfiles_dict_as_lines
-stripbase -default 0 -type boolean
-formatsizes -default 1 -type boolean
-listing -default "/" -choices {/ // //}
@values -min 1 -max -1 -type dict -unnamed true
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {args} {
set ts1 [clock milliseconds]
package require overtype
set argd [punk::args::parse $args withid ::punk::nav::fs::dirfiles_dict_as_lines]
lassign [dict values $argd] leaders opts vals
@ -1031,9 +1159,12 @@ tcl::namespace::eval punk::nav::fs {
# -- --- --- --- --- --- --- --- --- --- --- ---
set opt_stripbase [dict get $opts -stripbase]
set opt_stripbase [dict get $opts -stripbase]
set opt_formatsizes [dict get $opts -formatsizes]
set opt_listing [dict get $opts -listing]
# -- --- --- --- --- --- --- --- --- --- --- ---
#we still need to examine files for -listing / which means show only directories,
# because we want to display links/shortcuts that point to directories as directories
#if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied
set common_base ""
@ -1074,7 +1205,6 @@ tcl::namespace::eval punk::nav::fs {
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] {
set $fileset [list]
}
#set contents [lindex $list_of_dicts 0]
foreach contents $list_of_dicts {
lappend dirs {*}[dict get $contents dirs]
@ -1090,6 +1220,7 @@ tcl::namespace::eval punk::nav::fs {
lappend vfsmounts {*}[dict get $contents vfsmounts]
}
set fkeys [dict create] ;#avoid some file normalize calls..
if {$opt_stripbase && $common_base ne ""} {
set filetails [list]
@ -1224,27 +1355,41 @@ tcl::namespace::eval punk::nav::fs {
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
set ts2 [clock milliseconds]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::resolve $fname]
set target_type "file" ;#default/fallback
set shortcutinfo [punk::winlnk::resolve $fname]
if {[dict exists $shortcutinfo link_target]} {
set is_valid_lnk 1
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
set link_target_type [dict get $shortcutinfo target_type]
switch -- $link_target_type {
file {
set target_type "file"
}
directory - "local disk" {
set target_type "directory"
}
unknown {
#fall back to checking attributes and filesystem if we have a link_target but no target_type
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
} else {
#no link_target - probably an ordinary file - but there could have been some other error in reading the binary windows lnk format.
@ -1295,6 +1440,8 @@ tcl::namespace::eval punk::nav::fs {
}
unset finfo
puts stderr "dirfiles_dict_as_lines since ts2 [clock milliseconds] - $ts2 ms = [expr {[clock milliseconds] - $ts2}]"
puts stderr "dirfiles_dict_as_lines since start [clock milliseconds] - $ts1 ms = [expr {[clock milliseconds] - $ts1}]"
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -1304,58 +1451,82 @@ tcl::namespace::eval punk::nav::fs {
set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]]
set RST [punk::ansi::a]
if {$opt_listing eq "/"} {
#disply directories only (including items that were actually files that were links/shortcuts to directories)
set finfo_plus [list]
}
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold]
set f1 [punk::ansi::a+ white]
set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set d1 [punk::ansi::a+ cyan normal]
set d1_overrides [list]
#set d2 [punk::ansi::a+ defaultfg defaultbg normal]
set f1 [punk::ansi::a+ white normal]
set f1_overrides [list]
#set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set fdisp ""
if {[string length $d]} {
if {$d in $flaggedhidden} {
set d1 [punk::ansi::a+ cyan normal]
#set d1 [punk::ansi::a+ Term-grey50 normal]
lappend d1_overrides term-grey50
}
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red Yellow bold]
} else {
set d1 [punk::ansi::a+ green Purple bold]
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red White bold]
} else {
set d1 [punk::ansi::a+ green bold]
}
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red bold]
}
lappend d1_overrides Green
}
if {$d in $nonportable} {
#lappend d1_overrides underline undt-red ;#we use underlins to indicate symlinks and shortcuts, so we shouldn't use underlines here if possible.
lappend d1_overrides italic bold
}
#if {$d in $vfsmounts} {
# if {$d in $flaggedhidden} {
# #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indication of 'hidden' status - REVIEW
# #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
# #mark it differently for now.. (todo bug report?)
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red Yellow bold]
# } else {
# set d1 [punk::ansi::a+ green Purple bold]
# }
# } else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red White bold]
# } else {
# set d1 [punk::ansi::a+ green bold]
# }
# }
#} else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red bold]
# }
#}
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {[llength $d1_overrides]} {
set d1 [punk::ansi::a+ {*}$d1_overrides]
}
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
}
if {[llength $filerec]} {
set fname [dict get $filerec file]
set fdisp [dict get $filerec display]
if {$fname in $flaggedhidden} {
set f1 [punk::ansi::a+ Purple]
} else {
if {$fname in $nonportable} {
set f1 [punk::ansi::a+ red bold]
}
#set f1 [punk::ansi::a+ Term-grey50]
lappend f1_overrides term-grey50
}
if {$fname in $nonportable} {
lappend f1_overrides italic bold
}
if {[llength $f1_overrides]} {
set f1 [punk::ansi::a+ {*}$f1_overrides]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} else {
#either there are no files or opt_listing is / = show dirs only (some of which may have actually been files that were links/shortcuts to directories)
lappend displaylist [overtype::left $col1 $d1$d$RST]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
}
return [punk::lib::list_as_lines $displaylist]
@ -1469,6 +1640,12 @@ tcl::namespace::eval punk::nav::fs::system {
#[subsection {Namespace punk::nav::fs::system}]
#[para] Internal functions that are not part of the API
#utility function to copy values from one variable to another without sharing the reference.
#Useful for example to avoid some issues with possible shimmering of the underlying type of file paths.
proc valcopy {obj} {
append obj2 $obj {}
}
#ordinary emission of chunklist when no repl
proc emit_chunklist {chunklist} {
set result ""

1014
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/winlnk-0.1.1.tm

File diff suppressed because it is too large Load Diff

11
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/winpath-0.1.0.tm

@ -196,7 +196,8 @@ namespace eval punk::winpath {
#https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
#according to the above: Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
set reserved [list < > : \" / \\ | ? *]
#embedded nulls (\0) are also disallowed - but these are also disallowed on unix-like platforms.
set windows_reserved_names [list "CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6" "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7" "LPT8" "LPT9"]
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
@ -208,6 +209,14 @@ namespace eval punk::winpath {
#/./ /../ segments don't require protection - keep checking.
continue
}
if {[string toupper [file rootname $seg]] in $windows_reserved_names} {
#windows reserved names
#there are reports that these names aren't usable even with file extension - e.g that CON.txt is reserved and can't be created by some standard tools.
#In practice on windows 11 in 2026, cmd.exe,notepad,explorer and powershell seem to handle creation and access of CON.txt and PRN.txt etc without issue.
# the windows documentation reference above however still states that these names with an extension should be avoided.
#For this reason - we will still treat these as reserved and require protection with dos device syntax - even though in practice they seem to be usable without it.
return 1
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph

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

@ -1422,7 +1422,7 @@ namespace eval punk {
}
if {[string is digit -strict [join $subindices ""]]} {
#review tip 551 (tcl9+?)
#review tip 551 (underscores in numerical literals) (tcl9+)
#puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
#pure numeric keylist - put straight to lindex
#
@ -2650,6 +2650,76 @@ namespace eval punk {
}
}]
}
} elseif {[punk::lib::is_indexset $index]} {
#review - a basic math statement such as 5-1 is also a valid member of an indexset
#see punk::lib::is_indexset and punk::lib::indexset_resolve
#single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
set is_range [expr {[string first ".." $index] >= 0}]
if {$get_not} {
if {$is_range} {
lappend INDEX_OPERATIONS list-range-not
} else {
lappend INDEX_OPERATIONS listindex-not
}
set assign_script {
set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] <idx>]]
}
} else {
if {$is_range} {
lappend INDEX_OPERATIONS list-range
} else {
lappend INDEX_OPERATIONS listindex
}
set assign_script {
set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] <idx>] {lindex $leveldata $i}]
}
}
if {$do_bounds_check} {
#bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
if {$is_range} {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
lassign [split <idx> ..] idx1 _ idx2
set v2 [punk::lib::lindex_resolve_basic $len $idx2]
if {isinf($v2)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
set v1 [punk::lib::lindex_resolve_basic $len $idx1]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
set v1 [punk::lib::lindex_resolve_basic $len <idx>]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
}
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
${$assign_script}
}
}]
}
set script [string map [list <idx> $index] $script]
} elseif {[string first "end" $index] >=0} {
if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {

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

File diff suppressed because it is too large Load Diff

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

@ -229,12 +229,16 @@ tcl::namespace::eval punk::nav::fs {
} else {
set stripbase 1
}
if {$v eq "/"} {
#hack
dict set matchinfo files {}
dict set matchinfo filesizes {}
}
set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo]
#we need to pass matchinfo that includes files even when only doing a directory listing (d/ /)
#This is because we want to display links/shortcuts that point to directories as directories.
#( ./ listing needs to show navigable items)
#if {$v eq "/"} {
# #dodgy hack that doesn't give proper display of all links/shortcuts that are pointing to directories.
# dict set matchinfo files {}
# dict set matchinfo filesizes {}
#}
set out [dirfiles_dict_as_lines -listing $v -stripbase $stripbase $matchinfo]
#set chunklist [list]
#lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"]
set result "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"
@ -258,10 +262,10 @@ tcl::namespace::eval punk::nav::fs {
#puts stdout "-->[ansistring VIEW $result]"
return $result
} else {
set atail [lassign $args a1]
set atail [lassign $args cdtarget]
if {[llength $args] == 1} {
set a1 [lindex $args 0]
switch -exact -- $a1 {
set cdtarget [lindex $args 0]
switch -exact -- $cdtarget {
. - ./ {
tailcall punk::nav::fs::d/
}
@ -286,43 +290,88 @@ tcl::namespace::eval punk::nav::fs {
}
} else {
cd $up1
#set VIRTUAL_CWD [file normalize $a1]
#set VIRTUAL_CWD [file normalize $cdtarget]
}
tailcall punk::nav::fs::d/ $v
}
}
if {![regexp {[*?]} $a1] && [file pathtype $a1] ne "relative"} {
set cdtarget_copy [punk::nav::fs::system::valcopy $cdtarget]
set cdtarget_copy [string map {\\ /} $cdtarget_copy]
if {[string range $cdtarget_copy 0 3] eq "//?/"} {
#handle dos device paths - convert to normal path for glob testing
set glob_test [string range $cdtarget_copy 3 end]
set cdtarget_is_glob [regexp {[*?]} $glob_test]
} else {
set cdtarget_is_glob [regexp {[*?]} $cdtarget]
}
if {!$cdtarget_is_glob} {
set cdtarget_file_type [file type $cdtarget]
#e.g may be a link - whilst the type returned in the 'file stat' info reflects the type of the link target
} else {
set cdtarget_file_type "glob"
}
if {!$cdtarget_is_glob && [file pathtype $cdtarget] ne "relative"} {
#non-relative non-glob
if { ![string match //zipfs:/* $a1]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD $a1
tailcall punk::nav::fs::d/ $v
if {![string match //zipfs:/* $cdtarget]} {
switch -- $cdtarget_file_type {
link {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
}
if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD [file normalize $a1]
tailcall punk::nav::fs::d/ $v
if {!$cdtarget_is_glob && ![string match //zipfs:/* $cdtarget] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
switch -- $cdtarget_file_type {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
#if {[file type $cdtarget] eq "directory"} {
# cd $cdtarget
# #set VIRTUAL_CWD [file normalize $cdtarget]
# tailcall punk::nav::fs::d/ $v
#}
}
if {![regexp {[*?]} $a1]} {
if {!$cdtarget_is_glob} {
#NON-Glob target
#review
if {[string match //zipfs:/* $a1]} {
if {[Zipfs_path_within_zipfs_mounts $a1]} {
commandstack::basecall cd $a1
if {[string match //zipfs:/* $cdtarget]} {
if {[Zipfs_path_within_zipfs_mounts $cdtarget]} {
commandstack::basecall cd $cdtarget
}
set VIRTUAL_CWD $a1
set curdir $a1
set VIRTUAL_CWD $cdtarget
set curdir $cdtarget
} else {
set target [punk::path::normjoin $VIRTUAL_CWD $a1]
set target [punk::path::normjoin $VIRTUAL_CWD $cdtarget]
if {[string match //zipfs:/* $VIRTUAL_CWD]} {
if {[Zipfs_path_within_zipfs_mounts $target]} {
commandstack::basecall cd $target
@ -521,20 +570,93 @@ tcl::namespace::eval punk::nav::fs {
return $result
}
punk::args::define {
@id -id ::punk::nav::fs::d/new
-nonportable -type none -help\
"Allow creation of directories which may not be portable across platforms.
Use with caution and only when you know what you are doing.
This allows creation of directories with names that may be invalid on some
platforms, or that may have special meanings on some platforms
(e.g reserved device names on windows).
If -nonportable is not supplied, then an error will be raised if any supplied
path is non-portable as defined by punk::winpath::illegalname_test.
Regardless of whether -nonportable is supplied or not, some characters are not
suitable for windows or most other platforms and will be rejected with an error.
An example of this is the null character (\0)."
@values -min 1 -max -1 -type string
path -type string -multiple 1 -help\
"Path(s) to create. Can be absolute or relative.
If any path is rejected due to -nonportable or other invalid characters,
or because a parent directory is not writable, then no directories will be created.
If a path already exists, then it will be left as-is and no error will be raised.
If despite passing the name tests or writability tests, a directory cannot be
created for some reason (e.g other filesystem error) then an error will be raised
and processing of any remaining paths will be aborted."
}
#todo - synchronize overall behaviour of d/new with that of n/new (for namespaces)
proc d/new {args} {
if {![llength $args]} {
error "usage: d/new <dir> \[<dir> ...\]"
}
set a1 [lindex $args 0]
set argd [punk::args::parse $args withid ::punk::nav::fs::d/new]
lassign [dict values $argd] leaders opts values received
set paths [dict get $values path]
set allow_nonportable [dict exists $received -nonportable]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
set fullpath_list [list]
set error_paths [list]
foreach p $paths {
if {!$allow_nonportable && [punk::winpath::illegalname_test $p]} {
#error "punk::nav::fs::d/new Path '$p' is not portable and may not be created without -nonportable option"
lappend error_paths [list $p "Path '$p' is not portable and may not be created without -nonportable option"]
continue
}
if {[string first \0 $p] != -1} {
#error "punk::nav::fs::d/new Path '$p' contains null character which is not allowed"
lappend error_paths [list $p "Path '$p' contains null character which is not allowed"]
continue
}
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
#e.g can return something like //?/C:/test/illegalpath. which is not a valid path for mkdir.
set fullpath [file join $path1 {*}[lrange $args 1 end]]
#Some subpaths of the supplied paths to create may already exist.
#we should test write permissions on the nearest existing parent of the supplied path to create, rather than just on the supplied path itself which may not exist at all.
set parent [file dirname $fullpath]
while {![file exists $parent]} {
set parent [file dirname $parent]
}
if {![file writable $parent]} {
#error "punk::nav::fs::d/new Cannot create directory '$fullpath' as parent '$parent' is not writable"
lappend error_paths [list $fullpath "Cannot create directory '$fullpath' as parent '$parent' is not writable"]
continue
}
lappend fullpath_list $fullpath
}
if {[llength $fullpath_list] != [llength $paths]} {
set path_error_display ""
foreach e $error_paths {
set p [lindex $e 0]
set m [lindex $e 1]
append path_error_display " Path: '$p' Error: $m\n"
}
error "punk::nav::fs::d/new One or more supplied paths were invalid or not writable:\n$path_error_display"
}
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
set num_created 0
set error_string ""
foreach fullpath $fullpath_list {
if {[catch {file mkdir $fullpath}]} {
set error_string "Failed to create directory '$fullpath' - processing of remaining paths aborted."
break
}
incr num_created
}
file mkdir $fullpath
d/ $fullpath
if {$error_string ne ""} {
error "punk::nav::fs::d/new $error_string\n$num_created directories out of [llength $fullpath_list] were created successfully before the error was encountered."
}
d/ $curdir
}
#todo use unknown to allow d/~c:/etc ??
@ -849,11 +971,11 @@ tcl::namespace::eval punk::nav::fs {
#file attr //cookit:/ returns {-vfs 1 -handle {}}
#we will treat it differently for now - use generic handler REVIEW
set in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
set is_in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
if {[llength [package provide vfs]]} {
foreach mount [vfs::filesystem info] {
if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
set is_in_vfs 1
break
}
}
@ -871,27 +993,27 @@ tcl::namespace::eval punk::nav::fs {
} else {
set next_opt_with_times [list -with_times $opt_with_times]
}
if {$in_vfs} {
if {$is_in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set in_zipfs 0
set in_cookit 1
set in_other_pseudovol 1
set invfs ""
switch -glob -- $location {
//zipfs:/* {
if {[info commands ::tcl::zipfs::mount] ne ""} {
set in_zipfs 1
set invfs zipfs
}
}
//cookit:/* {
set in_cookit 1
set invfs cookit
}
default {
#handle 'other/unknown' that mounts at a volume-like path //pseudovol:/
#(intentionally will not match a dos device path such as //?/c:/)
if {[regexp {//((?:(?!:|/).)+):/.*} $location _match pseudovol]} {
#pseudovol probably more than one char long
#we don't really expect something like //c:/ , but anyway, it's not the same as c:/ and for all we know someone could use that as a volume name?
set in_other_pseudovol 1 ;#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
set invfs pseudovol
} else {
#we could use 'file attr' here to test if {-vfs 1}
#but it's an extra filesystem hit on all normal paths too (which can be expensive on some systems)
@ -900,20 +1022,24 @@ tcl::namespace::eval punk::nav::fs {
}
}
if {$in_zipfs} {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_cookit} {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_other} {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
switch -- $invfs {
zipfs {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
cookit {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
pseudovol {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
default {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
}
}
@ -1018,11 +1144,13 @@ tcl::namespace::eval punk::nav::fs {
@id -id ::punk::nav::fs::dirfiles_dict_as_lines
-stripbase -default 0 -type boolean
-formatsizes -default 1 -type boolean
-listing -default "/" -choices {/ // //}
@values -min 1 -max -1 -type dict -unnamed true
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {args} {
set ts1 [clock milliseconds]
package require overtype
set argd [punk::args::parse $args withid ::punk::nav::fs::dirfiles_dict_as_lines]
lassign [dict values $argd] leaders opts vals
@ -1031,9 +1159,12 @@ tcl::namespace::eval punk::nav::fs {
# -- --- --- --- --- --- --- --- --- --- --- ---
set opt_stripbase [dict get $opts -stripbase]
set opt_stripbase [dict get $opts -stripbase]
set opt_formatsizes [dict get $opts -formatsizes]
set opt_listing [dict get $opts -listing]
# -- --- --- --- --- --- --- --- --- --- --- ---
#we still need to examine files for -listing / which means show only directories,
# because we want to display links/shortcuts that point to directories as directories
#if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied
set common_base ""
@ -1074,7 +1205,6 @@ tcl::namespace::eval punk::nav::fs {
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] {
set $fileset [list]
}
#set contents [lindex $list_of_dicts 0]
foreach contents $list_of_dicts {
lappend dirs {*}[dict get $contents dirs]
@ -1090,6 +1220,7 @@ tcl::namespace::eval punk::nav::fs {
lappend vfsmounts {*}[dict get $contents vfsmounts]
}
set fkeys [dict create] ;#avoid some file normalize calls..
if {$opt_stripbase && $common_base ne ""} {
set filetails [list]
@ -1224,27 +1355,41 @@ tcl::namespace::eval punk::nav::fs {
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
set ts2 [clock milliseconds]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::resolve $fname]
set target_type "file" ;#default/fallback
set shortcutinfo [punk::winlnk::resolve $fname]
if {[dict exists $shortcutinfo link_target]} {
set is_valid_lnk 1
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
set link_target_type [dict get $shortcutinfo target_type]
switch -- $link_target_type {
file {
set target_type "file"
}
directory - "local disk" {
set target_type "directory"
}
unknown {
#fall back to checking attributes and filesystem if we have a link_target but no target_type
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
} else {
#no link_target - probably an ordinary file - but there could have been some other error in reading the binary windows lnk format.
@ -1295,6 +1440,8 @@ tcl::namespace::eval punk::nav::fs {
}
unset finfo
puts stderr "dirfiles_dict_as_lines since ts2 [clock milliseconds] - $ts2 ms = [expr {[clock milliseconds] - $ts2}]"
puts stderr "dirfiles_dict_as_lines since start [clock milliseconds] - $ts1 ms = [expr {[clock milliseconds] - $ts1}]"
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -1304,58 +1451,82 @@ tcl::namespace::eval punk::nav::fs {
set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]]
set RST [punk::ansi::a]
if {$opt_listing eq "/"} {
#disply directories only (including items that were actually files that were links/shortcuts to directories)
set finfo_plus [list]
}
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold]
set f1 [punk::ansi::a+ white]
set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set d1 [punk::ansi::a+ cyan normal]
set d1_overrides [list]
#set d2 [punk::ansi::a+ defaultfg defaultbg normal]
set f1 [punk::ansi::a+ white normal]
set f1_overrides [list]
#set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set fdisp ""
if {[string length $d]} {
if {$d in $flaggedhidden} {
set d1 [punk::ansi::a+ cyan normal]
#set d1 [punk::ansi::a+ Term-grey50 normal]
lappend d1_overrides term-grey50
}
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red Yellow bold]
} else {
set d1 [punk::ansi::a+ green Purple bold]
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red White bold]
} else {
set d1 [punk::ansi::a+ green bold]
}
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red bold]
}
lappend d1_overrides Green
}
if {$d in $nonportable} {
#lappend d1_overrides underline undt-red ;#we use underlins to indicate symlinks and shortcuts, so we shouldn't use underlines here if possible.
lappend d1_overrides italic bold
}
#if {$d in $vfsmounts} {
# if {$d in $flaggedhidden} {
# #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indication of 'hidden' status - REVIEW
# #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
# #mark it differently for now.. (todo bug report?)
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red Yellow bold]
# } else {
# set d1 [punk::ansi::a+ green Purple bold]
# }
# } else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red White bold]
# } else {
# set d1 [punk::ansi::a+ green bold]
# }
# }
#} else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red bold]
# }
#}
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {[llength $d1_overrides]} {
set d1 [punk::ansi::a+ {*}$d1_overrides]
}
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
}
if {[llength $filerec]} {
set fname [dict get $filerec file]
set fdisp [dict get $filerec display]
if {$fname in $flaggedhidden} {
set f1 [punk::ansi::a+ Purple]
} else {
if {$fname in $nonportable} {
set f1 [punk::ansi::a+ red bold]
}
#set f1 [punk::ansi::a+ Term-grey50]
lappend f1_overrides term-grey50
}
if {$fname in $nonportable} {
lappend f1_overrides italic bold
}
if {[llength $f1_overrides]} {
set f1 [punk::ansi::a+ {*}$f1_overrides]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} else {
#either there are no files or opt_listing is / = show dirs only (some of which may have actually been files that were links/shortcuts to directories)
lappend displaylist [overtype::left $col1 $d1$d$RST]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
}
return [punk::lib::list_as_lines $displaylist]
@ -1469,6 +1640,12 @@ tcl::namespace::eval punk::nav::fs::system {
#[subsection {Namespace punk::nav::fs::system}]
#[para] Internal functions that are not part of the API
#utility function to copy values from one variable to another without sharing the reference.
#Useful for example to avoid some issues with possible shimmering of the underlying type of file paths.
proc valcopy {obj} {
append obj2 $obj {}
}
#ordinary emission of chunklist when no repl
proc emit_chunklist {chunklist} {
set result ""

1014
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/winlnk-0.1.1.tm

File diff suppressed because it is too large Load Diff

11
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/winpath-0.1.0.tm

@ -196,7 +196,8 @@ namespace eval punk::winpath {
#https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
#according to the above: Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
set reserved [list < > : \" / \\ | ? *]
#embedded nulls (\0) are also disallowed - but these are also disallowed on unix-like platforms.
set windows_reserved_names [list "CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6" "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7" "LPT8" "LPT9"]
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
@ -208,6 +209,14 @@ namespace eval punk::winpath {
#/./ /../ segments don't require protection - keep checking.
continue
}
if {[string toupper [file rootname $seg]] in $windows_reserved_names} {
#windows reserved names
#there are reports that these names aren't usable even with file extension - e.g that CON.txt is reserved and can't be created by some standard tools.
#In practice on windows 11 in 2026, cmd.exe,notepad,explorer and powershell seem to handle creation and access of CON.txt and PRN.txt etc without issue.
# the windows documentation reference above however still states that these names with an extension should be avoided.
#For this reason - we will still treat these as reserved and require protection with dos device syntax - even though in practice they seem to be usable without it.
return 1
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph

72
src/vfs/_vfscommon.vfs/modules/punk-0.1.tm

@ -1422,7 +1422,7 @@ namespace eval punk {
}
if {[string is digit -strict [join $subindices ""]]} {
#review tip 551 (tcl9+?)
#review tip 551 (underscores in numerical literals) (tcl9+)
#puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
#pure numeric keylist - put straight to lindex
#
@ -2650,6 +2650,76 @@ namespace eval punk {
}
}]
}
} elseif {[punk::lib::is_indexset $index]} {
#review - a basic math statement such as 5-1 is also a valid member of an indexset
#see punk::lib::is_indexset and punk::lib::indexset_resolve
#single element of an indexset - e.g @..3 or @1..5 or @..end or @.. or @end..0 or @end-5..8 etc
set is_range [expr {[string first ".." $index] >= 0}]
if {$get_not} {
if {$is_range} {
lappend INDEX_OPERATIONS list-range-not
} else {
lappend INDEX_OPERATIONS listindex-not
}
set assign_script {
set assigned [lremove $assigned {*}[punk::lib::indexset_resolve [llength $leveldata] <idx>]]
}
} else {
if {$is_range} {
lappend INDEX_OPERATIONS list-range
} else {
lappend INDEX_OPERATIONS listindex
}
set assign_script {
set assigned [lmap i [punk::lib::indexset_resolve [llength $leveldata] <idx>] {lindex $leveldata $i}]
}
}
if {$do_bounds_check} {
#bounds check each element of the resolved indexset - if any are out of bounds, return mismatch-list-index-out-of-range
if {$is_range} {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
lassign [split <idx> ..] idx1 _ idx2
set v2 [punk::lib::lindex_resolve_basic $len $idx2]
if {isinf($v2)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
set v1 [punk::lib::lindex_resolve_basic $len $idx1]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
set v1 [punk::lib::lindex_resolve_basic $len <idx>]
if {isinf($v1)} {
${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
}
${$assign_script}
}
}]
}
} else {
append script \n [tstr -return string -allowcommands {
if {[catch {llength $leveldata} len]} {
#set action ?mismatch-not-a-list
${[tstr -ret string $tpl_return_mismatch_not_a_list]}
} else {
${$assign_script}
}
}]
}
set script [string map [list <idx> $index] $script]
} elseif {[string first "end" $index] >=0} {
if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {

5488
src/vfs/_vfscommon.vfs/modules/punk/lib-0.1.6.tm

File diff suppressed because it is too large Load Diff

391
src/vfs/_vfscommon.vfs/modules/punk/nav/fs-0.1.0.tm

@ -229,12 +229,16 @@ tcl::namespace::eval punk::nav::fs {
} else {
set stripbase 1
}
if {$v eq "/"} {
#hack
dict set matchinfo files {}
dict set matchinfo filesizes {}
}
set out [dirfiles_dict_as_lines -stripbase $stripbase $matchinfo]
#we need to pass matchinfo that includes files even when only doing a directory listing (d/ /)
#This is because we want to display links/shortcuts that point to directories as directories.
#( ./ listing needs to show navigable items)
#if {$v eq "/"} {
# #dodgy hack that doesn't give proper display of all links/shortcuts that are pointing to directories.
# dict set matchinfo files {}
# dict set matchinfo filesizes {}
#}
set out [dirfiles_dict_as_lines -listing $v -stripbase $stripbase $matchinfo]
#set chunklist [list]
#lappend chunklist [list stdout "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"]
set result "[punk::ansi::a+ brightwhite]$out[punk::ansi::a]\n"
@ -258,10 +262,10 @@ tcl::namespace::eval punk::nav::fs {
#puts stdout "-->[ansistring VIEW $result]"
return $result
} else {
set atail [lassign $args a1]
set atail [lassign $args cdtarget]
if {[llength $args] == 1} {
set a1 [lindex $args 0]
switch -exact -- $a1 {
set cdtarget [lindex $args 0]
switch -exact -- $cdtarget {
. - ./ {
tailcall punk::nav::fs::d/
}
@ -286,43 +290,88 @@ tcl::namespace::eval punk::nav::fs {
}
} else {
cd $up1
#set VIRTUAL_CWD [file normalize $a1]
#set VIRTUAL_CWD [file normalize $cdtarget]
}
tailcall punk::nav::fs::d/ $v
}
}
if {![regexp {[*?]} $a1] && [file pathtype $a1] ne "relative"} {
set cdtarget_copy [punk::nav::fs::system::valcopy $cdtarget]
set cdtarget_copy [string map {\\ /} $cdtarget_copy]
if {[string range $cdtarget_copy 0 3] eq "//?/"} {
#handle dos device paths - convert to normal path for glob testing
set glob_test [string range $cdtarget_copy 3 end]
set cdtarget_is_glob [regexp {[*?]} $glob_test]
} else {
set cdtarget_is_glob [regexp {[*?]} $cdtarget]
}
if {!$cdtarget_is_glob} {
set cdtarget_file_type [file type $cdtarget]
#e.g may be a link - whilst the type returned in the 'file stat' info reflects the type of the link target
} else {
set cdtarget_file_type "glob"
}
if {!$cdtarget_is_glob && [file pathtype $cdtarget] ne "relative"} {
#non-relative non-glob
if { ![string match //zipfs:/* $a1]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD $a1
tailcall punk::nav::fs::d/ $v
if {![string match //zipfs:/* $cdtarget]} {
switch -- $cdtarget_file_type {
link {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
}
}
if {![regexp {[*?]} $a1] && ![string match //zipfs:/* $a1] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
if {[file type $a1] eq "directory"} {
cd $a1
#set VIRTUAL_CWD [file normalize $a1]
tailcall punk::nav::fs::d/ $v
if {!$cdtarget_is_glob && ![string match //zipfs:/* $cdtarget] && ![string match "//zipfs:/*" $VIRTUAL_CWD]} {
switch -- $cdtarget_file_type {
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
}
}
directory {
cd $cdtarget
#set VIRTUAL_CWD $cdtarget
tailcall punk::nav::fs::d/ $v
}
}
#if {[file type $cdtarget] eq "directory"} {
# cd $cdtarget
# #set VIRTUAL_CWD [file normalize $cdtarget]
# tailcall punk::nav::fs::d/ $v
#}
}
if {![regexp {[*?]} $a1]} {
if {!$cdtarget_is_glob} {
#NON-Glob target
#review
if {[string match //zipfs:/* $a1]} {
if {[Zipfs_path_within_zipfs_mounts $a1]} {
commandstack::basecall cd $a1
if {[string match //zipfs:/* $cdtarget]} {
if {[Zipfs_path_within_zipfs_mounts $cdtarget]} {
commandstack::basecall cd $cdtarget
}
set VIRTUAL_CWD $a1
set curdir $a1
set VIRTUAL_CWD $cdtarget
set curdir $cdtarget
} else {
set target [punk::path::normjoin $VIRTUAL_CWD $a1]
set target [punk::path::normjoin $VIRTUAL_CWD $cdtarget]
if {[string match //zipfs:/* $VIRTUAL_CWD]} {
if {[Zipfs_path_within_zipfs_mounts $target]} {
commandstack::basecall cd $target
@ -521,20 +570,93 @@ tcl::namespace::eval punk::nav::fs {
return $result
}
punk::args::define {
@id -id ::punk::nav::fs::d/new
-nonportable -type none -help\
"Allow creation of directories which may not be portable across platforms.
Use with caution and only when you know what you are doing.
This allows creation of directories with names that may be invalid on some
platforms, or that may have special meanings on some platforms
(e.g reserved device names on windows).
If -nonportable is not supplied, then an error will be raised if any supplied
path is non-portable as defined by punk::winpath::illegalname_test.
Regardless of whether -nonportable is supplied or not, some characters are not
suitable for windows or most other platforms and will be rejected with an error.
An example of this is the null character (\0)."
@values -min 1 -max -1 -type string
path -type string -multiple 1 -help\
"Path(s) to create. Can be absolute or relative.
If any path is rejected due to -nonportable or other invalid characters,
or because a parent directory is not writable, then no directories will be created.
If a path already exists, then it will be left as-is and no error will be raised.
If despite passing the name tests or writability tests, a directory cannot be
created for some reason (e.g other filesystem error) then an error will be raised
and processing of any remaining paths will be aborted."
}
#todo - synchronize overall behaviour of d/new with that of n/new (for namespaces)
proc d/new {args} {
if {![llength $args]} {
error "usage: d/new <dir> \[<dir> ...\]"
}
set a1 [lindex $args 0]
set argd [punk::args::parse $args withid ::punk::nav::fs::d/new]
lassign [dict values $argd] leaders opts values received
set paths [dict get $values path]
set allow_nonportable [dict exists $received -nonportable]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
set fullpath_list [list]
set error_paths [list]
foreach p $paths {
if {!$allow_nonportable && [punk::winpath::illegalname_test $p]} {
#error "punk::nav::fs::d/new Path '$p' is not portable and may not be created without -nonportable option"
lappend error_paths [list $p "Path '$p' is not portable and may not be created without -nonportable option"]
continue
}
if {[string first \0 $p] != -1} {
#error "punk::nav::fs::d/new Path '$p' contains null character which is not allowed"
lappend error_paths [list $p "Path '$p' contains null character which is not allowed"]
continue
}
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
#e.g can return something like //?/C:/test/illegalpath. which is not a valid path for mkdir.
set fullpath [file join $path1 {*}[lrange $args 1 end]]
#Some subpaths of the supplied paths to create may already exist.
#we should test write permissions on the nearest existing parent of the supplied path to create, rather than just on the supplied path itself which may not exist at all.
set parent [file dirname $fullpath]
while {![file exists $parent]} {
set parent [file dirname $parent]
}
if {![file writable $parent]} {
#error "punk::nav::fs::d/new Cannot create directory '$fullpath' as parent '$parent' is not writable"
lappend error_paths [list $fullpath "Cannot create directory '$fullpath' as parent '$parent' is not writable"]
continue
}
lappend fullpath_list $fullpath
}
if {[llength $fullpath_list] != [llength $paths]} {
set path_error_display ""
foreach e $error_paths {
set p [lindex $e 0]
set m [lindex $e 1]
append path_error_display " Path: '$p' Error: $m\n"
}
error "punk::nav::fs::d/new One or more supplied paths were invalid or not writable:\n$path_error_display"
}
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
set num_created 0
set error_string ""
foreach fullpath $fullpath_list {
if {[catch {file mkdir $fullpath}]} {
set error_string "Failed to create directory '$fullpath' - processing of remaining paths aborted."
break
}
incr num_created
}
file mkdir $fullpath
d/ $fullpath
if {$error_string ne ""} {
error "punk::nav::fs::d/new $error_string\n$num_created directories out of [llength $fullpath_list] were created successfully before the error was encountered."
}
d/ $curdir
}
#todo use unknown to allow d/~c:/etc ??
@ -849,11 +971,11 @@ tcl::namespace::eval punk::nav::fs {
#file attr //cookit:/ returns {-vfs 1 -handle {}}
#we will treat it differently for now - use generic handler REVIEW
set in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
set is_in_vfs 0 ;#we use this only for a vfs which is reported to have a mountpoint by vfs::filesystem info - not all that have -vfs 1 attr like cookit.
if {[llength [package provide vfs]]} {
foreach mount [vfs::filesystem info] {
if {[punk::mix::base::lib::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
set is_in_vfs 1
break
}
}
@ -871,27 +993,27 @@ tcl::namespace::eval punk::nav::fs {
} else {
set next_opt_with_times [list -with_times $opt_with_times]
}
if {$in_vfs} {
if {$is_in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set in_zipfs 0
set in_cookit 1
set in_other_pseudovol 1
set invfs ""
switch -glob -- $location {
//zipfs:/* {
if {[info commands ::tcl::zipfs::mount] ne ""} {
set in_zipfs 1
set invfs zipfs
}
}
//cookit:/* {
set in_cookit 1
set invfs cookit
}
default {
#handle 'other/unknown' that mounts at a volume-like path //pseudovol:/
#(intentionally will not match a dos device path such as //?/c:/)
if {[regexp {//((?:(?!:|/).)+):/.*} $location _match pseudovol]} {
#pseudovol probably more than one char long
#we don't really expect something like //c:/ , but anyway, it's not the same as c:/ and for all we know someone could use that as a volume name?
set in_other_pseudovol 1 ;#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
#flag so we don't use twapi - hope generic can handle it (uses tcl glob)
set invfs pseudovol
} else {
#we could use 'file attr' here to test if {-vfs 1}
#but it's an extra filesystem hit on all normal paths too (which can be expensive on some systems)
@ -900,20 +1022,24 @@ tcl::namespace::eval punk::nav::fs {
}
}
if {$in_zipfs} {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_cookit} {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} elseif {$in_other} {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
} else {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
switch -- $invfs {
zipfs {
#relative vs absolute? review - cwd valid for //zipfs:/ ??
set listing [punk::du::lib::du_dirlisting_zipfs $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
cookit {
#seems to be a vfs - except it mounts on a pseudo-volume path //cookit:/
#don't use twapi
#could possibly use du_dirlisting_tclvfs REVIEW
#files and folders are all returned with the -types hidden option for glob on windows
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
pseudovol {
set listing [punk::du::lib::du_dirlisting_generic $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
default {
set listing [punk::du::dirlisting $location -glob $match_contents {*}$next_opt_with_sizes {*}$next_opt_with_times]
}
}
}
@ -1018,11 +1144,13 @@ tcl::namespace::eval punk::nav::fs {
@id -id ::punk::nav::fs::dirfiles_dict_as_lines
-stripbase -default 0 -type boolean
-formatsizes -default 1 -type boolean
-listing -default "/" -choices {/ // //}
@values -min 1 -max -1 -type dict -unnamed true
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {args} {
set ts1 [clock milliseconds]
package require overtype
set argd [punk::args::parse $args withid ::punk::nav::fs::dirfiles_dict_as_lines]
lassign [dict values $argd] leaders opts vals
@ -1031,9 +1159,12 @@ tcl::namespace::eval punk::nav::fs {
# -- --- --- --- --- --- --- --- --- --- --- ---
set opt_stripbase [dict get $opts -stripbase]
set opt_stripbase [dict get $opts -stripbase]
set opt_formatsizes [dict get $opts -formatsizes]
set opt_listing [dict get $opts -listing]
# -- --- --- --- --- --- --- --- --- --- --- ---
#we still need to examine files for -listing / which means show only directories,
# because we want to display links/shortcuts that point to directories as directories
#if multiple dicts and -stripbase = 1 - we can only strip the longest common part of the searchbases supplied
set common_base ""
@ -1074,7 +1205,6 @@ tcl::namespace::eval punk::nav::fs {
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedreadonly flaggedsystem nonportable vfsmounts] {
set $fileset [list]
}
#set contents [lindex $list_of_dicts 0]
foreach contents $list_of_dicts {
lappend dirs {*}[dict get $contents dirs]
@ -1090,6 +1220,7 @@ tcl::namespace::eval punk::nav::fs {
lappend vfsmounts {*}[dict get $contents vfsmounts]
}
set fkeys [dict create] ;#avoid some file normalize calls..
if {$opt_stripbase && $common_base ne ""} {
set filetails [list]
@ -1224,27 +1355,41 @@ tcl::namespace::eval punk::nav::fs {
#review - symlink to shortcut? hopefully will just work
#classify as file or directory - fallback to file if unknown/undeterminable
set finfo_plus [list]
set ts2 [clock milliseconds]
foreach fdict $finfo {
set fname [dict get $fdict file]
if {[file extension $fname] eq ".lnk"} {
if {![catch {package require punk::winlnk}]} {
set shortcutinfo [punk::winlnk::resolve $fname]
set target_type "file" ;#default/fallback
set shortcutinfo [punk::winlnk::resolve $fname]
if {[dict exists $shortcutinfo link_target]} {
set is_valid_lnk 1
set tgt [dict get $shortcutinfo link_target]
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
set link_target_type [dict get $shortcutinfo target_type]
switch -- $link_target_type {
file {
set target_type "file"
}
directory - "local disk" {
set target_type "directory"
}
unknown {
#fall back to checking attributes and filesystem if we have a link_target but no target_type
if {[file exists $tgt]} {
#file type could return 'link' - we will use isfile/isdirectory
if {[file isfile $tgt]} {
set target_type file
} elseif {[file isdirectory $tgt]} {
set target_type directory
} else {
set target_type file ;## ?
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
}
} else {
#todo - see if punk::winlnk has info about the type at the time of linking
#for now - treat as file
}
} else {
#no link_target - probably an ordinary file - but there could have been some other error in reading the binary windows lnk format.
@ -1295,6 +1440,8 @@ tcl::namespace::eval punk::nav::fs {
}
unset finfo
puts stderr "dirfiles_dict_as_lines since ts2 [clock milliseconds] - $ts2 ms = [expr {[clock milliseconds] - $ts2}]"
puts stderr "dirfiles_dict_as_lines since start [clock milliseconds] - $ts1 ms = [expr {[clock milliseconds] - $ts1}]"
#set widest1 [punk::pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -1304,58 +1451,82 @@ tcl::namespace::eval punk::nav::fs {
set displaylist [list]
set col1 [string repeat " " [expr {$widest1 + 2}]]
set RST [punk::ansi::a]
if {$opt_listing eq "/"} {
#disply directories only (including items that were actually files that were links/shortcuts to directories)
set finfo_plus [list]
}
foreach d $dirs filerec $finfo_plus {
set d1 [punk::ansi::a+ cyan bold]
set d2 [punk::ansi::a+ defaultfg defaultbg normal]
#set f1 [punk::ansi::a+ white bold]
set f1 [punk::ansi::a+ white]
set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set d1 [punk::ansi::a+ cyan normal]
set d1_overrides [list]
#set d2 [punk::ansi::a+ defaultfg defaultbg normal]
set f1 [punk::ansi::a+ white normal]
set f1_overrides [list]
#set f2 [punk::ansi::a+ defaultfg defaultbg normal]
set fdisp ""
if {[string length $d]} {
if {$d in $flaggedhidden} {
set d1 [punk::ansi::a+ cyan normal]
#set d1 [punk::ansi::a+ Term-grey50 normal]
lappend d1_overrides term-grey50
}
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. colour will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red Yellow bold]
} else {
set d1 [punk::ansi::a+ green Purple bold]
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red White bold]
} else {
set d1 [punk::ansi::a+ green bold]
}
}
} else {
if {$d in $nonportable} {
set d1 [punk::ansi::a+ red bold]
}
lappend d1_overrides Green
}
if {$d in $nonportable} {
#lappend d1_overrides underline undt-red ;#we use underlins to indicate symlinks and shortcuts, so we shouldn't use underlines here if possible.
lappend d1_overrides italic bold
}
#if {$d in $vfsmounts} {
# if {$d in $flaggedhidden} {
# #we could have a hidden dir which is also a vfs.. colour will be overridden giving no indication of 'hidden' status - REVIEW
# #(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
# #mark it differently for now.. (todo bug report?)
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red Yellow bold]
# } else {
# set d1 [punk::ansi::a+ green Purple bold]
# }
# } else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red White bold]
# } else {
# set d1 [punk::ansi::a+ green bold]
# }
# }
#} else {
# if {$d in $nonportable} {
# set d1 [punk::ansi::a+ red bold]
# }
#}
#dlink-style & dshortcut_style are for underlines - can be added with colours already set
if {[llength $d1_overrides]} {
set d1 [punk::ansi::a+ {*}$d1_overrides]
}
if {$d in $dir_symlinks} {
append d1 $dlink_style
} elseif {$d in $dir_shortcuts} {
append d1 $dshortcut_style
}
}
if {[llength $filerec]} {
set fname [dict get $filerec file]
set fdisp [dict get $filerec display]
if {$fname in $flaggedhidden} {
set f1 [punk::ansi::a+ Purple]
} else {
if {$fname in $nonportable} {
set f1 [punk::ansi::a+ red bold]
}
#set f1 [punk::ansi::a+ Term-grey50]
lappend f1_overrides term-grey50
}
if {$fname in $nonportable} {
lappend f1_overrides italic bold
}
if {[llength $f1_overrides]} {
set f1 [punk::ansi::a+ {*}$f1_overrides]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
} else {
#either there are no files or opt_listing is / = show dirs only (some of which may have actually been files that were links/shortcuts to directories)
lappend displaylist [overtype::left $col1 $d1$d$RST]
}
lappend displaylist [overtype::left $col1 $d1$d$RST]$f1$fdisp$RST
}
return [punk::lib::list_as_lines $displaylist]
@ -1469,6 +1640,12 @@ tcl::namespace::eval punk::nav::fs::system {
#[subsection {Namespace punk::nav::fs::system}]
#[para] Internal functions that are not part of the API
#utility function to copy values from one variable to another without sharing the reference.
#Useful for example to avoid some issues with possible shimmering of the underlying type of file paths.
proc valcopy {obj} {
append obj2 $obj {}
}
#ordinary emission of chunklist when no repl
proc emit_chunklist {chunklist} {
set result ""

1014
src/vfs/_vfscommon.vfs/modules/punk/winlnk-0.1.1.tm

File diff suppressed because it is too large Load Diff

11
src/vfs/_vfscommon.vfs/modules/punk/winpath-0.1.0.tm

@ -196,7 +196,8 @@ namespace eval punk::winpath {
#https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
#according to the above: Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
set reserved [list < > : \" / \\ | ? *]
#embedded nulls (\0) are also disallowed - but these are also disallowed on unix-like platforms.
set windows_reserved_names [list "CON" "PRN" "AUX" "NUL" "COM1" "COM2" "COM3" "COM4" "COM5" "COM6" "COM7" "COM8" "COM9" "LPT1" "LPT2" "LPT3" "LPT4" "LPT5" "LPT6" "LPT7" "LPT8" "LPT9"]
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
@ -208,6 +209,14 @@ namespace eval punk::winpath {
#/./ /../ segments don't require protection - keep checking.
continue
}
if {[string toupper [file rootname $seg]] in $windows_reserved_names} {
#windows reserved names
#there are reports that these names aren't usable even with file extension - e.g that CON.txt is reserved and can't be created by some standard tools.
#In practice on windows 11 in 2026, cmd.exe,notepad,explorer and powershell seem to handle creation and access of CON.txt and PRN.txt etc without issue.
# the windows documentation reference above however still states that these names with an extension should be avoided.
#For this reason - we will still treat these as reserved and require protection with dos device syntax - even though in practice they seem to be usable without it.
return 1
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph

Loading…
Cancel
Save