return [expr {[dict size $o_data] - 1}] ;#return index of item
}
method remove {idx {endRange ""}} {
if {[string length $endRange]} {
error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time"
}
if {[string is integer -strict $idx]} {
if {$idx < 0} {
set idx "end-[expr {abs($idx+1)}]"
}
set key [lindex [dict keys $o_data] $idx]
set posn $idx
} else {
set key $idx
set posn [lsearch -exact [dict keys $o_data] $key]
if {$posn < 0} {
error "[self object] no such index: '$idx' in this collection"
}
}
dict unset o_data $key
return
}
method clear {} {
set o_data [dict create]
return
}
method reverse_the_collection {} {
#named slightly obtusely because reversing the data when there may be references held is a potential source of bugs
#the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy.
#todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations.
#intended for single grapheme - but will work for multiple
#cannot contain ansi or newlines
#(a cache of ansifreestring_width calls - as these are quite regex heavy)
proc grapheme_width_cached {ch} {
#review - effective memory leak on longrunning programs if never cleared
#tradeoff in fragmenting cache and reducing efficiency vs ability to clear in a scoped manner
proc grapheme_width_cached {ch {key ""}} {
variable grapheme_widths
if {[dict exists $grapheme_widths $ch]} {
return [dict get $grapheme_widths $ch]
#if key eq "*" - we won't be able to clear that cache individually. Perhaps that's ok
if {[dict exists $grapheme_widths $key $ch]} {
return [dict get $grapheme_widths $key $ch]
}
set width [punk::char::ansifreestring_width $ch] ;#review - can we provide faster version if we know it's a single grapheme rather than a string? (grapheme is still a string as it may have combiners/diacritics)
dict set grapheme_widths $ch $width
dict set grapheme_widths $key $ch $width
return $width
}
proc grapheme_width_cache_clear {key} {
variable grapheme_widths
if {$key eq "*} {
set grapheme_widths [dict create]
} else {
dict unset grapheme_widths $key
}
return
}
#no char_width - use grapheme_width terminology to be clearer
proc grapheme_width {char} {
error "grapheme_width unimplemented - use ansifreestring_width"
interp alias {} lpop {} ::punk::lib::compat::lmaptcl
}
#lmap came in Tcl 8.6 - so probably not much need for a tcl forward compatibility version - but here it is anyway
proc lmaptcl {varnames list script} {
set result [list]
set varlist [list]
foreach varname $varnames {
upvar 1 $varname var_$varname ;#ensure no collisions with vars in this proc
lappend varlist var_$varname
}
foreach $varlist $list {
lappend result [uplevel 1 $script]
}
return $result
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::lib::compat ---}]
@ -196,6 +244,99 @@ namespace eval punk::lib {
#[para] Core API functions for punk::lib
#[list_begin definitions]
#The closure-like behaviour is *very* slow especially when called from a context such as the global namespace with lots of vars and large arrays such as ::env
proc lmapflat_closure {varnames list script} {
set result [list]
set values [list]
foreach v $varnames {
lappend values "\$$v"
}
# -- --- ---
#capture - use uplevel 1 or namespace eval depending on context
set capture [uplevel 1 {
apply { varnames {
set capturevars [dict create]
set capturearrs [dict create]
foreach fullv $varnames {
set v [namespace tail $fullv]
upvar 1 $v var
if {[info exists var]} {
if {(![array exists var])} {
dict set capturevars $v $var
} else {
dict set capturearrs capturedarray_$v [array get var]
}
} else {
#A variable can show in the results for 'info vars' but still not 'exist'. e.g a 'variable x' declaration in the namespace where the variable has never been set
}
}
return [dict create vars $capturevars arrs $capturearrs]
} } [info vars]
} ]
# -- --- ---
set cvars [dict get $capture vars]
set carrs [dict get $capture arrs]
set apply_script ""
foreach arrayalias [dict keys $carrs] {
set realname [string range $arrayalias [string first _ $arrayalias]+1 end]
append out "${indent}Extended underlines/colours can suppress other SGR codes on terminals that don't support them if codes are merged." \n
append out "${indent}punk::ansi tries to keep them in separate escape sequences even during merge operations to avoid this" \n
append out "[a+ web-white]Standard SGR colours and attributes $RST" \n
append out "${indent}punk::ansi tries to keep them in separate escape sequences (standard SGR followed by extended) even during merge operations to avoid this." \n
append out "${indent}If a fallback to standard underline is required, underline should be added along with extended codes such as underlinedotted, underlinedouble etc" \n
append out "${indent}e.g cyan with curly yellow underline or fallback all cyan underlined \[a+ cyan undercurly underline undt-yellow\]text\[a] -> [a+ {*}$fc cyan undercurly underline undt-yellow]text$RST" \n
append out "[a+ {*}$fc web-white]Standard SGR colours and attributes $RST" \n
append out [textblock::join $indent "Examine a sequence: a? bold rgb-46-139-87 Rgb#C71585 "] \n
append out \n
append out "[a+ web-white]Web colours[a]" \n
append out "[a+ {*}$fc web-white]Web colours[a]" \n
append out [textblock::join $indent "To see all names use: a? web"] \n
append out [textblock::join $indent "To see specific colour groups use: a? web groupname1 groupname2..."] \n
append out [textblock::join $indent "Valid group names (can be listed in any order): basic pink red orange yellow brown purple blue cyan green white grey"] \n
#whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache clear
if {[info exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} {
set colour_disabled 1
}
#allow a mechanism to override the colour_disabled terminal preference - for code that is generating colour for something else - don't let no_color ruin everything.
set forcecolour 0
set fcpos [lsearch $args "forcecol*"] ;#allow forcecolor forcecolour
if {$fcpos >= 0} {
set forcecolour 1
set args [lremove $args $fcpos]
}
set t [list]
set e [list] ;#extended codes needing to go in own escape sequence
#whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache clear
if {[info exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} {
set colour_disabled 1
}
#allow a mechanism to override the colour_disabled terminal preference - for code that is generating colour for something else - don't let no_color ruin everything.
set forcecolour 0
set fcpos [lsearch $args "forcecol*"] ;#allow forcecolor forcecolour
if {$fcpos >=0} {
set forcecolour 1
set args [lremove $args $fcpos]
}
set t [list]
set e [list] ;#extended codes will suppress standard SGR colours and attributes if merged in same escape sequence
dict set codestate_empty underline "" ;#4 on 24 off
#nonstandard 4:3,4:4,4:5
dict set codestate_empty curlyunderline ""
dict set codestate_empty dottedunderline ""
dict set codestate_empty dashedunderline ""
#nonstandard/extended 4:0,4:1,4:2,4:3,4:4,4:5
#4:1 single underline and 4:2 double underline deliberately kept separate to standard SGR versions
#The extended codes are merged separately allowing fallback SGR to be specified for terminals which don't support extended underlines
dict set codestate_empty underextended "" ;#4:0 for no extended underline 4:1 etc for underline styles
#dict set codestate_empty undersingle ""
#dict set codestate_empty underdouble ""
#dict set codestate_empty undercurly ""
#dict set codestate_empty underdottedn ""
#dict set codestate_empty underdashed ""
dict set codestate_empty blink "" ;#5 or 6 for slow/fast, 25 for off
dict set codestate_empty reverse "" ;#7 on 27 off
@ -3411,7 +3648,7 @@ namespace eval punk::ansi {
dict set codestate_empty strike "" ;#9 on 29 off
dict set codestate_empty font "" ;#10, 11-19 10 being primary
dict set codestate_empty gothic "" ;#20
dict set codestate_empty doubleunderline "" ;#21
dict set codestate_empty doubleunderline "" ;#21 (standard SGR double as opposed to underdouble)
dict set codestate_empty proportional "" ;#26 - see note below
dict set codestate_empty frame_or_circle "" ;#51,52 on - 54 off (54 off) (not generally used - mintty has repurposed for emoji variation selector)
@ -3422,7 +3659,7 @@ namespace eval punk::ansi {
dict set codestate_empty ideogram_doubleoverline ""
dict set codestate_empty ideogram_clear ""
dict set codestate_empty overline "" ;#53 on 55 off - probably not supported - pass through.
dict set codestate_empty overline "" ;#53 on 55 off - probably not supported - pass through. Seem to be ok to merge with other SGR even if not supported.
dict set codestate_empty underlinecolour "" ;#58 - same arguments as 256colour and rgb (nonstandard - in Kitty ,VTE,mintty and iTerm2)
#how to let rawmode loop handle it? It doesn't seem to get through if we return 0
puts stderr "signal ctrl-c while in raw mode"
after 200 {exit 42} ;#temp
flush stderr
return 42
}
#note - returning 0 means pass event to other handlers including OS default handler
if {$::repl::signal_control_c <= 2} {
set remaining [expr {3 - $::repl::signal_control_c}]
puts stderr "signal ctrl-c (perform $remaining more to quit, enter to return to repl)"
flush stderr
return 1
} elseif {$::repl::signal_control_c == 3} {
puts stderr "signal ctrl-c x3 received - quitting"
flush stderr
after 25
quit
return 1
} elseif {$::repl::signal_control_c == 4} {
puts stderr "signal ctrl-c x4 received - one more to hard exit"
flush stderr
if {![catch {package require twapi}]} {
#If script launched with windows batch file - we have to be careful to stop a ctrl-c from eventually reaching the batch file when the program terminates, even if fully handled here.
#This is done from within the launching batch file