diff --git a/src/bootsupport/modules/overtype-1.5.0.tm b/src/bootsupport/modules/overtype-1.5.0.tm deleted file mode 100644 index f4e466f3..00000000 --- a/src/bootsupport/modules/overtype-1.5.0.tm +++ /dev/null @@ -1,1039 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.0 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -##e.g package require frobz -package require textutil -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#candidate for zig/c implementation? -proc overtype::stripansi {text} { - variable escape_terminals ;#dict - variable ansi_2byte_codes_dict - #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway - if {[string first \033 $text] <0 && [string first \009c $text] <0} { - #\033 same as \x1b - return $text - } - - set text [convert_g0 $text] - - #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. - #line endings can theoretically occur within an ansi escape sequence (review e.g title?) - set inputlist [split $text ""] - set outputlist [list] - - set 2bytecodes [dict values $ansi_2byte_codes_dict] - - set in_escapesequence 0 - #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls - set i 0 - foreach u $inputlist { - set v [lindex $inputlist $i+1] - set uv ${u}${v} - if {$in_escapesequence eq "2b"} { - #2nd byte - done. - set in_escapesequence 0 - } elseif {$in_escapesequence != 0} { - set escseq [dict get $escape_terminals $in_escapesequence] - if {$u in $escseq} { - set in_escapesequence 0 - } elseif {$uv in $escseq} { - set in_escapseequence 2b ;#flag next byte as last in sequence - } - } else { - #handle both 7-bit and 8-bit CSI and OSC - if {[regexp {^(?:\033\[|\u009b)} $uv]} { - set in_escapesequence CSI - } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { - set in_escapesequence OSC - } elseif {$uv in $2bytecodes} { - #self-contained e.g terminal reset - don't pass through. - set in_escapesequence 2b - } else { - lappend outputlist $u - } - } - incr i - } - return [join $outputlist ""] -} - -#review -#todo - map other chars to unicode equivs -proc overtype::convert_g0 {text} { - #using not \033 inside to stop greediness - review how does it compare to ".*?" - set re {\033\(0[^\033]*\033\(B} - set re2 {\033\(0(.*)\033\(B} ;#capturing - set parts [ta::_perlish_split $re $text] - set out "" - foreach {pt g} $parts { - append out $pt - if {$g ne ""} { - #puts --$g-- - #box sample - #lqk - #x x - #mqj - #m = boxd_lur - #set map [list l \u250f k \u2513] ;#heavy - set map [list l \u250c q \u2500 k \u2510 x \u2502 m \u2514 j \u2518] ;#light - - regexp $re2 $g _match contents - append out [string map $map $contents] - } - } - return $out -} - -#todo - convert esc(0 graphics sequences to single char unicode equivalents e.g box drawing set -# esc) ?? -proc overtype::stripansi_gx {text} { - #e.g "\033(0" - select VT100 graphics for character set G0 - #e.g "\033(B" - reset - #e.g "\033)0" - select VT100 graphics for character set G1 - #e.g "\033)X" - where X is any char other than 0 to reset ?? - return [convert_g0 $text] -} - - -#This shouldn't be called on text containing ansi codes! -proc overtype::strip_nonprinting_ascii {str} { - #review - some single-byte 'control' chars have visual representations e.g ETX as heart - #It is currently used for screen display width calculations - #equivalent for various unicode combining chars etc? - set map [list\ - \007 ""\ - [format %c 0] ""\ - [format %c 0x7f] ""\ - ] - return [string map $map $str] -} - -#length of text for printing characters only -#review - unicode and other non-printing chars and combining sequences? -#certain unicode chars are full-width (single char 2 columns wide) e.g see "Halfwdith and fullwidth forms" and ascii_fuillwidth blocks in punk::char::charset_names -#review - is there an existing library or better method? print to a terminal and query cursor position? -#Note this length calculation is only suitable for lines being appended to other strings if the line is pre-processed to account for backspace and carriage returns first -#If the raw line is appended to another string without such processing - the backspaces & carriage returns can affect data prior to the start of the string. -proc overtype::printing_length {line} { - if {[string first \n $line] >= 0} { - error "line_print_length must not contain newline characters" - } - - #review - - set line [stripansi $line] - - set line [strip_nonprinting_ascii $line] ;#only strip nonprinting after stripansi - some like BEL are part of ansi - #backspace 0x08 only erases* printing characters anyway - so presumably order of processing doesn't matter - #(* more correctly - moves cursor back) - #backspace will not move beyond a preceding newline - but we have disallowed newlines for this function already - #leading backspaces will eat into any string (even prompt in non-standard tclsh shell) that is prepended to the line - # - but for the purposes of overtype we wouldn't want that - so we strip it here in the length calculation and should strip leading backspaces in the actual data concatenation operations too. - #curiously - a backspace sequence at the end of a string also doesn't reduce the printing width - so we can also strip from RHS - - #Note that backspace following a \t will only shorten the string by one (ie it doesn't move back the whole tab width like it does interactively in the terminal) - #for this reason - it would seem best to normalize the tabs to spaces prior to performing the backspace calculation - otherwise we won't account for the 'short' tabs it effectivley produces - #normalize tabs to an appropriate* width - #*todo - handle terminal/context where tabwidth != the default 8 spaces - set line [textutil::tabify::untabify2 $line] - - set bs [format %c 0x08] - #set line [string map [list "\r${bs}" "\r"] $line] ;#backsp following a \r will have no effect - set line [string trim $line $bs] - set n 0 - - set chars [split $line ""] - #build an output - set idx 0 - set outchars [list] - set outsizes [list] - foreach c $chars { - if {$c eq $bs} { - if {$idx > 0} { - incr idx -1 - } - } elseif {$c eq "\r"} { - set idx 0 - } else { - priv::printing_length_addchar $idx $c - incr idx - } - } - set line2 [join $outchars ""] - return [punk::char::string_width $line2] -} - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with overtype::stripansi. Alternatively try overtype::printing_length" - } - return [punk::char::string_width $text] -} - -namespace eval overtype::priv { - proc printing_length_addchar {i c} { - upvar outchars outc - upvar outsizes outs - set nxt [llength $outc] - if {$i < $nxt} { - lset outc $i $c - } else { - lappend outc $c - } - } -} - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {printing_length $v}]] - set overlines [split $overblock \n] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set undertext_printlen [printing_length $undertext] - set overlen [printing_length $overtext] - set diff [expr {$overlen - $colwidth}] - - #review - #append overtext "\033\[0m" - - if {$diff > 0} { - #background line is narrower - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - lappend outputlines $rendered - } else { - #we know overtext is shorter or equal - lappend outputlines [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {printing_length $v}]] - set overlines [split $overblock \n] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set olen [printing_length $overtext] - set ulen [printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - #review - append overtext "\033\[0m" - - set diff [expr {$colwidth - $olen}] - if {$diff > 0} { - #background block is wider - set half [expr {round(int($diff / 2))}] - if {[string match right [dict get $opts -bias]]} { - if {[expr {2 * $half}] < $diff} { - incr half - } - } - - set rhs [expr {$diff - $half - 1}] - set lhs [expr {$half - 1}] - set rhsoffset [expr {$rhs +1}] - if 0 { - set a [string range $undertext 0 $lhs] - set background [string range $undertext $lhs+1 end-$rhsoffset] - set b [renderline -transparent $opt_transparent $background $overtext] - set c [string range $undertext end-$rhs end] - lappend outputlines $a$b$c - } - lappend outputlines [renderline -start $lhs -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - - } else { - #overlay wider or equal - set rendered [renderline -transparent $opt_transparent -overflow [dict get $opts -overflow] $undertext $overtext] - if {$diff < 0} { - #overlay is wider - trim if overflow not specified in opts - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {[dict get $opts -ellipsis]} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } else { - #widths match - } - lappend outputlines $rendered - #lappend outputlines [renderline -transparent $opt_transparent $undertext $overtext] - } - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {printing_length $v}]] - set overlines [split $overblock \n] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set olen [printing_length $overtext] - set ulen [printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - #review - #append overtext "\033\[0m" - - set overflowlength [expr {$olen - $colwidth}] - if {$overflowlength > 0} { - #overtext wider than undertext column - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -start 0 $undertext $overtext] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - lappend outputlines $rendered - } else { - #lappend outputlines [string range $undertext 0 end-$olen]$overtext - lappend outputlines [renderline -transparent $opt_transparent -start [expr {$colwidth - $olen}] $undertext $overtext] - } - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#-returnextra to enable returning of overflow and length -# todo - use ta::detect to short-circuit processing and do simple string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters! -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >=0 || [string first \n $over] >= 0} { - error "overtype::renderline not allowed to contain newlines" - } - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -start 0\ - -returnextra 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -start] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -returnextra] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [dict create] - - set i_u -1 - set i_o 0 - set out [list] - set u_codestack [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach ch [split $pt ""] { - set width [punk::char::string_width $ch] - incr i_u - dict set understacks $i_u $u_codestack - lappend out $ch - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - incr i_u - dict set understacks $i_u $u_codestack - lappend out "" - } - } - #underlay should already have been rendered and not have non-sgr codes - but let's check for and not stack them if other codes are here - if {[priv::is_sgr $code]} { - if {[priv::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - lappend u_codestack $code - } - } - #consider also other codes that should be stacked..? - } - #trailing codes in effect for underlay - if {[llength $undermap]} { - dict set understacks [expr {$i_u + 1}] $u_codestack - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad [string repeat " " $opt_colstart] - append startpad $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad] - #### - - - - set overstacks [dict create] - set o_codestack [list] - set pt_overchars "" - foreach {pt code} $overmap { - append pt_overchars $pt - foreach ch [split $pt ""] { - dict set overstacks $i_o $o_codestack - incr i_o - } - if {[priv::is_sgr $code]} { - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {[priv::has_sgr_leadingreset $code]} { - #m code which has sgr reset at start - no need to replay prior sgr codes - set o_codestack [list $code] - } else { - lappend o_codestack $code - } - } - } - # -- --- --- --- --- --- --- --- - - - - - set bs [format %c 0x08] - set idx 0 ;# line index (cursor - 1) - set idx_over -1 - foreach {pt code} $overmap { - set ptchars [split $pt ""] ;#for lookahead - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - foreach ch $ptchars { - incr idx_over - if {$ch eq "\r"} { - set idx $opt_colstart - } elseif {$ch eq "\b"} { - #review - backspace effect on double-width chars - if {$idx > $opt_colstart} { - incr idx -1 - } - } elseif {($idx < $opt_colstart)} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - set owidth [punk::char::string_width $ch] - if {$idx > [llength $out]-1} { - lappend out " " - dict set understacks $idx [list] ;#review - use idx-1 codestack? - incr idx - } else { - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - incr idx - } elseif {$uwidth == 1} { - incr idx - if {$owidth > 1} { - incr idx - } - } elseif {$uwidth > 1} { - if {[punk::char::string_width $ch] == 1} { - #normal singlewide transparency - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] - incr idx - } - } - } else { - #2wide transparency over 2wide in underlay - incr idx - } - } - } - } else { - #non-transparent char in overlay - set owidth [punk::char::string_width $ch] - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2wide char in underlay - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - - } elseif {$uwidth == 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - } - } elseif {$uwidth > 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $opt_exposed2 [dict get $overstacks $idx_over] - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } - } - } - } - #check following code - if {![priv::is_sgr $code]} { - - } - } - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - - #coalesce and replay codestacks for out char list - set outstring "" - set remstring "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set out_rawchars ""; #for overflow counting - set output_to "outstring" ;#var in effect depending on overflow - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $out { - append out_rawchars $ch - if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] < $num_under_columns} { - } else { - #todo - check if we overflowed with a double-width char ? - #store visualwidth which may be short - set in_overflow 1 - } - } - set cstack [dict get $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append $output_to \033\[m - } - foreach code $cstack { - append $output_to $code - } - } - append $output_to $ch - set prevstack $cstack - incr i - if {$in_overflow} { - set output_to "remstring" - } - } - if {[dict size $understacks] > 0} { - append $output_to [join [dict get $understacks [expr {[dict size $understacks]-1}]] ""] ;#tail codes - } - if {[string length $remstring]} { - #puts stderr "remainder:$remstring" - } - #pdict $understacks - if {$opt_returnextra} { - return [list $outstring $visualwidth [string length $outstring] $remstring] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} -namespace eval overtype::priv { - #todo - move to punk::ansi::codetype - proc is_sgr {code} { - #SGR (Select Graphic Rendition) - codes ending in 'm' - e.g colour/underline - #we will accept and pass through the less common colon separator (ITU Open Document Architecture) - #Terminals should generally ignore it if they don't use it - regexp {\033\[[0-9;:]*m$} $code - } - proc is_cursor_move_in_line {code} { - #review - what about CSI n : m H where row n happens to be current line? - regexp {\033\[[0-9]*(:?C|D|G)$} - } - #pure SGR reset - proc is_sgr_reset {code} { - #todo 8-bit csi - regexp {\033\[0*m$} $code - } - #whether this code has 0 (or equivalently empty) parameter (but may set others) - #if an SGR code as a reset in it - we don't need to carry forward any previous SGR codes - #it generally only makes sense for the reset to be the first entry - otherwise the code has ineffective portions - #However - detecting zero or empty parameter in other positions requires knowing all other codes that may allow zero or empty params. - #We will only look at initial parameter as this is the well-formed normal case. - #Review - consider normalizing sgr codes to remove other redundancies such as setting fg or bg color twice in same code - proc has_sgr_leadingreset {code} { - set params "" - regexp {\033\[(.*)m} $code _match params - set plist [split $params ";"] - if {[string trim [lindex $plist 0] 0] eq ""} { - #e.g \033\[m \033\[0\;...m \033\[0000...m - return 1 - } else { - return 0 - } - } - #has_sgr_reset - rather than support this - create an sgr normalize function that removes dead params and brings reset to front of param list - proc render_addchar {i c stack} { - upvar out o - upvar understacks ustacks - set nxt [llength $o] - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - dict set ustacks $i $stack - } - -} - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype::ta { - namespace path ::overtype - #*based* on but not identical to: - #https://github.com/perlancar/perl-Text-ANSI-Util/blob/master/lib/Text/ANSI/BaseUtil.pm - - #handle both 7-bit and 8-bit csi - #review - does codepage affect this? e.g ebcdic has 8bit csi in different position - - #CSI - #variable re_csi_open {(?:\033\[|\u009b)[0-9;]+} ;#too specific - doesn't detect \033\[m - variable re_csi_open {(?:\033\[|\u009b])} - - #colour and style - variable re_csi_colour {(?:\033\[|\u009b)[0-9;]*m} ;#e.g \033\[31m \033\[m \033\[0m \033\[m0000m - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - variable re_csi_code {(?:\033\[|\u009b])[0-9;]*[a-zA-Z\\@^_|~`]} - - #OSC - termnate with BEL (\a \007) or ST (string terminator \033\\) - #variable re_esc_osc1 {(?:\033\]|\u009c).*\007} - #variable re_esc_osc2 {(?:\033\]|\u009c).*\033\\} - - #test - non-greedy - variable re_esc_osc1 {(?:\033\]|\u009c).*?\007} - variable re_esc_osc2 {(?:\033\]|\u009c).*?\033\\} - - variable re_ansi_detect "${re_csi_open}|${re_esc_osc1}|${re_esc_osc2}" - - #detect any ansi escapes - #review - only detect 'complete' codes - or just use the opening escapes for performance? - proc detect {text} { - variable re_ansi_detect - #variable re_csi_open - #variable re_esc_osc1 - #variable re_esc_osc2 - #todo - other escape sequences - #expr {[regexp $re_csi_open $text] || [regexp $re_esc_osc1 $text] || [regexp $re_esc_osc2 $text]} - expr {[regexp $re_ansi_detect $text]} - } - #not in perl ta - proc detect_csi {text} { - variable re_csi_colour - expr {[regexp $re_csi_colour $text]} - } - proc strip {text} { - tailcall stripansi $text - } - #note this is character length after stripping ansi codes - not the printing length - proc length {text} { - string length [overtype::stripansi $text] - } - #todo - handle newlines - #not in perl ta - proc printing_length {text} { - - } - - proc trunc {text width args} { - - } - - #not in perl ta - #returns just the plaintext portions in a list - proc split_at_codes {text} { - variable re_esc_osc1 - variable re_esc_osc2 - variable re_csi_code - textutil::splitx $text "${re_csi_code}|${re_esc_osc1}|${re_esc_osc2}" - } - - # -- --- --- --- --- --- - #Split $text to a list containing alternating ANSI color codes and text. - #ANSI color codes are always on the second element, fourth, and so on. - #(ie plaintext on odd list-indices ansi on even indices) - # Example: - #ta_split_codes "" # => "" - #ta_split_codes "a" # => "a" - #ta_split_codes "a\e[31m" # => {"a" "\e[31m"} - #ta_split_codes "\e[31ma" # => {"" "\e[31m" "a"} - #ta_split_codes "\e[31ma\e[0m" # => {"" "\e[31m" "a" "\e[0m"} - #ta_split_codes "\e[31ma\e[0mb" # => {"" "\e[31m" "a" "\e[0m", "b"} - #ta_split_codes "\e[31m\e[0mb" # => {"" "\e[31m\e[0m" "b"} - # - proc split_codes {text} { - variable re_esc_osc1 - variable re_esc_osc2 - variable re_csi_code - set re "(?:${re_csi_code}|${re_esc_osc1}|${re_esc_osc2})+" - return [_perlish_split $re $text] - } - #like split_codes - but each ansi-escape is split out separately (with empty string of plaintext between codes so odd/even plain ansi still holds) - proc split_codes_single {text} { - variable re_esc_osc1 - variable re_esc_osc2 - variable re_csi_code - set re "${re_csi_code}|${re_esc_osc1}|${re_esc_osc2}" - return [_perlish_split $re $text] - } - - #review - tcl greedy expressions may match multiple in one element - proc _perlish_split {re text} { - if {[string length $text] == 0} { - return {} - } - set list [list] - set start 0 - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - lappend list [string range $text $start $matchStart-1] [string range $text $matchStart $matchEnd] - set start [expr {$matchEnd+1}] - } - lappend list [string range $text $start end] - return $list - } - proc _ws_split {text} { - regexp -all -inline {(?:\S+)|(?:\s+)} $text - } - # -- --- --- --- --- --- - -} - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.0 -}] -return \ No newline at end of file diff --git a/src/bootsupport/modules/overtype-1.5.1.tm b/src/bootsupport/modules/overtype-1.5.1.tm deleted file mode 100644 index 92f2464a..00000000 --- a/src/bootsupport/modules/overtype-1.5.1.tm +++ /dev/null @@ -1,963 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.1 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.5.1] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - } - return [punk::char::string_width $text] -} - - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - set overlines [split $overblock \n] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set undertext_printlen [punk::ansi::printing_length $undertext] - set overlen [punk::ansi::printing_length $overtext] - set diff [expr {$overlen - $colwidth}] - - #review - #append overtext "\033\[0m" - - if {$diff > 0} { - #background line is narrower - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - lappend outputlines $rendered - } else { - #we know overtext is shorter or equal - lappend outputlines [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - set overlines [split $overblock \n] - set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - #set olen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - #review - #append overtext "\033\[0m" - - set under_exposed [expr {$colwidth - $overblock_width}] - if {$under_exposed > 0} { - #background block is wider - if {$under_exposed % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed / 2}] - } else { - set beforehalf [expr {$under_exposed / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - - if 0 { - set rhs [expr {$diff - $half - 1}] - set lhs [expr {$half - 1}] - set rhsoffset [expr {$rhs +1}] - set a [string range $undertext 0 $lhs] - set background [string range $undertext $lhs+1 end-$rhsoffset] - set b [renderline -transparent $opt_transparent $background $overtext] - set c [string range $undertext end-$rhs end] - lappend outputlines $a$b$c - } - lappend outputlines [renderline -start $left_exposed -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - - } else { - #overlay wider or equal - set rendered [renderline -transparent $opt_transparent -overflow [dict get $opts -overflow] $undertext $overtext] - if {$under_exposed < 0} { - #overlay is wider - trim if overflow not specified in opts - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {[dict get $opts -ellipsis]} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } else { - #zero under_exposed - widths match - } - lappend outputlines $rendered - #lappend outputlines [renderline -transparent $opt_transparent $undertext $overtext] - } - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - set overlines [split $overblock \n] - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set olen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - #review - #append overtext "\033\[0m" - - set overflowlength [expr {$olen - $colwidth}] - if {$overflowlength > 0} { - #overtext wider than undertext column - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -start 0 $undertext $overtext] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - lappend outputlines $rendered - } else { - #lappend outputlines [string range $undertext 0 end-$olen]$overtext - lappend outputlines [renderline -transparent $opt_transparent -start [expr {$colwidth - $olen}] $undertext $overtext] - } - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#-returnextra to enable returning of overflow and length -# todo - use punk::ansi::ta::detect to short-circuit processing and do simple string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters! -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >=0 || [string first \n $over] >= 0} { - error "overtype::renderline not allowed to contain newlines" - } - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -start 0\ - -returnextra 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -start] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -returnextra] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [dict create] - - set i_u -1 - set i_o 0 - set out [list] - set u_codestack [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach ch [split $pt ""] { - set width [punk::char::string_width $ch] - incr i_u - dict set understacks $i_u $u_codestack - lappend out $ch - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - incr i_u - dict set understacks $i_u $u_codestack - lappend out "" - } - } - #underlay should already have been rendered and not have non-sgr codes - but let's check for and not stack them if other codes are here - if {[priv::is_sgr $code]} { - if {[priv::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - lappend u_codestack $code - } - } - #consider also other codes that should be stacked..? - } - #trailing codes in effect for underlay - if {[llength $undermap]} { - dict set understacks [expr {$i_u + 1}] $u_codestack - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad [string repeat " " $opt_colstart] - append startpad $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad] - #### - - - - set overstacks [dict create] - set o_codestack [list] - set pt_overchars "" - foreach {pt code} $overmap { - append pt_overchars $pt - foreach ch [split $pt ""] { - dict set overstacks $i_o $o_codestack - incr i_o - } - if {[priv::is_sgr $code]} { - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {[priv::has_sgr_leadingreset $code]} { - #m code which has sgr reset at start - no need to replay prior sgr codes - set o_codestack [list $code] - } else { - lappend o_codestack $code - } - } - } - # -- --- --- --- --- --- --- --- - - - - - set bs [format %c 0x08] - set idx 0 ;# line index (cursor - 1) - set idx_over -1 - foreach {pt code} $overmap { - set ptchars [split $pt ""] ;#for lookahead - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - foreach ch $ptchars { - incr idx_over - if {$ch eq "\r"} { - set idx $opt_colstart - } elseif {$ch eq "\b"} { - #review - backspace effect on double-width chars - if {$idx > $opt_colstart} { - incr idx -1 - } - } elseif {($idx < $opt_colstart)} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - set owidth [punk::char::string_width $ch] - if {$idx > [llength $out]-1} { - lappend out " " - dict set understacks $idx [list] ;#review - use idx-1 codestack? - incr idx - } else { - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - incr idx - } elseif {$uwidth == 1} { - incr idx - if {$owidth > 1} { - incr idx - } - } elseif {$uwidth > 1} { - if {[punk::char::string_width $ch] == 1} { - #normal singlewide transparency - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] - incr idx - } - } - } else { - #2wide transparency over 2wide in underlay - incr idx - } - } - } - } else { - #non-transparent char in overlay - set owidth [punk::char::string_width $ch] - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2wide char in underlay - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - - } elseif {$uwidth == 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - } - } elseif {$uwidth > 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $opt_exposed2 [dict get $overstacks $idx_over] - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } - } - } - } - #check following code - if {![priv::is_sgr $code]} { - - } - } - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - - #coalesce and replay codestacks for out char list - set outstring "" - set remstring "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set out_rawchars ""; #for overflow counting - set output_to "outstring" ;#var in effect depending on overflow - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $out { - append out_rawchars $ch - if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] < $num_under_columns} { - } else { - #todo - check if we overflowed with a double-width char ? - #store visualwidth which may be short - set in_overflow 1 - } - } - set cstack [dict get $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append $output_to \033\[m - } - foreach code $cstack { - append $output_to $code - } - } - append $output_to $ch - set prevstack $cstack - incr i - if {$in_overflow} { - set output_to "remstring" - } - } - if {[dict size $understacks] > 0} { - append $output_to [join [dict get $understacks [expr {[dict size $understacks]-1}]] ""] ;#tail codes - } - if {[string length $remstring]} { - #puts stderr "remainder:$remstring" - } - #pdict $understacks - if {$opt_returnextra} { - return [list $outstring $visualwidth [string length $outstring] $remstring] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} -namespace eval overtype::priv { - #todo - move to punk::ansi::codetype - proc is_sgr {code} { - #SGR (Select Graphic Rendition) - codes ending in 'm' - e.g colour/underline - #we will accept and pass through the less common colon separator (ITU Open Document Architecture) - #Terminals should generally ignore it if they don't use it - regexp {\033\[[0-9;:]*m$} $code - } - proc is_cursor_move_in_line {code} { - #review - what about CSI n : m H where row n happens to be current line? - regexp {\033\[[0-9]*(:?C|D|G)$} - } - #pure SGR reset - proc is_sgr_reset {code} { - #todo 8-bit csi - regexp {\033\[0*m$} $code - } - #whether this code has 0 (or equivalently empty) parameter (but may set others) - #if an SGR code as a reset in it - we don't need to carry forward any previous SGR codes - #it generally only makes sense for the reset to be the first entry - otherwise the code has ineffective portions - #However - detecting zero or empty parameter in other positions requires knowing all other codes that may allow zero or empty params. - #We will only look at initial parameter as this is the well-formed normal case. - #Review - consider normalizing sgr codes to remove other redundancies such as setting fg or bg color twice in same code - proc has_sgr_leadingreset {code} { - set params "" - regexp {\033\[(.*)m} $code _match params - set plist [split $params ";"] - if {[string trim [lindex $plist 0] 0] eq ""} { - #e.g \033\[m \033\[0\;...m \033\[0000...m - return 1 - } else { - return 0 - } - } - #has_sgr_reset - rather than support this - create an sgr normalize function that removes dead params and brings reset to front of param list - proc render_addchar {i c stack} { - upvar out o - upvar understacks ustacks - set nxt [llength $o] - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - dict set ustacks $i $stack - } - -} - - -# -- --- --- --- --- --- --- --- --- --- --- -if 0 { -namespace eval overtype::ta { - namespace path ::overtype - # *based* on but not identical to: - #https://github.com/perlancar/perl-Text-ANSI-Util/blob/master/lib/Text/ANSI/BaseUtil.pm - - #handle both 7-bit and 8-bit csi - #review - does codepage affect this? e.g ebcdic has 8bit csi in different position - - #CSI - #variable re_csi_open {(?:\033\[|\u009b)[0-9;]+} ;#too specific - doesn't detect \033\[m - variable re_csi_open {(?:\033\[|\u009b])} - - #colour and style - variable re_csi_colour {(?:\033\[|\u009b)[0-9;]*m} ;#e.g \033\[31m \033\[m \033\[0m \033\[m0000m - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - variable re_csi_code {(?:\033\[|\u009b])[0-9;]*[a-zA-Z\\@^_|~`]} - - #OSC - termnate with BEL (\a \007) or ST (string terminator \033\\) - #variable re_esc_osc1 {(?:\033\]|\u009c).*\007} - #variable re_esc_osc2 {(?:\033\]|\u009c).*\033\\} - - #test - non-greedy - variable re_esc_osc1 {(?:\033\]|\u009c).*?\007} - variable re_esc_osc2 {(?:\033\]|\u009c).*?\033\\} - - variable re_ansi_detect "${re_csi_open}|${re_esc_osc1}|${re_esc_osc2}" - - #detect any ansi escapes - #review - only detect 'complete' codes - or just use the opening escapes for performance? - #proc detect {text} { - # variable re_ansi_detect - # #variable re_csi_open - # #variable re_esc_osc1 - # #variable re_esc_osc2 - # #todo - other escape sequences - # #expr {[regexp $re_csi_open $text] || [regexp $re_esc_osc1 $text] || [regexp $re_esc_osc2 $text]} - # expr {[regexp $re_ansi_detect $text]} - #} - #not in perl ta - #proc detect_csi {text} { - # variable re_csi_colour - # expr {[regexp $re_csi_colour $text]} - #} - proc strip {text} { - tailcall punk::ansi::stripansi $text - } - #note this is character length after stripping ansi codes - not the printing length - proc length {text} { - string length [punk::ansi::stripansi $text] - } - - - # -- --- --- --- --- --- - #Split $text to a list containing alternating ANSI color codes and text. - #ANSI color codes are always on the second element, fourth, and so on. - #(ie plaintext on odd list-indices ansi on even indices) - # Example: - #ta_split_codes "" # => "" - #ta_split_codes "a" # => "a" - #ta_split_codes "a\e[31m" # => {"a" "\e[31m"} - #ta_split_codes "\e[31ma" # => {"" "\e[31m" "a"} - #ta_split_codes "\e[31ma\e[0m" # => {"" "\e[31m" "a" "\e[0m"} - #ta_split_codes "\e[31ma\e[0mb" # => {"" "\e[31m" "a" "\e[0m", "b"} - #ta_split_codes "\e[31m\e[0mb" # => {"" "\e[31m\e[0m" "b"} - # - #proc split_codes {text} { - # variable re_esc_osc1 - # variable re_esc_osc2 - # variable re_csi_code - # set re "(?:${re_csi_code}|${re_esc_osc1}|${re_esc_osc2})+" - # return [_perlish_split $re $text] - #} - ##like split_codes - but each ansi-escape is split out separately (with empty string of plaintext between codes so odd/even plain ansi still holds) - #proc split_codes_single {text} { - # variable re_esc_osc1 - # variable re_esc_osc2 - # variable re_csi_code - # set re "${re_csi_code}|${re_esc_osc1}|${re_esc_osc2}" - # return [_perlish_split $re $text] - #} - - ##review - tcl greedy expressions may match multiple in one element - #proc _perlish_split {re text} { - # if {[string length $text] == 0} { - # return {} - # } - # set list [list] - # set start 0 - # while {[regexp -start $start -indices -- $re $text match]} { - # lassign $match matchStart matchEnd - # lappend list [string range $text $start $matchStart-1] [string range $text $matchStart $matchEnd] - # set start [expr {$matchEnd+1}] - # } - # lappend list [string range $text $start end] - # return $list - #} - ## -- --- --- --- --- --- - -} -} ;# end if 0 - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.1 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.5.3.tm b/src/bootsupport/modules/overtype-1.5.3.tm deleted file mode 100644 index 6b54a4ac..00000000 --- a/src/bootsupport/modules/overtype-1.5.3.tm +++ /dev/null @@ -1,1037 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.3 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.5.3] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - } - return [punk::char::string_width $text] -} - - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set right_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set undertext_printlen [punk::ansi::printing_length $undertext] - if {$undertext_printlen < $colwidth} { - set udiff [expr {$colwidth - $undertext_printlen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set overtext_printlen [punk::ansi::printing_length $overtext] - set overflowlength [expr {$overtext_printlen - $colwidth}] - - #review - #append overtext "\033\[0m" - - - if {$overflowlength > 0} { - #background line is narrower than data in line - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #we know overtext data is shorter or equal (for this line) - lappend outputlines [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rendered [renderline -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - lappend outputlines [renderline -start $left_exposed -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set left_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - puts xxx - set undertext "$undertext[string repeat { } $udiff]" - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - #padding always on right - if alignment is required it should be done to block beforehand - not here - set overtextpadding "$overtext[string repeat { } $odiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -start 0 $undertext $overtext] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - lappend outputlines [renderline -transparent $opt_transparent -start $left_exposed $undertext $overtext] - } - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#-returnextra to enable returning of overflow and length -# todo - use punk::ansi::ta::detect to short-circuit processing and do simple string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters! -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >=0 || [string first \n $over] >= 0} { - error "overtype::renderline not allowed to contain newlines" - } - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -start 0\ - -returnextra 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -start] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -returnextra] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [dict create] - - set i_u -1 - set i_o 0 - set out [list] - set u_codestack [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach ch [split $pt ""] { - set width [punk::char::string_width $ch] - incr i_u - dict set understacks $i_u $u_codestack - lappend out $ch - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - incr i_u - dict set understacks $i_u $u_codestack - lappend out "" - } - } - #underlay should already have been rendered and not have non-sgr codes - but let's check for and not stack them if other codes are here - if {[priv::is_sgr $code]} { - if {[priv::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - lappend u_codestack $code - } - } - #consider also other codes that should be stacked..? - } - #trailing codes in effect for underlay - if {[llength $undermap]} { - dict set understacks [expr {$i_u + 1}] $u_codestack - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad [string repeat " " $opt_colstart] - append startpad $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad] - #### - - - - set overstacks [dict create] - set o_codestack [list] - set pt_overchars "" - foreach {pt code} $overmap { - append pt_overchars $pt - foreach ch [split $pt ""] { - dict set overstacks $i_o $o_codestack - incr i_o - } - if {[priv::is_sgr $code]} { - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {[priv::has_sgr_leadingreset $code]} { - #m code which has sgr reset at start - no need to replay prior sgr codes - set o_codestack [list $code] - } else { - lappend o_codestack $code - } - } - } - # -- --- --- --- --- --- --- --- - - - - - set bs [format %c 0x08] - set idx 0 ;# line index (cursor - 1) - set idx_over -1 - foreach {pt code} $overmap { - set ptchars [split $pt ""] ;#for lookahead - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - foreach ch $ptchars { - incr idx_over - if {$ch eq "\r"} { - set idx $opt_colstart - } elseif {$ch eq "\b"} { - #review - backspace effect on double-width chars - if {$idx > $opt_colstart} { - incr idx -1 - } - } elseif {($idx < $opt_colstart)} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - set owidth [punk::char::string_width $ch] - if {$idx > [llength $out]-1} { - lappend out " " - dict set understacks $idx [list] ;#review - use idx-1 codestack? - incr idx - } else { - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - incr idx - } elseif {$uwidth == 1} { - incr idx - if {$owidth > 1} { - incr idx - } - } elseif {$uwidth > 1} { - if {[punk::char::string_width $ch] == 1} { - #normal singlewide transparency - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] - incr idx - } - } - } else { - #2wide transparency over 2wide in underlay - incr idx - } - } - } - } else { - #non-transparent char in overlay - set owidth [punk::char::string_width $ch] - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2wide char in underlay - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - - } elseif {$uwidth == 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - } - } elseif {$uwidth > 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $opt_exposed2 [dict get $overstacks $idx_over] - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } - } - } - } - #check following code - if {![priv::is_sgr $code]} { - - } - } - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - - #coalesce and replay codestacks for out char list - set outstring "" - set remstring "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set out_rawchars ""; #for overflow counting - set output_to "outstring" ;#var in effect depending on overflow - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $out { - append out_rawchars $ch - if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] < $num_under_columns} { - } else { - #todo - check if we overflowed with a double-width char ? - #store visualwidth which may be short - set in_overflow 1 - } - } - set cstack [dict get $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append $output_to \033\[m - } - foreach code $cstack { - append $output_to $code - } - } - append $output_to $ch - set prevstack $cstack - incr i - if {$in_overflow} { - set output_to "remstring" - } - } - if {[dict size $understacks] > 0} { - append $output_to [join [dict get $understacks [expr {[dict size $understacks]-1}]] ""] ;#tail codes - } - if {[string length $remstring]} { - #puts stderr "remainder:$remstring" - } - #pdict $understacks - if {$opt_returnextra} { - return [list $outstring $visualwidth [string length $outstring] $remstring] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#same as textblock::size - but we don't want that circular dependency -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - set textblock [textutil::tabify::untabify2 $textblock] - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - set textblock [punk::ansi::stripansi $textblock] - if {[string first \n $textblock] >= 0} { - set width [tcl::mathfunc::max {*}[lmap v [lines_as_list -- $textblock] {::punk::char::string_width $v}]] - } else { - set width [punk::char::string_width $textblock] - } - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - - - #todo - move to punk::ansi::codetype - proc is_sgr {code} { - #SGR (Select Graphic Rendition) - codes ending in 'm' - e.g colour/underline - #we will accept and pass through the less common colon separator (ITU Open Document Architecture) - #Terminals should generally ignore it if they don't use it - regexp {\033\[[0-9;:]*m$} $code - } - proc is_cursor_move_in_line {code} { - #review - what about CSI n : m H where row n happens to be current line? - regexp {\033\[[0-9]*(:?C|D|G)$} - } - #pure SGR reset - proc is_sgr_reset {code} { - #todo 8-bit csi - regexp {\033\[0*m$} $code - } - #whether this code has 0 (or equivalently empty) parameter (but may set others) - #if an SGR code as a reset in it - we don't need to carry forward any previous SGR codes - #it generally only makes sense for the reset to be the first entry - otherwise the code has ineffective portions - #However - detecting zero or empty parameter in other positions requires knowing all other codes that may allow zero or empty params. - #We will only look at initial parameter as this is the well-formed normal case. - #Review - consider normalizing sgr codes to remove other redundancies such as setting fg or bg color twice in same code - proc has_sgr_leadingreset {code} { - set params "" - regexp {\033\[(.*)m} $code _match params - set plist [split $params ";"] - if {[string trim [lindex $plist 0] 0] eq ""} { - #e.g \033\[m \033\[0\;...m \033\[0000...m - return 1 - } else { - return 0 - } - } - #has_sgr_reset - rather than support this - create an sgr normalize function that removes dead params and brings reset to front of param list - proc render_addchar {i c stack} { - upvar out o - upvar understacks ustacks - set nxt [llength $o] - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - dict set ustacks $i $stack - } - -} - - -# -- --- --- --- --- --- --- --- --- --- --- -if 0 { -namespace eval overtype::ta { - namespace path ::overtype - # *based* on but not identical to: - #https://github.com/perlancar/perl-Text-ANSI-Util/blob/master/lib/Text/ANSI/BaseUtil.pm - - #handle both 7-bit and 8-bit csi - #review - does codepage affect this? e.g ebcdic has 8bit csi in different position - - #CSI - #variable re_csi_open {(?:\033\[|\u009b)[0-9;]+} ;#too specific - doesn't detect \033\[m - variable re_csi_open {(?:\033\[|\u009b])} - - #colour and style - variable re_csi_colour {(?:\033\[|\u009b)[0-9;]*m} ;#e.g \033\[31m \033\[m \033\[0m \033\[m0000m - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - variable re_csi_code {(?:\033\[|\u009b])[0-9;]*[a-zA-Z\\@^_|~`]} - - #OSC - termnate with BEL (\a \007) or ST (string terminator \033\\) - #variable re_esc_osc1 {(?:\033\]|\u009c).*\007} - #variable re_esc_osc2 {(?:\033\]|\u009c).*\033\\} - - #test - non-greedy - variable re_esc_osc1 {(?:\033\]|\u009c).*?\007} - variable re_esc_osc2 {(?:\033\]|\u009c).*?\033\\} - - variable re_ansi_detect "${re_csi_open}|${re_esc_osc1}|${re_esc_osc2}" - - #detect any ansi escapes - #review - only detect 'complete' codes - or just use the opening escapes for performance? - #proc detect {text} { - # variable re_ansi_detect - # #variable re_csi_open - # #variable re_esc_osc1 - # #variable re_esc_osc2 - # #todo - other escape sequences - # #expr {[regexp $re_csi_open $text] || [regexp $re_esc_osc1 $text] || [regexp $re_esc_osc2 $text]} - # expr {[regexp $re_ansi_detect $text]} - #} - #not in perl ta - #proc detect_csi {text} { - # variable re_csi_colour - # expr {[regexp $re_csi_colour $text]} - #} - proc strip {text} { - tailcall punk::ansi::stripansi $text - } - #note this is character length after stripping ansi codes - not the printing length - proc length {text} { - string length [punk::ansi::stripansi $text] - } - - - # -- --- --- --- --- --- - #Split $text to a list containing alternating ANSI color codes and text. - #ANSI color codes are always on the second element, fourth, and so on. - #(ie plaintext on odd list-indices ansi on even indices) - # Example: - #ta_split_codes "" # => "" - #ta_split_codes "a" # => "a" - #ta_split_codes "a\e[31m" # => {"a" "\e[31m"} - #ta_split_codes "\e[31ma" # => {"" "\e[31m" "a"} - #ta_split_codes "\e[31ma\e[0m" # => {"" "\e[31m" "a" "\e[0m"} - #ta_split_codes "\e[31ma\e[0mb" # => {"" "\e[31m" "a" "\e[0m", "b"} - #ta_split_codes "\e[31m\e[0mb" # => {"" "\e[31m\e[0m" "b"} - # - #proc split_codes {text} { - # variable re_esc_osc1 - # variable re_esc_osc2 - # variable re_csi_code - # set re "(?:${re_csi_code}|${re_esc_osc1}|${re_esc_osc2})+" - # return [_perlish_split $re $text] - #} - ##like split_codes - but each ansi-escape is split out separately (with empty string of plaintext between codes so odd/even plain ansi still holds) - #proc split_codes_single {text} { - # variable re_esc_osc1 - # variable re_esc_osc2 - # variable re_csi_code - # set re "${re_csi_code}|${re_esc_osc1}|${re_esc_osc2}" - # return [_perlish_split $re $text] - #} - - ##review - tcl greedy expressions may match multiple in one element - #proc _perlish_split {re text} { - # if {[string length $text] == 0} { - # return {} - # } - # set list [list] - # set start 0 - # while {[regexp -start $start -indices -- $re $text match]} { - # lassign $match matchStart matchEnd - # lappend list [string range $text $start $matchStart-1] [string range $text $matchStart $matchEnd] - # set start [expr {$matchEnd+1}] - # } - # lappend list [string range $text $start end] - # return $list - #} - ## -- --- --- --- --- --- - -} -} ;# end if 0 - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.3 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.5.6.tm b/src/bootsupport/modules/overtype-1.5.6.tm deleted file mode 100644 index 5c56838b..00000000 --- a/src/bootsupport/modules/overtype-1.5.6.tm +++ /dev/null @@ -1,928 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.6 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.5.6] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::string_width $text] -} - - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set right_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set undertext_printlen [punk::ansi::printing_length $undertext] - if {$undertext_printlen < $colwidth} { - set udiff [expr {$colwidth - $undertext_printlen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set overtext_printlen [punk::ansi::printing_length $overtext] - set overflowlength [expr {$overtext_printlen - $colwidth}] - - #review - #append overtext "\033\[0m" - - - if {$overflowlength > 0} { - #background line is narrower than data in line - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #we know overtext data is shorter or equal (for this line) - lappend outputlines [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rendered [renderline -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - lappend outputlines [renderline -start $left_exposed -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set left_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - puts xxx - set undertext "$undertext[string repeat { } $udiff]" - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - #padding always on right - if alignment is required it should be done to block beforehand - not here - set overtextpadding "$overtext[string repeat { } $odiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -start 0 $undertext $overtext] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - lappend outputlines [renderline -transparent $opt_transparent -start $left_exposed $undertext $overtext] - } - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#-returnextra to enable returning of overflow and length -# todo - use punk::ansi::ta::detect to short-circuit processing and do simple string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters! -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - #should also rule out \v - if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines" - } - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -start 0\ - -returnextra 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -start] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -returnextra] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [dict create] - - set i_u -1 - set i_o 0 - set out [list] - set u_codestack [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach grapheme [punk::char::grapheme_split $pt] { - set width [punk::char::string_width $grapheme] - incr i_u - dict set understacks $i_u $u_codestack - lappend out $grapheme - if {$width > 1} { - incr i_u - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - dict set understacks $i_u $u_codestack - lappend out "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - lappend u_codestack $code - } - #consider also if there are other codes that should be stacked..? - } - #trailing codes in effect for underlay - if {[llength $undermap]} { - dict set understacks [expr {$i_u + 1}] $u_codestack - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad [string repeat " " $opt_colstart] - append startpad $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad] - #### - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - - set overstacks [dict create] - set o_codestack [list] - set pt_overchars "" - foreach {pt code} $overmap { - append pt_overchars $pt - foreach grapheme [punk::char::grapheme_split $pt] { - dict set overstacks $i_o $o_codestack - incr i_o - } - - if {[punk::ansi::codetype::is_sgr $code]} { - if {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #m code which has sgr reset at start - no need to replay prior sgr codes - set o_codestack [list $code] - } else { - lappend o_codestack $code - } - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - lappend o_codestack $code - } - - } - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - set bs [format %c 0x08] - set idx 0 ;# line index (cursor - 1) - set idx_over -1 - foreach {pt code} $overmap { - #set ptchars [split $pt ""] ;#for lookahead - set graphemes [punk::char::grapheme_split $pt] - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - foreach ch $graphemes { - incr idx_over - if {$ch eq "\r"} { - set idx $opt_colstart - } elseif {$ch eq "\b"} { - #review - backspace effect on double-width chars - if {$idx > $opt_colstart} { - incr idx -1 - } - } elseif {($idx < $opt_colstart)} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - set owidth [punk::char::string_width $ch] - if {$idx > [llength $out]-1} { - lappend out " " - dict set understacks $idx [list] ;#review - use idx-1 codestack? - incr idx - } else { - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - } elseif {$uwidth == 1} { - incr idx - if {$owidth > 1} { - incr idx - } - } elseif {$uwidth > 1} { - if {[punk::char::string_width $ch] == 1} { - #normal singlewide transparency - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] - incr idx - } - } - } else { - #2wide transparency over 2wide in underlay - incr idx - } - } - } - } else { - #non-transparent char in overlay - set owidth [punk::char::string_width $ch] - set uwidth [punk::char::string_width [lindex $out $idx]] - if {[lindex $out $idx] eq ""} { - #2nd col of 2wide char in underlay - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } elseif {$uwidth == 0} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - - } elseif {$uwidth == 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - } - } elseif {$uwidth > 1} { - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $opt_exposed2 [dict get $overstacks $idx_over] - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } - } - } - } - - #cursor movement? - #if {![punk::ansi::codetype::is_sgr $code]} { - # - #} - } - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - - #coalesce and replay codestacks for out char list - set outstring "" - set remstring "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set out_rawchars ""; #for overflow counting - set output_to "outstring" ;#var in effect depending on overflow - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $out { - append out_rawchars $ch - if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] < $num_under_columns} { - } else { - #todo - check if we overflowed with a double-width char ? - #store visualwidth which may be short - set in_overflow 1 - } - } - set cstack [dict get $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append $output_to \033\[m - } - foreach code $cstack { - append $output_to $code - } - } - append $output_to $ch - set prevstack $cstack - incr i - if {$in_overflow} { - set output_to "remstring" - } - } - if {[dict size $understacks] > 0} { - append $output_to [join [dict get $understacks [expr {[dict size $understacks]-1}]] ""] ;#tail codes - } - if {[string length $remstring]} { - #puts stderr "remainder:$remstring" - } - #pdict $understacks - if {$opt_returnextra} { - return [list $outstring $visualwidth [string length $outstring] $remstring] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#same as textblock::size - but we don't want that circular dependency -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - set textblock [textutil::tabify::untabify2 $textblock] - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - set textblock [punk::ansi::stripansi $textblock] - if {[string first \n $textblock] >= 0} { - set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::string_width $v}]] - } else { - set width [punk::char::string_width $textblock] - } - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - - #is actually addgrapheme? - proc render_addchar {i c stack} { - upvar out o - upvar understacks ustacks - set nxt [llength $o] - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - dict set ustacks $i $stack - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.6 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.5.7.tm b/src/bootsupport/modules/overtype-1.5.7.tm deleted file mode 100644 index aefb0840..00000000 --- a/src/bootsupport/modules/overtype-1.5.7.tm +++ /dev/null @@ -1,998 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.7 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.5.7] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::string_width $text] -} - - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set right_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set undertext_printlen [punk::ansi::printing_length $undertext] - if {$undertext_printlen < $colwidth} { - set udiff [expr {$colwidth - $undertext_printlen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set overtext_printlen [punk::ansi::printing_length $overtext] - set overflowlength [expr {$overtext_printlen - $colwidth}] - - #review - #append overtext "\033\[0m" - - - if {$overflowlength > 0} { - #background line is narrower than data in line - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #we know overtext data is shorter or equal (for this line) - lappend outputlines [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rendered [renderline -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - lappend outputlines [renderline -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - } - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set left_exposed $under_exposed_max - - set outputlines [list] - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - puts xxx - set undertext "$undertext[string repeat { } $udiff]" - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - #padding always on right - if alignment is required it should be done to block beforehand - not here - set overtextpadding "$overtext[string repeat { } $odiff]" - } - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rendered [renderline -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn 1 $undertext $overtext] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - lappend outputlines [renderline -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - } - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#-returnextra to enable returning of overflow and length -# todo - use punk::ansi::ta::detect to short-circuit processing and do simple string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters! -#bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - #should also rule out \v - if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines" - } - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -startcolumn 1\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - - set known_opts [dict keys $defaults] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - if {$k ni $known_opts} { - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#start cursor column - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [dict create] - - set i_u -1 - set i_o 0 - set outcols [list] - set u_codestack [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach grapheme [punk::char::grapheme_split $pt] { - set width [punk::char::string_width $grapheme] - incr i_u - dict set understacks $i_u $u_codestack - lappend outcols $grapheme - if {$width > 1} { - incr i_u - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - dict set understacks $i_u $u_codestack - lappend outcols "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - lappend u_codestack $code - } - #consider also if there are other codes that should be stacked..? - } - #trailing codes in effect for underlay - if {[llength $undermap]} { - dict set understacks [expr {$i_u + 1}] $u_codestack - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad [string repeat " " [expr {$opt_colstart -1}]] - append startpad $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad] - #### - set colcursor $opt_colstart - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - - set overstacks [dict create] - set o_codestack [list] - set pt_overchars "" - foreach {pt code} $overmap { - append pt_overchars $pt - foreach grapheme [punk::char::grapheme_split $pt] { - dict set overstacks $i_o $o_codestack - incr i_o - } - - if {[punk::ansi::codetype::is_sgr $code]} { - if {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #m code which has sgr reset at start - no need to replay prior sgr codes - set o_codestack [list $code] - } else { - lappend o_codestack $code - } - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - lappend o_codestack $code - } - - } - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - set bs [format %c 0x08] - set idx 0 ;# line index (cursor - 1) - set idx_over -1 - foreach {pt code} $overmap { - #set ptchars [split $pt ""] ;#for lookahead - set overlay_graphemes [punk::char::grapheme_split $pt] - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - foreach ch $overlay_graphemes { - set within_undercols [expr {$idx <= [llength $outcols]-1}] - incr idx_over - if {$ch eq "\r"} { - set idx [expr {$opt_colstart -1}] - } elseif {$ch eq "\b"} { - #review - backspace effect on double-width chars - if {$idx > ($opt_colstart -1)} { - incr idx -1 - } - } elseif {($idx < ($opt_colstart -1))} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - dict set understacks $idx [list] ;#review - use idx-1 codestack? - incr idx - } else { - set uwidth [punk::char::string_width [lindex $outcols $idx]] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - } elseif {$uwidth == 1} { - set owidth [punk::char::string_width $ch] - incr idx - if {$owidth > 1} { - incr idx - } - } elseif {$uwidth > 1} { - if {[punk::char::string_width $ch] == 1} { - #normal singlewide transparency - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] - incr idx - } - } - } else { - #2wide transparency over 2wide in underlay - incr idx - } - } - } - } else { - #non-transparent char in overlay - set uwidth [punk::char::string_width [lindex $outcols $idx]] - - if {$within_undercols && [lindex $outcols $idx] eq ""} { - #2nd col of 2wide char in underlay - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [dict get $understacks [expr {$idx-1}]] - } - incr idx - - } elseif {$uwidth == 0} { - if {$within_undercols} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - #overflow - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } - } elseif {$uwidth == 1} { - set owidth [punk::char::string_width $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - } else { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx "" [dict get $overstacks $idx_over] - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {[llength $outcols] >= [expr {$idx +2}] && [lindex $outcols $idx+1] eq ""} { - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [dict get $understacks [expr {$idx+1}]] - } - incr idx - } - } elseif {$uwidth > 1} { - set owidth [punk::char::string_width $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx - priv::render_addchar $idx $opt_exposed2 [dict get $overstacks $idx_over] - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [dict get $overstacks $idx_over] - incr idx 2 - } - } - } - } - - #cursor movement? - #if {![punk::ansi::codetype::is_sgr $code]} { - # - #} - if {[punk::ansi::codetype::is_cursor_move_in_line $code]} { - } - set re_col_move {\x1b\[([0-9]*)(C|D|G)} - if {[regexp $re_col_move $code _match num type]} { - if {$type eq "C"} { - #left-arrow/move-back - if {$num eq ""} {set num 1} - incr idx -$num - if {$idx < $opt_colstart} { - set idx $opt_colstart - } - } elseif {$type eq "D"} { - #right-arrow/move forward - if {$num eq ""} {set num 1} - if {!$opt_overflow || ($idx + $num) <= [llength $outcols]-1} { - incr idx $num - if {$idx > [llength $outcols]-1} { - set idx [llength $outcols] -1 - } - } else { - set idxstart $idx - set idxend [expr {[llength $outcols]-1}] - set moveend [expr {$idxend - $idxstart}] - incr idx $moveend - set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - priv::render_addchar $idx " " $stackinfo - } - } - } elseif {$type eq "G"} { - #move absolute column - #adjust to colstart - as column 1 is within overlay - #ie - set num [expr {$num + $opt_colstart}] - error "renderline absolute col move ESC G unimplemented" - } - - } - } - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" - set remstring "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set out_rawchars ""; #for overflow counting - set output_to "outstring" ;#var in effect depending on overflow - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $outcols { - append out_rawchars $ch - if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] > $num_under_columns} { - #todo - check if we overflowed with a double-width char ? - #store visualwidth which may be short - set in_overflow 1 - } - } - if {$in_overflow} { - set output_to "remstring" - } - set cstack [dict get $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append $output_to \033\[m - } - foreach code $cstack { - append $output_to $code - } - } - append $output_to $ch - set prevstack $cstack - incr i - } - if {[dict size $understacks] > 0} { - append $output_to [join [dict get $understacks [expr {[dict size $understacks]-1}]] ""] ;#tail codes - } - if {[string length $remstring]} { - #puts stderr "remainder:$remstring" - } - #pdict $understacks - if {$opt_returnextra} { - set cursorinfo "" - return [list result $outstring visualwidth - stringlen [string length $outstring] remainder $remstring cursor [expr {$idx + 1}]] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - set textblock [textutil::tabify::untabify2 $textblock] - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - set textblock [punk::ansi::stripansi $textblock] - if {[string first \n $textblock] >= 0} { - set width [tcl::mathfunc::max {*}[lmap v [punk::lib::lines_as_list -- $textblock] {::punk::char::string_width $v}]] - } else { - set width [punk::char::string_width $textblock] - } - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - - #is actually addgrapheme? - proc render_addchar {i c stack} { - upvar outcols o - upvar understacks ustacks - set nxt [llength $o] - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - dict set ustacks $i $stack - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.7 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.5.9.tm b/src/bootsupport/modules/overtype-1.5.9.tm deleted file mode 100644 index a635158a..00000000 --- a/src/bootsupport/modules/overtype-1.5.9.tm +++ /dev/null @@ -1,1756 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.5.9 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.5.9] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable grapheme_widths [dict create] - - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::ansifreestring_width $text] -} - - -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -proc overtype::left {args} { - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - set right_exposed $under_exposed_max - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $underlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set undertext_printlen [punk::ansi::printing_length $undertext] - if {$undertext_printlen < $colwidth} { - set udiff [expr {$colwidth - $undertext_printlen}] - append undertext [string repeat { } $udiff] - } - set overtext_printlen [punk::ansi::printing_length $overtext] - set overflowlength [expr {$overtext_printlen - $colwidth}] - - - set overtext [string cat $replay_codes_overlay $overtext] - set undertext [string cat $replay_codes_underlay $undertext] - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow [dict get $opts -overflow] $undertext $overtext] - set rendered [dict get $rinfo result] - if {$overflowlength > 0} { - #background line is narrower than data in line - - if {![dict get $opts -overflow]} { - #set overtext [string range $overtext 0 $colwidth-1] ;#string range won't be correct e.g if contains ansi codes or leading \r or \b etc - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #review - ansi codes in overlay's overflow? - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - #lappend outputlines $rendered - } - #we know overtext data is shorter or equal (for this line) - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - #set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - #lappend outputlines [dict get $rinfo result] - lappend outputlines $rendered - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] - -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [dict get $rinfo result] - #todo - get replay_codes from overflow_right instead of wherever it was truncated? - - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -align "left"\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_align [dict get $opts -align] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$colwidth - $overblock_width)}] - set left_exposed $under_exposed_max - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_align { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set ellipsis [string cat $replay_codes $opt_ellipsistext] - #todo - overflow on left if allign = right?? - set rendered [overtype::right $rendered $ellipsis] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#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 overtype::grapheme_width_cached {ch} { - variable grapheme_widths - if {[dict exists $grapheme_widths $ch]} { - return [dict get $grapheme_widths $ch] - } - set width [punk::char::ansifreestring_width $ch] - dict set grapheme_widths $ch $width - return $width -} - -#v2 -#-returnextra to enable returning of overflow and length -#review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#todo - review transparency issues with single/double width characters -#bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? -proc overtype::renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines in undertext" - } - #if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - # error "overtype::renderline not allowed to contain newlines" - #} - - set defaults [dict create\ - -overflow 0\ - -transparent 0\ - -startcolumn 1\ - -cursor_column 1\ - -cursor_row ""\ - -insert_mode 1\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return - #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs - - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error - - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -info - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [dict get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [dict get $opts -cursor_row] - if {[string length $opt_row_context]} { - if {![string is integer -strict $opt_row_context] || $opt_row_context <1 } { - error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" - } - } - set opt_insert_mode [dict get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) - #default is for overtype - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - if {$opt_row_context eq ""} { - set cursor_row 0 ;#we aren't allowed to make assumptions about our context. zero represents cursor_row_change - not an absolute row (for which zero is invalid anyway) - } else { - set cursor_row "=$opt_row_context" ;#we are at this row number in the greater context - allow moves that explicitly refer to this row without returning prematurely - } - - - #----- - # - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review - } - set overdata $over - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over 8] - } - #------- - - #ta_detect ansi and do simpler processing? - - #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column, - #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway. - - # -- --- --- --- --- --- --- --- - set undermap [punk::ansi::ta::split_codes_single $under] - set understacks [list] - - set i_u -1 ;#underlay may legitimately be empty - set undercols [list] - set u_codestack [list] - #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway - set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics) - #set understacks_gx [dict create] - set understacks_gx [list] - set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - append pt_underchars $pt - foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. - #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - set width 1 - } - default { - set width [grapheme_width_cached $grapheme] - } - } - #set width [grapheme_width_cached $grapheme] - incr i_u - #dict set understacks $i_u $u_codestack - lappend understacks $u_codestack - #dict set understacks_gx $i_u $u_gx_stack - lappend understacks_gx $u_gx_stack - - lappend undercols $grapheme - if {$width > 1} { - incr i_u - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - #dict set understacks $i_u $u_codestack - lappend understacks $u_codestack - #dict set understacks_gx $i_u $u_gx_stack - lappend understacks_gx $u_gx_stack - lappend undercols "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - lappend u_codestack $code - } else { - #leave SGR stack as is - if {[punk::ansi::codetype::is_gx_open $code]} { - set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set u_gx_stack [list] - } - } - } - - #consider also if there are other codes that should be stacked..? - } - - #trailing codes in effect for underlay - #replay code for last overlay position in input line - # whether or not we get that far - we need to return it for possible replay on next line - if {[llength $undermap]} { - #dict set understacks [expr {$i_u + 1}] $u_codestack ;#This is one column higher than our input - lappend understacks $u_codestack - set replay_codes_underlay [join $u_codestack ""] - - # For gx we need the column after the data too ? - #dict set understacks_gx [expr {$i_u +1}] $u_gx_stack - lappend understacks_gx $u_gx_stack - } else { - set replay_codes_underlay "" - #in case overlay onto emptystring as underlay - #dict set understacks 0 [list] - lappend understacks [list] - #dict set understacks_gx 0 [list] - lappend understacks_gx [list] - } - #note - be careful.. understacks 1 bigger than input - for insertion at end (review) - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char really) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad_overlay [string repeat " " [expr {$opt_colstart -1}]] - append startpad_overlay $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] - #### - - #??? - set colcursor $opt_colstart - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - - #set overstacks [dict create] - set overstacks [list] - set overstacks_gx [list] - - set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc) - set o_gxstack [list] - set pt_overchars "" - set i_o 0 - set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use - foreach {pt code} $overmap { - append pt_overchars $pt - #will get empty pt between adjacent codes - foreach grapheme [punk::char::grapheme_split $pt] { - #dict set overstacks $i_o $o_codestack - lappend overstacks $o_codestack - #dict set overstacks_gx $i_o $o_gxstack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split) - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - lappend o_codestack $code - lappend overlay_grapheme_control_list [list sgr $code] - } else { - if {[punk::ansi::codetype::is_gx_open $code]} { - set o_gxstack [list "gx0_on"] - lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set o_gxstack [list] - lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } else { - lappend overlay_grapheme_control_list [list other $code] - } - } - } - - } - #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme - set max_overlay_grapheme_index [expr {$i_o -1}] - #dict set overstacks $i_o $o_codestack - lappend overstacks $o_codestack - #dict set overstacks_gx $i_o $o_gxstack - lappend overstacks_gx $o_gxstack - set replay_codes_overlay [join $o_codestack ""] - - #if {[dict exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [dict get $overstacks $max_overlay_grapheme_index] ""] - #} else { - # set replay_codes_overlay "" - #} - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - #we need to initialise overflow_idx before any potential row-movements - as they need to break and force in_overflow to 1 - if {$opt_overflow} { - set overflow_idx -1 - } else { - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to. - - set unapplied "" ;#if we break for move row (but not for /v ?) - set insert_lines_above 0 ;#return key - set insert_lines_below 0 - - #set idx 0 ;# line index (cursor - 1) - #set idx [expr {$opt_colstart + $opt_colcursor} -1] - set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1 - set cursor_column $opt_colcursor - set idx_over -1 - - - #movements only occur within the overlay range. - #an underlay is however not necessary.. e.g - #renderline -overflow 1 "" data - #foreach {pt code} $overmap {} - set insert_mode $opt_insert_mode ;#default 1 - set in_overflow 0 - #puts "-->$overlay_grapheme_control_list<--" - #puts "-->overflow_idx: $overflow_idx" - for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} { - set gc [lindex $overlay_grapheme_control_list $gci] - lassign $gc type item - - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - #foreach ch $overlay_graphemes {} - switch -- $type { - g { - set ch $item - incr idx_over - if {$overflow_idx != -1 && $idx == $overflow_idx } { - #review 2w overflow? - set in_overflow 1 - #first overstack codeset only - #priv::render_addchar $idx $ch [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - set cursor_column [expr {$idx + 1}] - continue - } - - set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our original data width - - if {$in_overflow} { - #render any char - even \b\v\r into outcols - will become part of overflow - #no stacks added from here on - raw codes go into overflow/remainder - priv::render_addchar $idx $ch [list] [list] $insert_mode - incr idx ;#width doesn't matter from here - } else { - if {($idx < ($opt_colstart -1))} { - incr idx - } elseif {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - #dict set understacks $idx [list] ;#review - use idx-1 codestack? - lset understacks $idx [list] - incr idx - incr cursor_column - } else { - #todo - punk::char::char_width - set g [lindex $outcols $idx] - set uwidth [grapheme_width_cached $g] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - incr cursor_column - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - incr cursor_column - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - incr idx - incr cursor_column - if {$owidth > 1} { - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - if {[grapheme_width_cached $ch] == 1} { - if {!$insert_mode} { - #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - incr cursor_column - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - incr cursor_column - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } else { - #? todo - decide what transparency even means for insert mode - incr idx - incr cursor_column - } - } else { - #2wide transparency over 2wide in underlay - review - incr idx - incr cursor_column - } - } - } - } else { - set chtest [string map [list \n \b \r \v ] $ch] - switch -- $chtest { - "" { - switch -- [string index $cursor_row 0] { - 0 { - incr cursor_row - } - = { - #deliberately keeping literals in switch statement rather than matching on =$opt_row_context directly (keep switch byte-compiled) - if {$cursor_row eq "=$opt_row_context"} { - set cursor_row "=[expr {$opt_row_context +1}]" ;#we can return an absolute next cursor row - } else { - error "overtype::renderline bad cursor_row $cursor_row encountered when lf encountered" - } - } - default { - #we should have already returned if cursor_row is not 1 or "=$opt_row_context" - error "overtype::renderline bad cursor_row $cursor_row encountered when lf encountered." - } - } - - #override overflow_idx even if it was set to -1 due to opt_overflow = 1 - set overflow_idx $idx - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - append unapplied [join [lindex $overstacks $idx_over] ""] - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - append unapplied "\x1b(0" - } - "gx0_off" { - append unapplied "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - append unapplied "\x1b(0" - } elseif {$item eq "gx0_off"} { - append unapplied "\x1b(B" - } - } else { - append unapplied $item - } - incr idx_over - } - if {$idx == 0} { - set insert_lines_above 1 - } else { - set insert_lines_below 1 - } - break - set cursor_column 1 - } - "" { - set idx [expr {$opt_colstart -1}] - set cursor_column 1 ;#? - } - "" { - #literal backspace char - not necessarily from keyboard - #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype - #(important for -transparent option - hence replacement chars for half-exposed etc) - #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR) - if {$idx > ($opt_colstart -1)} { - incr idx -1 - incr cursor_column -1 - } - } - "" { - #end processing this overline. rest of line is remainder. cursor for column as is. - if {$cursor_row eq 0} { - incr cursor_row - } elseif {$cursor_row eq "=$opt_row_context"} { - set cursor_row "=[expr {$opt_row_context +1}]" ;#we can return an absolute next cursor row - } else { - #we should have already returned if cursor_row is not 1 or "=$opt_row_context" - error "overtype::renderline bad cursor_row $cursor_row encountered when \v encountered" - } - #override overflow_idx even if it was set to -1 due to opt_overflow = 1 - set overflow_idx [expr $idx] - set in_overflow 1 - incr idx - #keep cursor_column at same column - #break - } - default { - - #non-transparent char in overlay - set uwidth [grapheme_width_cached [lindex $outcols $idx]] - - if {$within_undercols && [lindex $outcols $idx] eq ""} { - #2nd col of 2wide char in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??) - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks [expr {$idx-1}]] [lindex $understacks_gx [expr {$idx-1}]] 0 - } - incr idx - incr cursor_column - } else { - set prevcolinfo [lindex $outcols $idx-1] - #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right - #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?) - #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char - #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises - #It is perhaps best avoided at another level and try to make renderline do exactly as it's told - #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect. - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index - if {$prevcolinfo ne ""} { - #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks [expr {$idx-1}]] [lindex $understacks_gx [expr {$idx-1}]] 0 ;#replace not insert - } ;# else?? - incr idx - incr cursor_column - } - - } elseif {$uwidth == 0} { - if {$within_undercols} { - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column 2 - } else { - #overflow - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {[llength $outcols] >= [expr {$idx +2}] && [lindex $outcols $idx+1] eq ""} { - #priv::render_addchar [expr {$idx+1}] $opt_exposed2 [dict get $understacks [expr {$idx+1}]] [dict get $understacks_gx [expr {$idx+1}]] $insert_mode - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks [expr {$idx+1}]] [lindex $understacks_gx [expr {$idx+1}]] $insert_mode - } - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - #1wide over 2wide in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #insert mode just pushes all to right - no exposition char here - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx 2 - incr cursor_column 2 - } - } - } - } ;# end switch - } - } - } - other { - set code $item - if {$in_overflow} { - #render controls into overflow/remainder output - priv::render_addchar $idx $code [list] [list] $insert_mode - incr idx ;#take up a column for each control sequence too - continue - } - - #cursor movement? - #if {![punk::ansi::codetype::is_sgr $code]} { - # - #} - #if {[punk::ansi::codetype::is_cursor_move_in_line $code]} { - #} - set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - set re_row_move {\x1b\[([0-9]*)(A|B)$} - set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} - set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - set matchinfo [list] - - switch -regexp -matchvar matchinfo -- $code\ - $re_col_move { - lassign $matchinfo _match num type - switch -- $type { - D { - #left-arrow/move-back - if {$num eq ""} {set num 1} - incr idx -$num - incr cursor_column -$num - if {$idx < $opt_colstart-1} { - #wrap to previous line and position cursor at end of data - set idx [expr {$opt_colstart-1}] - set cursor_column $opt_colstart - } - } - C { - #right-arrow/move forward - if {$num eq ""} {set num 1} - - #todo - retrict to moving 1 position past datalen? restrict to column width? - #should ideally wrap to next line - if {!$opt_overflow || ($idx + $num) <= [llength $outcols]} { - incr idx $num - incr cursor_column $num - - if {$idx > [llength $outcols]+1} { - #set idx [expr {[llength $outcols] -1}] - set idx [expr {[llength $outcols]+2}] - set cursor_column [expr {[llength $outcols]+1}] - } - } else { - if {!$insert_mode} { - #block editing style with arrow keys - #overtype mode - set idxstart $idx - set idxend [expr {[llength $outcols]}] - set moveend [expr {$idxend - $idxstart}] - if {$moveend < 0} {set moveend 0} ;#sanity? - #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" - incr idx $moveend - incr cursor_column $moveend - #if {[dict exists $understacks $idx]} { - # set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #} else { - # set stackinfo [list] - #} - if {$idx < [llength $understacks]} { - set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - } else { - set stackinfo [list] - } - if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [dict get $understacks_gx $idx] - set gxstackinfo [lindex $understacks_gx $idx] - } else { - set gxstackinfo [list] - } - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - incr cursor_column - priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode - } - } else { - #normal - insert - incr idx $num - incr cursor_column $num - if {$idx > [llength $outcols]} { - set idx [expr {[llength $outcols]}];#allow one beyond - for adding character at end of line - set cursor_column [expr {[llength $outcols]+1}] - } - } - } - } - G { - #move absolute column - #adjust to colstart - as column 1 is within overlay - #??? - set idx [expr {$num + $opt_colstart -1}] - set cursor_column $num - error "renderline absolute col move ESC G unimplemented" - } - } - }\ - $re_row_move { - lassign $matchinfo _match num type - switch -- $type { - A { - #move up - if {$num eq ""} {set num 1} - if {$cursor_row eq 0} { - incr cursor_row -1 ;#relative change - } elseif {$cursor_row eq "=$opt_row_context"} { - set cursor_row "=[expr {$opt_row_context -1}]" ;#we can return an absolute next cursor row - } else { - #we should have already returned if cursor_row is not 1 or "=$opt_row_context" - error "overtype::renderline bad cursor_row $cursor_row encountered when \v encountered" - } - #ensure rest of *overlay* is emitted to remainder - incr idx - #retain cursor_column - break - } - B { - #move down - if {$num eq ""} {set num 1} - if {$cursor_row eq 0} { - incr cursor_row 1 ;#relative change - } elseif {$cursor_row eq "=$opt_row_context"} { - set cursor_row "=[expr {$opt_row_context +1}]" ;#we can return an absolute next cursor row - } else { - #we should have already returned if cursor_row is not 1 or "=$opt_row_context" - error "overtype::renderline bad cursor_row $cursor_row encountered when \v encountered" - } - #set overflow_idx $idx ;#ensure rest of line is emitted to remainder - incr idx - #retain cursor_column - break - } - } - }\ - $re_both_move { - lassign $matchinfo _match row col - if {$row eq ""} {set row 1} - if {$col eq ""} {set col 1} - set cursor_column $col - }\ - $re_vt_sequence { - lassign $matchinfo _match key mod - #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~ - # - #e.g esc \[2~ insert esc \[2;2~ shift-insert - #mod - subtract 1, and then use bitmask - #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?) - #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]" - if {$key eq "1"} { - #home - } elseif {$key eq "2"} { - #Insert - if {$mod eq ""} { - #no modifier key - set insert_mode [expr {!$insert_mode}] - #rather than set the cursor - we return the insert mode state so the caller can decide - } - } elseif {$key eq "3"} { - #Delete - switch -- $mod { - "" { - priv::render_delchar $idx - } - "5" { - #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?) - } - } - } elseif {$key eq "4"} { - #End - } elseif {$key eq "5"} { - #pgup - } elseif {$key eq "6"} { - #pgDn - } elseif {$key eq "7"} { - #Home - #?? - set idx [expr {$opt_colstart -1}] - set cursor_column 1 - } elseif {$key eq "8"} { - #End - } elseif {$key eq "11"} { - #F1 - or ESCOP or e.g shift F1 ESC\[1;2P - } elseif {$key eq "12"} { - #F2 - or ESCOQ - } elseif {$key eq "13"} { - #F3 - or ESCOR - } elseif {$key eq "14"} { - #F4 - or ESCOS - } elseif {$key eq "15"} { - #F5 or shift F5 ESC\[15;2~ - } elseif {$key eq "17"} { - #F6 - } elseif {$key eq "18"} { - #F7 - } elseif {$key eq "19"} { - #F8 - } elseif {$key eq "20"} { - #F9 - } elseif {$key eq "21"} { - #F10 - } elseif {$key eq "23"} { - #F11 - } elseif {$key eq "24"} { - #F12 - } - - } - } - sgr { - #prior to overflow - we have our sgr codes already in stacks - #post-overflow we need to keep them in order along with non sgr codes and graphemes - if {$in_overflow} { - set code $item - #render controls into output - will become overflow/remainder - priv::render_addchar $idx $code [list] [list] $insert_mode - incr idx ;#take up an overflow column for each control sequence too - } - } - gx0 { - if {$in_overflow} { - set code $item - if {$code eq "gx0_on"} { - set actual "\x1b(0" - } else { - set actual "\x1b(B" - } - priv::render_addchar $idx $actual [list] [list] $insert_mode - incr idx - } - - } - } - } - - #-------------- - #if {$in_overflow} { - # #set cursor_column [expr {$overflow_idx -1}] - # set cursor_column [expr {$overflow_idx +1}] - #} else { - # set cursor_column [expr {$idx + 1}] - #} - - - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - if {$overflow_idx == -1} { - #overflow was initially unlimited and hasn't been overridden - } else { - - } - - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" ;#output prior to overflow - set overflow_right "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set prev_g0 [list] - #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves - set in_overflow 0 ;#used to stop char-width scanning once in overflow - foreach ch $outcols { - #puts "---- [ansistring VIEW $ch]" - - if {$in_overflow} { - #ch could be a control-sequence or a grapheme once in overflow - if {$i == $overflow_idx} { - #only run when we exactly hit overflow_idx - if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {[llength $g0]} { - append outstring "\x1b(B" - } - } - #add first codestack only - if {$i < [llength $understacks]} { - set cstack [lindex $understacks $i] - append overflow_right [join $cstack ""] - } - } - append overflow_right $ch - } else { - if {$overflow_idx != -1 && $i+1 == $overflow_idx} { - #one before overflow - #will be in overflow in next iteration - set in_overflow 1 - - if {[grapheme_width_cached $ch]> 1} { - #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide) - set ch $opt_exposed1 - } - } - - if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {$g0 ne $prev_g0} { - if {$g0 eq [list "gx0_on"]} { - append outstring "\x1b(0" - } else { - append outstring "\x1b(B" - } - } - set prev_g0 $g0 - } else { - set prev_g0 [list] - } - #code replay when not in overflow - if {$i < [llength $understacks]} { - #set cstack [dict get $understacks $i] - set cstack [lindex $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack]} { - append outstring \033\[m - } - append outstring [join $cstack ""] - } - set prevstack $cstack - } else { - set prevstack [list] - } - append outstring $ch - } - incr i - } - - set replay_codes "" - if {[llength $understacks] > 0} { - if {$overflow_idx == -1} { - #set tail_idx [dict size $understacks] - set tail_idx [llength $understacks] - } else { - set tail_idx [llength $undercols] - } - #if {[dict exists $understacks [expr {$tail_idx-1}]]} { - # set replay_codes [join [dict get $understacks [expr {$tail_idx-1}]] ""] ;#tail replay codes - #} - if {$tail_idx-1 < [llength $understacks]} { - set replay_codes [join [lindex $understacks [expr {$tail_idx-1}]] ""] ;#tail replay codes - } - if {$tail_idx-1 < [llength $understacks_gx]} { - #set gx0 [dict get $understacks_gx [expr {$tail_idx-1}]] - set gx0 [lindex $understacks_gx [expr {$tail_idx-1}]] - if {$gx0 eq [list "gx0_on"]} { - #if it was on, turn gx0 off at the point we stop processing overlay - append outstring "\x1b(B" - } - } - } - if {[string length $overflow_right]} { - #puts stderr "remainder:$overflow_right" - } - #pdict $understacks - if {[punk::ansi::ta::detect_sgr $outstring]} { - append outstring [punk::ansi::a] - } - if {$opt_returnextra} { - #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review - #replay_codes_underlay is the set of codes in effect at the very end of the original underlay - #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied) - #todo - replay_codes for gx0 mode - return [list\ - result $outstring\ - visualwidth [punk::ansi::printing_length $outstring]\ - stringlen [string length $outstring]\ - overflow_idx $overflow_idx\ - overflow_right $overflow_right\ - unapplied $unapplied\ - insert_mode $insert_mode\ - insert_lines_above $insert_lines_above\ - insert_lines_below $insert_lines_below\ - cursor_column $cursor_column\ - cursor_row_change $cursor_row\ - replay_codes $replay_codes\ - replay_codes_underlay $replay_codes_underlay\ - replay_codes_overlay $replay_codes_overlay\ - ] - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - if {[string first \t $textblock] >= 0} { - set textblock [textutil::tabify::untabify2 $textblock] - } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] - } - if {[string first \n $textblock] >= 0} { - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] - } else { - set num_le 0 - set width [punk::char::ansifreestring_width $textblock] - } - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - variable cache_is_sgr [dict create] - - #we are likely to be asking the same question of the same ansi codes repeatedly - #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS - #todo - test if still worthwhile after a large cache is built up. (limit cache size?) - proc is_sgr {code} { - variable cache_is_sgr - if {[dict exists $cache_is_sgr $code]} { - return [dict get $cache_is_sgr $code] - } - set answer [punk::ansi::codetype::is_sgr $code] - dict set cache_is_sgr $code $answer - return $answer - } - proc render_delchar {i} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - set nxt [llength $o] - if {$i < $nxt} { - set o [lreplace $o $i $i] - set ustacks [lreplace $ustacks $i $i] - set gxstacks [lreplace $gxstacks $i $i] - } else { - - } - } - #is actually addgrapheme? - proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - - set nxt [llength $o] - if {!$insert_mode} { - if {$i < $nxt} { - lset o $i $c - } else { - lappend o $c - } - #dict set ustacks $i $sgrstack - if {$i < [llength $ustacks]} { - lset ustacks $i $sgrstack - } else { - lappend ustacks $sgrstack - } - #dict set gxstacks $i $gx0stack - if {$i < [llength $gxstacks]} { - lset gxstacks $i $gx0stack - } else { - lappend gxstacks $gx0stack - } - } else { - #insert of single-width vs double-width when underlying is double-width? - if {$i < $nxt} { - set o [linsert $o $i $c] - } else { - lappend o $c - } - #rewrite our whole understacks - #for inserts - the dict structure of the ansi stacks is less than ideal. - #set new [dict create] - #dict for {k v} $ustacks { - # if {$k < $i} { - # dict set new $k $v - # } elseif {$k == $i} { - # dict set new $k $sgrstack - # dict set new [expr {$k+1}] $v - # } else { - # dict set new [expr {$k+1}] $v - # } - #} - #set ustacks $new - if {$i < [llength $ustacks]} { - set ustacks [linsert $ustacks $i $sgrstack] - } else { - lappend ustacks $sgrstack - } - - if {$i < [llength $gxstacks]} { - set gxstacks [linsert $gxstacks $i $gx0stack] - } else { - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.5.9 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/overtype-1.6.0.tm b/src/bootsupport/modules/overtype-1.6.0.tm deleted file mode 100644 index f5bdf82b..00000000 --- a/src/bootsupport/modules/overtype-1.6.0.tm +++ /dev/null @@ -1,3292 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.6.0 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.0] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6 -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -package require punk::assertion -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix string range -# - need to extract and replace ansi codes? - -namespace eval overtype { - namespace import ::punk::assertion::assert - punk::assertion::active true - - namespace path ::punk::lib - - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - namespace eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -namespace eval overtype { - variable grapheme_widths [dict create] - - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [dict create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - -#proc overtype::stripansi {text} { -# variable escape_terminals ;#dict -# variable ansi_2byte_codes_dict -# #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway -# if {[string first \033 $text] <0 && [string first \009c $text] <0} { -# #\033 same as \x1b -# return $text -# } -# -# set text [convert_g0 $text] -# -# #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. -# #line endings can theoretically occur within an ansi escape sequence (review e.g title?) -# set inputlist [split $text ""] -# set outputlist [list] -# -# set 2bytecodes [dict values $ansi_2byte_codes_dict] -# -# set in_escapesequence 0 -# #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls -# set i 0 -# foreach u $inputlist { -# set v [lindex $inputlist $i+1] -# set uv ${u}${v} -# if {$in_escapesequence eq "2b"} { -# #2nd byte - done. -# set in_escapesequence 0 -# } elseif {$in_escapesequence != 0} { -# set escseq [dict get $escape_terminals $in_escapesequence] -# if {$u in $escseq} { -# set in_escapesequence 0 -# } elseif {$uv in $escseq} { -# set in_escapseequence 2b ;#flag next byte as last in sequence -# } -# } else { -# #handle both 7-bit and 8-bit CSI and OSC -# if {[regexp {^(?:\033\[|\u009b)} $uv]} { -# set in_escapesequence CSI -# } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { -# set in_escapesequence OSC -# } elseif {$uv in $2bytecodes} { -# #self-contained e.g terminal reset - don't pass through. -# set in_escapesequence 2b -# } else { -# lappend outputlist $u -# } -# } -# incr i -# } -# return [join $outputlist ""] -#} - - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::stripansi. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::stripansi $text] - } - return [punk::char::ansifreestring_width $text] -} - -#todo - consider a way to merge overtype::left/centre/right -#These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock -#overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay. -#(i.e not even necessariy having it's top left within the underlay) -namespace eval overtype::priv { -} - -#could return larger than colwidth -proc _get_row_append_column {row} { - upvar outputlines outputlines - set idx [expr {$row -1}] - if {$row <= 1 || $row > [llength $outputlines]} { - return 1 - } else { - upvar opt_overflow opt_overflow - upvar colwidth colwidth - set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]] - set endpos [expr {$existinglen +1}] - if {$opt_overflow} { - return $endpos - } else { - if {$endpos > $colwidth} { - return $colwidth + 1 - } else { - return $endpos - } - } - } -} -#string range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r -#render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string. -#The underlay and overlay can be multiline blocks of text of varying line lengths. -#The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text. -#This is a single-shot rendering of strings - ie there is no way to chain another call containing a cursor-restore to previously rendered output and have it know about any cursor-saves in the first call. -# a cursor start position other than top-left is a possible addition to consider. -#see editbuf in punk::repl for a more stateful ansi-processor. Both systems use loops over overtype::renderline -proc overtype::left {args} { - #*** !doctools - #[call [fun overtype::left] [arg args] ] - #[para] usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext - - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - lassign [lrange $args end-1 end] underblock overblock - set defaults [dict create\ - -bias ignored\ - -width \uFFEF\ - -height \uFFeF\ - -wrap 0\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -appendlines 1\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -experimental 0\ - -looplimit 100000\ - ] - #-ellipsis args not used if -wrap is true - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -looplimit - -width - -height - -bias - -wrap - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -appendlines - -transparent - -exposed1 - -exposed2 - -experimental {} - default { - set known_opts [dict keys $defaults] - error "overtype::left unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_overflow [dict get $opts -overflow] - ##### - # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. - set opt_wrap [dict get $opts -wrap] ;#wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) - ##### - #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. - set opt_width [dict get $opts -width] - set opt_height [dict get $opts -height] - set opt_appendlines [dict get $opts -appendlines] - set opt_transparent [dict get $opts -transparent] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [dict get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - - #a hack until we work out how to avoid infinite loops... - # - set looplimit [dict get $opts -looplimit] - - # ---------------------------- - # -experimental dev flag to set flags etc - # ---------------------------- - set data_mode 0 - set test_mode 1 - set info_mode 0 - set edit_mode 0 - set opt_experimental [dict get $opts -experimental] - foreach o $opt_experimental { - switch -- $o { - test_mode { - set test_mode 1 - set info_mode 1 - } - old_mode { - set test_mode 0 - set info_mode 1 - } - data_mode { - set data_mode 1 - } - info_mode { - set info_mode 1 - } - edit_mode { - set edit_mode 1 - } - } - } - # ---------------------------- - - #modes - set insert_mode 0 ;#can be toggled by insert key or ansi IRM sequence ESC [ 4 h|l - set autowrap_mode $opt_wrap - set reverse_mode 0 - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - - #set underlines [split $underblock \n] - - #underblock is a 'rendered' block - so width height make sense - if {$opt_width eq "\uFFEF"} { - lassign [blocksize $underblock] _w colwidth _h colheight - } else { - set colwidth $opt_width - set colheight $opt_height - } - if {$underblock eq ""} { - set blank "\x1b\[0m\x1b\[0m" - #set underlines [list "\x1b\[0m\x1b\[0m"] - set underlines [lrepeat $colheight $blank] - } else { - set underlines [lines_as_list -ansiresets 1 $underblock] - } - - #todo - reconsider the 'line' as the natural chunking mechanism for the overlay. - #In practice an overlay ANSI stream can be a single line with ansi moves/restores etc - or even have no moves or newlines, just relying on wrapping at the output colwidth - #In such cases - we process the whole shebazzle for the first output line - only reducing by the applied amount at the head each time, reprocessing the long tail each time. - #(in cases where there are interline moves or cursor jumps anyway) - #This works - but doesn't seem efficient. - #On the other hand.. maybe it depends on the data. For simpler files it's more efficient than splitting first - - if {!$test_mode} { - set inputchunks [split $overblock \n] - } else { - set scheme 3 - switch -- $scheme { - 0 { - #one big chunk - set inputchunks [list $overblock] - } - 1 { - set inputchunks [punk::ansi::ta::split_codes $overblock] - } - 2 { - - #split into lines if possible first - then into plaintext/ansi-sequence chunks ? - set inputchunks [list ""] ;#put an empty plaintext split in for starters - set i 1 - set lines [split $overblock \n] - foreach ln $lines { - if {$i < [llength $lines]} { - append ln \n - } - set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single? - set lastpt [lindex $inputchunks end] - lset inputchunks end [string cat $lastpt [lindex $sequence_split 0]] - lappend inputchunks {*}[lrange $sequence_split 1 end] - incr i - } - } - 3 { - #it turns out line based chunks are faster than the above.. probably because some of those end up doing the regex splitting twice - set lflines [list] - set inputchunks [split $overblock \n] - foreach ln $inputchunks { - append ln \n - lappend lflines $ln - } - if {[llength $lflines]} { - lset lflines end [string range [lindex $lflines end] 0 end-1] - } - set inputchunks $lflines[unset lflines] - - } - } - } - - - #overblock height/width isn't useful in the presence of an ansi input overlay with movements. The number of lines may bear little relationship to the output height - #lassign [blocksize $overblock] _w overblock_width _h overblock_height - - - set replay_codes_underlay [dict create 1 ""] - #lappend replay_codes_overlay "" - set replay_codes_overlay "" - set unapplied "" - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - - - set outputlines $underlines - set overidx 0 - - #underlines are not necessarily processed in order - depending on cursor-moves applied from overtext - set row 1 - if {$data_mode} { - set col [_get_row_append_column $row] - } else { - set col 1 - } - - set instruction_stats [dict create] - - set loop 0 - #while {$overidx < [llength $inputchunks]} { } - - while {[llength $inputchunks]} { - #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx "" - set overtext [lpop inputchunks 0] - if {![string length $overtext]} { - continue - } - #puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----" - set undertext [lindex $outputlines [expr {$row -1}]] - set renderedrow $row - - #renderline pads each underaly line to width with spaces and should track where end of data is - - - #set overtext [string cat [lindex $replay_codes_overlay $overidx] $overtext] - set overtext [string cat $replay_codes_overlay $overtext] - if {[dict exists $replay_codes_underlay $row]} { - set undertext [string cat [dict get $replay_codes_underlay $row] $undertext] - } - #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary - - #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l - #set LASTCALL [list -info 1 -insert_mode $insert_mode -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set rinfo [renderline -experimental $opt_experimental -info 1 -insert_mode $insert_mode -cursor_restore_attributes $cursor_saved_attributes -autowrap_mode $autowrap_mode -transparent $opt_transparent -width $colwidth -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -cursor_column $col -cursor_row $row $undertext $overtext] - set instruction [dict get $rinfo instruction] - set insert_mode [dict get $rinfo insert_mode] - set autowrap_mode [dict get $rinfo autowrap_mode] ;# - #set reverse_mode [dict get $rinfo reverse_mode];#how to support in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set overflow_right_column [dict get $rinfo overflow_right_column] - set unapplied [dict get $rinfo unapplied] - set post_render_col [dict get $rinfo cursor_column] - set post_render_row [dict get $rinfo cursor_row] - set c_saved_pos [dict get $rinfo cursor_saved_position] - set c_saved_attributes [dict get $rinfo cursor_saved_attributes] - set visualwidth [dict get $rinfo visualwidth] - set insert_lines_above [dict get $rinfo insert_lines_above] - set insert_lines_below [dict get $rinfo insert_lines_below] - dict set replay_codes_underlay [expr {$renderedrow+1}] [dict get $rinfo replay_codes_underlay] - #lset replay_codes_overlay [expr $overidx+1] [dict get $rinfo replay_codes_overlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - - - - #-- todo - detect looping properly - if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} { - puts stderr "overtype::left loop?" - puts [ansistring VIEW $rinfo] - break - } - #-- - - if {[dict size $c_saved_pos] >= 1} { - set cursor_saved_position $c_saved_pos - set cursor_saved_attributes $c_saved_attributes - } - - #background line is narrower than data in line - - set overflow_handled 0 - if {!$opt_overflow && !$autowrap_mode} { - #not allowed to overflow column or wrap therefore we get overflow data to truncate - if {[dict get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim [ansistrip $lostdata]] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - set overflow_handled 1 - } else { - #no wrap - no ellipsis - silently truncate - set overflow_handled 1 - } - } - - - - set nextprefix "" - - - #todo - handle potential insertion mode as above for cursor restore? - #keeping separate branches for debugging - review and merge as appropriate when stable - dict incr instruction_stats $instruction - switch -- $instruction { - {} { - if {$test_mode == 0} { - incr row - if {$data_mode} { - set col [_get_row_append_column $row] - if {$col > $colwidth} { - - } - } else { - set col 1 - } - } else { - #lf included in data - set row $post_render_row - set col $post_render_col - - #set col 1 - #if {$post_render_row != $renderedrow} { - # set col 1 - #} else { - # set col $post_render_col - #} - } - } - up { - - #renderline knows it's own line number, and knows not to go above row l - #it knows that a move whilst 1-beyond the width conflicts with the linefeed and reduces the move by one accordingly. - #row returned should be correct. - #column may be the overflow column - as it likes to report that to the caller. - - #Note that an ansi up sequence after last column going up to a previous line and also beyond the last column, will result in the next grapheme going onto the following line. - #this seems correct - as the column remains beyond the right margin so subsequent chars wrap (?) review - #puts stderr "up $post_render_row" - #puts stderr "$rinfo" - - #puts stdout "1 row:$row col $col" - set row $post_render_row - #data_mode (naming?) determines if we move to end of existing data or not. - #data_mode 0 means ignore existing line length and go to exact column - #set by -experimental flag - if {$data_mode == 0} { - set col $post_render_col - } else { - #This doesn't really work if columns are pre-filled with spaces..we can't distinguish them from data - #we need renderline to return the number of the maximum column filled (or min if we ever do r-to-l) - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - - #puts stdout "2 row:$row col $col" - #puts stdout "-----------------------" - #puts stdout $rinfo - #flush stdout - } - down { - if {$data_mode == 0} { - #renderline doesn't know how far down we can go.. - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set row $post_render_row - set col $post_render_col - } else { - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - - } - } - restore_cursor { - #testfile belinda.ans uses this - - #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" - if {[dict exists $cursor_saved_position row]} { - set row [dict get $cursor_saved_position row] - set col [dict get $cursor_saved_position column] - #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]" - #set nextprefix $cursor_saved_attributes - #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes - set replay_codes_overlay [dict get $rinfo replay_codes_overlay]$cursor_saved_attributes - #set replay_codes_overlay $cursor_saved_attributes - set cursor_saved_position [dict create] - set cursor_saved_attributes "" - } else { - #TODO - #?restore without save? - #should move to home position and reset ansi SGR? - #puts stderr "overtype::left cursor_restore without save data available" - } - #If we were inserting prior to hitting the cursor_restore - there could be overflow_right data - generally the overtype functions aren't for inserting - but ansi can enable it - #if we were already in overflow when cursor_restore was hit - it shouldn't have been processed as an action - just stored. - if {!$overflow_handled && $overflow_right ne ""} { - #wrap before restore? - possible effect on saved cursor position - #this overflow data has previously been rendered so has no cursor movements or further save/restore operations etc - #we can just insert another call to renderline to solve this.. ? - #It would perhaps be more properly handled as a queue of instructions from our initial renderline call - #we don't need to worry about overflow next call (?)- but we should carry forward our gx and ansi stacks - - puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]" - - set sub_info [overtype::renderline -info 1 -width $colwidth -insert_mode $insert_mode -autowrap_mode $autowrap_mode -overflow [dict get $opts -overflow] "" $overflow_right] - set foldline [dict get $sub_info result] - set insert_mode [dict get $sub_info insert_mode] ;#probably not needed.. - set autowrap_mode [dict get $sub_info autowrap_mode] ;#nor this.. - linsert outputlines $renderedrow $foldline - #review - row & col set by restore - but not if there was no save.. - } - set overflow_handled 1 - - } - move { - ######## - if {$post_render_row > [llength $outputlines]} { - #Ansi moves need to create new lines ? - #if {$opt_appendlines} { - # set diff [expr {$post_render_row - [llength $outputlines]}] - # if {$diff > 0} { - # lappend outputlines {*}[lrepeat $diff ""] - # } - # set row $post_render_row - #} else { - set row [llength $outputlines] - #} - } else { - set row $post_render_row - } - ####### - set col $post_render_col - #overflow + unapplied? - } - lf_start { - #raw newlines - must be test_mode - # ---------------------- - #test with fruit.ans - #test - treating as newline below... - #append rendered $overflow_right - #set overflow_right "" - set row $renderedrow - incr row - if {$row > [llength $outputlines]} { - lappend outputlines "" - } - set col 1 - # ---------------------- - } - lf_mid { - - if 0 { - #set rhswidth [punk::ansi::printing_length $overflow_right] - #only show debug when we have overflow? - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - - set rhs "" - if {$overflow_right ne ""} { - set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $overflow_right]] - set rhs [textblock::frame -title overflow_right $rhs] - } - puts [textblock::join $lhs " $post_render_col " $rhs] - } - - if {!$test_mode} { - #rendered - append rendered $overflow_right - #set replay_codes_overlay "" - set overflow_right "" - - - set row $renderedrow - - set col 1 - incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - set edit_mode 0 - if {$edit_mode} { - set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied] - set overflow_right "" - set unapplied "" - set row $post_render_row - #set col $post_render_col - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - append rendered $overflow_right - set overflow_right "" - set row $post_render_row - set col 1 - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } - } - } - lf_overflow { - #linefeed after colwidth e.g at column 81 for an 80 col width - #we may also have other control sequences that came after col 80 e.g cursor save - - if 0 { - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - set rhs "" - - #assertion - there should be no overflow.. - puts $lhs - } - assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right - - set row $post_render_row - #set row $renderedrow - #incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - set col 1 - - } - newlines_above { - #we get a newlines_above instruction when received at column 1 - #In some cases we want to treat that as request to insert a new blank line above, and move our row 1 down (staying with the data) - #in other cases - we want to treat at column 1 the same as any other - - puts "--->newlines_above" - puts "rinfo: $rinfo" - #renderline doesn't advance the row for us - the caller has the choice to implement or not - set row $post_render_row - set col $post_render_col - if {$insert_lines_above > 0} { - set row $renderedrow - set outputlines [linsert $outputlines $renderedrow-1 {*}[lrepeat $insert_lines_above ""]] - incr row [expr {$insert_lines_above -1}] ;#we should end up on the same line of text (at a different index), with new empties inserted above - #? set row $post_render_row #can renderline tell us? - } - } - newlines_below { - #obsolete? - use for ANSI insert lines sequence - if {$data_mode == 0} { - puts --->nl_below - set row $post_render_row - set col $post_render_col - if {$insert_lines_below == 1} { - if {$test_mode == 0} { - set row $renderedrow - set outputlines [linsert $outputlines [expr {$renderedrow }] {*}[lrepeat $insert_lines_below ""]] ;#note - linsert can add to end too - incr row $insert_lines_below - set col 1 - } else { - #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]] - #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs] - #set rhs "" - #if {$overflow_right ne ""} { - # set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $overflow_right]] - # set rhs [textblock::frame -title overflow_right $rhs] - #} - #puts [textblock::join $lhs $rhs] - - #rendered - append rendered $overflow_right - # - - - set overflow_right "" - set row $renderedrow - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat $insert_lines_below ""] - } - incr row $insert_lines_below - set col 1 - - - - } - } - } else { - set row $post_render_row - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } else { - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $colwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $colwidth - } - } - } - } - wrapmoveforward { - #doesn't seem to be used by fruit.ans testfile - #used by dzds.ans - #note that cursor_forward may move deep into the next line - or even span multiple lines !TODO - set c $colwidth - set r $post_render_row - if {$post_render_col > $colwidth} { - set i $c - while {$i <= $post_render_col} { - if {$c == $colwidth+1} { - incr r - if {$opt_appendlines} { - if {$r < [llength $outputlines]} { - lappend outputlines "" - } - } - set c 1 - } else { - incr c - } - incr i - } - set col $c - } else { - #why are we getting this instruction then? - puts stderr "wrapmoveforward - test" - set r [expr {$post_render_row +1}] - set c $post_render_col - } - set row $r - set col $c - } - wrapmovebackward { - set c $colwidth - set r $post_render_row - if {$post_render_col < 1} { - set c 1 - set i $c - while {$i >= $post_render_col} { - if {$c == 0} { - if {$r > 1} { - incr r -1 - set c $colwidth - } else { - #leave r at 1 set c 1 - #testfile besthpav.ans first line top left border alignment - set c 1 - break - } - } else { - incr c -1 - } - incr i -1 - } - set col $c - } else { - puts stderr "Wrapmovebackward - but postrendercol >= 1???" - } - set row $r - set col $c - } - overflow { - #normal single-width grapheme overflow - #puts "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]" - set row $post_render_row ;#renderline will not advance row when reporting overflow char - incr row - set col 1 ;#whether wrap or not - next data is at column 1 - if {!$autowrap_mode} { - set overflow_handled 1 - set unapplied "" - #handled by dropping it - } - } - overflow_splitchar { - #2nd half of grapheme would overflow - grapheme returned in unapplied. There may also be overflow_right from earlier inserts - #todo - consider various options .. re-render a single trailing space or placeholder on same output line, etc - incr row - if {$autowrap_mode} { - set col 1 - } else { - set overflow_handled 1 - #handled by dropping it - } - } - vt { - - #can vt add a line like a linefeed can? - set row $post_render_row - set col $post_render_col - } - default { - puts stderr "overtype::left unhandled renderline instruction '$instruction'" - } - - } - - if {$renderedrow <= [llength $outputlines]} { - lset outputlines [expr {$renderedrow-1}] $rendered - } else { - if {$opt_appendlines} { - lappend outputlines $rendered - } else { - #? - lset outputlines [expr {$renderedrow-1}] $rendered - } - } - - if {!$overflow_handled} { - append nextprefix $overflow_right - } - - append nextprefix $unapplied - - if 0 { - if {$nextprefix ne ""} { - set nextoveridx [expr {$overidx+1}] - if {$nextoveridx >= [llength $inputchunks]} { - lappend inputchunks $nextprefix - } else { - #lset overlines $nextoveridx $nextprefix[lindex $overlines $nextoveridx] - set inputchunks [linsert $inputchunks $nextoveridx $nextprefix] - } - } - } - - if {$nextprefix ne ""} { - set inputchunks [linsert $inputchunks 0 $nextprefix] - } - - - incr overidx - incr loop - if {$loop >= $looplimit} { - puts stderr "overtype::left looplimit reached" - lappend outputlines "[a+ red bold] - looplimit $looplimit reached[a]" - break - } - } - - set result [join $outputlines \n] - if {$info_mode} { - append result \n$instruction_stats\n - } - return $result -} - -namespace eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} -#todo - left-right ellipsis ? -proc overtype::centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set defaults [dict create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- - - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$colwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[string tolower [dict get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow [dict get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [dict get $rinfo result] - set overflow_right [dict get $rinfo overflow_right] - set unapplied [dict get $rinfo unapplied] - #todo - get replay_codes from overflow_right instead of wherever it was truncated? - - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![dict get $opts -overflow]} { - #lappend outputlines [string range $overtext 0 [expr {$colwidth - 1}]] - #set overtext [string range $overtext 0 $colwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #don't use string range on ANSI data - #set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] -} - -proc overtype::right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set defaults [dict create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -align "left"\ - ] - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {} - default { - set known_opts [dict keys $defaults] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [dict get $opts -transparent] - set opt_ellipsis [dict get $opts -ellipsis] - set opt_ellipsistext [dict get $opts -ellipsistext] - set opt_ellipsiswhitespace [dict get $opts -ellipsiswhitespace] - set opt_overflow [dict get $opts -overflow] - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - set opt_align [dict get $opts -align] - # -- --- --- --- --- --- - - set norm [list \r\n \n] - set underblock [string map $norm $underblock] - set overblock [string map $norm $overblock] - - set underlines [split $underblock \n] - #set colwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w colwidth _h colheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$colwidth - $overblock_width)}] - set left_exposed $under_exposed_max - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $colwidth} { - set udiff [expr {$colwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_align { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext [string cat $replay_codes_underlay $undertext] - set overtext [string cat $replay_codes_overlay $overtext] - - set overflowlength [expr {$overtext_datalen - $colwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -overflow $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [dict get $rinfo replay_codes] - set rendered [dict get $rinfo result] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [string range $overtext end-[expr {$overflowlength-1}] end] - if {[string trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set ellipsis [string cat $replay_codes $opt_ellipsistext] - #todo - overflow on left if allign = right?? - set rendered [overtype::right $rendered $ellipsis] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need overflow here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -overflow $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [dict get $rinfo result] - } - set replay_codes [dict get $rinfo replay_codes] - set replay_codes_underlay [dict get $rinfo replay_codes_underlay] - set replay_codes_overlay [dict get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] -} - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [dict create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [dict merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -namespace eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#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 overtype::grapheme_width_cached {ch} { - variable grapheme_widths - if {[dict exists $grapheme_widths $ch]} { - return [dict get $grapheme_widths $ch] - } - set width [punk::char::ansifreestring_width $ch] - dict set grapheme_widths $ch $width - return $width -} - - - -# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### -# renderline written from a left-right line orientation perspective as a first-shot at getting something useful. -# ultimately right-to-left, top-to-bottom and bottom-to-top are probably needed. -# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### -# -# -#-returnextra enables returning of overflow and length -#review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation? -#review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements -#(could render it by faking it with sixels and a lot of work - find/make a sixel font and ensure it's exactly 2 cols per char) -#todo - review transparency issues with single/double width characters -#bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? -proc overtype::renderline {args} { - #*** !doctools - #[call [fun overtype::renderline] [arg args] ] - #[para] renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode commandline repl for the Tcl Punk Shell - #[para] It is also a central part of an ansi (micro) virtual terminal-emulator of sorts - #[para] This system does a half decent job at rendering 90's ANSI art to manipulable colour text blocks that can be joined & framed for layout display within a unix or windows terminal - #[para] Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't affect another. - #[para] Calling on the punk::ansi library - it can coalesce codes to keep the size down. - #[para] It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - #[para] renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a static underlay - #[para] The underlay is generally expected to be an ordered set of lines or a rectangular text block analogous to a terminal screen - but it can also be ragged in line length, or just blank. - #[para] The overlay couuld be similar - in which case it may often be used to overwrite a column or section of the underlay. - #[para] The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - # - #[para] renderline itself only deals with a single line - or sometimes a single character. It is generally called from a loop that does further terminal-like or textblock processing. - #[para] By suppyling the -info 1 option - it can return various fields indicating the state of the render. - #[para] The main 3 are the result, overflow_right, and unapplied. - #[para] Renderline handles cursor movements from either keystrokes or ANSI sequences but for a full system the aforementioned loop will need to be in place to manage the set of lines under manipulation. - - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} - } - lassign [lrange $args end-1 end] under over - if {[string first \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines in undertext" - } - #if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - # error "overtype::renderline not allowed to contain newlines" - #} - - set defaults [dict create\ - -etabs 0\ - -width \uFFEF\ - -overflow 0\ - -transparent 0\ - -startcolumn 1\ - -cursor_column 1\ - -cursor_row ""\ - -insert_mode 1\ - -autowrap_mode 1\ - -reverse_mode 0\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -cursor_restore_attributes ""\ - -experimental {}\ - ] - #-cursor_restore_attributes only - for replay stack - position and actual setting/restoring handled by throwback to caller - - #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return - #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs - - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency or overflow - #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error - - set argsflags [lrange $args 0 end-2] - dict for {k v} $argsflags { - switch -- $k { - -experimental - -width - -overflow - -transparent - -startcolumn - -cursor_column - -cursor_row - -insert_mode - -autowrap_mode - -reverse_mode - -info - -exposed1 - -exposed2 - -cursor_restore_attributes {} - default { - set known_opts [dict keys $defaults] - error "overtype::renderline unknown option '$k'. Known options: $known_opts" - } - } - } - set opts [dict merge $defaults $argsflags] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_width [dict get $opts -width] - set opt_etabs [dict get $opts -etabs] - set opt_overflow [dict get $opts -overflow] - set opt_colstart [dict get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [dict get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [dict get $opts -cursor_row] - if {[string length $opt_row_context]} { - if {![string is integer -strict $opt_row_context] || $opt_row_context <1 } { - error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode) - set opt_insert_mode [dict get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) - #default is for overtype - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_autowrap_mode [dict get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line - set opt_reverse_mode [dict get $opts -reverse_mode] ;#DECSNM - # -- --- --- --- --- --- --- --- --- --- --- --- - set temp_cursor_saved [dict get $opts -cursor_restore_attributes] - - set test_mode 0 - set cp437_glyphs 0 - foreach e [dict get $opts -experimental] { - switch -- $e { - test_mode { - set test_mode 1 - set cp437_glyphs 1 - } - } - } - set cp437_map [dict create] - if {$cp437_glyphs} { - set cp437_map [set ::punk::ansi::cp437_map] - #for cp437 images we need to map these *after* splitting ansi - #some old files might use newline for its glyph.. but we can't easily support that. - #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs? - dict unset cp437_map \n - } - - set opt_transparent [dict get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [dict get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [dict get $opts -exposed1] - set opt_exposed2 [dict get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - if {$opt_row_context eq ""} { - set cursor_row 1 - } else { - set cursor_row $opt_row_context - } - - - #----- - # - if {[info exists punk::console::tabwidth]} { - #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted - #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - - set overdata $over - if {!$cp437_glyphs} { - #REVIEW! tabify will give different answers for an ANSI colourised string vs plain text - if {!$opt_etabs} { - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under $tw] - } - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over $tw] - } - } - } - #------- - - #ta_detect ansi and do simpler processing? - - #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column, - #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway. - - # -- --- --- --- --- --- --- --- - if {$under ne ""} { - set undermap [punk::ansi::ta::split_codes_single $under] - } else { - set undermap [list] - } - set understacks [list] - set understacks_gx [list] - - set i_u -1 ;#underlay may legitimately be empty - set undercols [list] - set u_codestack [list] - #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway - set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics) - #set pt_underchars "" ;#for string_columns length calculation for overflow 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - #append pt_underchars $pt - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. - #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - set width 1 - } - default { - if {$grapheme eq "\u0000"} { - #use null as empty cell representation - review - #use of this will probably collide with some application at some point - #consider an option to set the empty cell character - set width 1 - } else { - set width [grapheme_width_cached $grapheme] - #we still want most controls and other zero-length codepoints such as \u200d (zero width joiner) to stay zero-length - #we substitute lone ESC that weren't captured within ANSI context as a debugging aid to see malformed ANSI - #todo - default to off and add a flag (?) to enable this substitution - set sub_stray_escapes 0 - if {$sub_stray_escapes && $width == 0} { - if {$grapheme eq "\x1b"} { - set gvis [ansistring VIEW $grapheme] - set grapheme $gvis - set width 1 - } - } - } - } - } - #set width [grapheme_width_cached $grapheme] - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - - lappend undercols $grapheme - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - #todo - if -etabs enabled - then we treat \t as the width determined by our elastic tabstop - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - lappend undercols "" - } - } - - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - if {$code ne ""} { - set c1c2 [string range $code 0 1] - set leadernorm [string range [string map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - ] $c1c2] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - switch -- $leadernorm { - 7CSI - 8CSI { - #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse - #REVIEW - what else could end in m but be mistaken as a normal SGR code here? - set maybemouse "" - if {[string index $c1c2 0] eq "\x1b"} { - set maybemouse [string index $code 2] - } - - if {$maybemouse ne "<" && [string index $code end] eq "m"} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - #basic simplification first.. straight dups - set dup_posns [lsearch -all -exact $u_codestack $code] ;#-exact because of square-bracket glob chars - set u_codestack [lremove $u_codestack {*}$dup_posns] - lappend u_codestack $code - } - } - } - 7GFX { - switch -- [string index $code 2] { - "0" { - set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess - } - B { - set u_gx_stack [list] - } - } - } - default { - - } - - } - - #if {[punk::ansi::codetype::is_sgr_reset $code]} { - # #set u_codestack [list] - #} elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #} elseif {[punk::ansi::codetype::is_sgr $code]} { - #} else { - # #leave SGR stack as is - # if {[punk::ansi::codetype::is_gx_open $code]} { - # } elseif {[punk::ansi::codetype::is_gx_close $code]} { - # } - #} - } - #consider also if there are other codes that should be stacked..? - } - - if {!$test_mode} { - #fill columns to width with spaces, and carry over stacks - we will have to keep track of where the underlying data ends manually - TODO - #Specifying a width is suitable for terminal-like applications and text-blocks - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff " "] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - } else { - #NULL empty cell indicator - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - lappend undercols {*}[lrepeat $diff "\u0000"] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - - } - if {$opt_width ne "\uFFEF"} { - set colwidth $opt_width - } else { - set colwidth [llength $undercols] - } - - - if 0 { - # ----------------- - # if we aren't extending understacks & understacks_gx each time we incr idx above the undercols length.. this doesn't really serve a purpose - # Review. - # ----------------- - #replay code for last overlay position in input line - # whether or not we get that far - we need to return it for possible replay on next line - if {[llength $understacks]} { - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - } else { - #in case overlay onto emptystring as underlay - lappend understacks [list] - lappend understacks_gx [list] - } - # ----------------- - } - - #trailing codes in effect for underlay - if {[llength $u_codestack]} { - #set replay_codes_underlay [join $u_codestack ""] - set replay_codes_underlay [punk::ansi::codetype::sgr_merge_list {*}$u_codestack] - } else { - set replay_codes_underlay "" - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char ?) padding on the left of the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpad_overlay [string repeat " " [expr {$opt_colstart -1}]] - append startpad_overlay $overdata ;#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - set overmap [punk::ansi::ta::split_codes_single $startpad_overlay] - #### - - #??? - set colcursor $opt_colstart - #TODO - make a little virtual column object - #we need to refer to column1 or columnmin? or columnmax without calculating offsets due to to startcolumn - #need to lock-down what start column means from perspective of ANSI codes moving around - the offset perspective is unclear and a mess. - - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - set overstacks [list] - set overstacks_gx [list] - - set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc) - set o_gxstack [list] - set pt_overchars "" - set i_o 0 - set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use - #experiment - set overlay_grapheme_control_stacks [list] - foreach {pt code} $overmap { - if {$cp437_glyphs} { - set pt [string map $cp437_map $pt] - } - append pt_overchars $pt - #will get empty pt between adjacent codes - foreach grapheme [punk::char::grapheme_split $pt] { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - lappend overlay_grapheme_control_stacks $o_codestack - #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split) - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] ;#reset better than empty list - fixes some ansi art issues - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $o_codestack $code] ;#must be -exact because of square-bracket glob chars - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[regexp {\x1b7|\x1b\[s} $code]} { - #experiment - #cursor_save - for the replays review. - #jmn - #set temp_cursor_saved [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - lappend overlay_grapheme_control_list [list other $code] - } elseif {[regexp {\x1b8|\x1b\[u} $code]} { - #experiment - #cursor_restore - for the replays - set o_codestack [list $temp_cursor_saved] - lappend overlay_grapheme_control_list [list other $code] - } else { - if {[punk::ansi::codetype::is_gx_open $code]} { - set o_gxstack [list "gx0_on"] - lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set o_gxstack [list] - lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } else { - lappend overlay_grapheme_control_list [list other $code] - } - } - } - - } - #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme - set max_overlay_grapheme_index [expr {$i_o -1}] - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - - #set replay_codes_overlay [join $o_codestack ""] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - - #if {[dict exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [dict get $overstacks $max_overlay_grapheme_index] ""] - #} else { - # set replay_codes_overlay "" - #} - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - # -- --- --- - #we need to initialise overflow_idx before any potential row-movements - as they need to perform a loop break and force in_excess to 1 - if {$opt_overflow} { - #somewhat counterintuitively - overflow true means we can have lines as long as we want, but either way there can be excess data that needs to be thrown back to the calling loop. - set overflow_idx -1 - } else { - #overflow zero - we can't grow beyond our column width - so we get ellipsis or truncation - if {$opt_width ne "\uFFEF"} { - set overflow_idx [expr {$opt_width}] - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - } - # -- --- --- - - set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to. - - set unapplied "" ;#if we break for move row (but not for /v ?) - set insert_lines_above 0 ;#return key - set insert_lines_below 0 - set instruction "" - - # -- --- --- - #cursor_save_dec, cursor_restore_dec etc - set cursor_restore_required 0 - set cursor_saved_attributes "" - set cursor_saved_position "" - # -- --- --- - - #set idx 0 ;# line index (cursor - 1) - #set idx [expr {$opt_colstart + $opt_colcursor} -1] - - #idx is the per column output index - set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1 - #cursor_column is usually one above idx - but we have opt_colstart which is like a margin - todo: remove cursor_column from the following loop and calculate it's offset when breaking or at end. - #(for now we are incrementing/decrementing both in sync - which is a bit silly) - set cursor_column $opt_colcursor - - #idx_over is the per grapheme overlay index - set idx_over -1 - - - #movements only occur within the overlay range. - #an underlay is however not necessary.. e.g - #renderline -overflow 1 "" data - #foreach {pt code} $overmap {} - set insert_mode $opt_insert_mode ;#default 1 - set autowrap_mode $opt_autowrap_mode ;#default 1 - - - #puts "-->$overlay_grapheme_control_list<--" - #puts "-->overflow_idx: $overflow_idx" - for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} { - set gc [lindex $overlay_grapheme_control_list $gci] - lassign $gc type item - - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - #foreach ch $overlay_graphemes {} - switch -- $type { - g { - set ch $item - incr idx_over; #idx_over (until unapplied reached anyway) is per *grapheme* in the overlay - not per col. - if {($idx < ($opt_colstart -1))} { - incr idx [grapheme_width_cached $ch] - continue - } - #set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our active data width - set within_undercols [expr {$idx <= $colwidth-1}] - - #https://www.enigma.com/resources/blog/the-secret-world-of-newline-characters - #\x85 NEL in the c1 control set is treated by some terminal emulators (e.g Hyper) as a newline, - #on some it's invisble but doesn't change the line, on some it's a visible glyph of width 1. - #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable - #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE - - set chtest [string map [list \n \x85 \b \r \v \x7f ] $ch] - #puts --->chtest:$chtest - #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached - switch -- $chtest { - "" { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - if {$idx == 0} { - #puts "---a at col 1" - #linefeed at column 1 - #leave the overflow_idx ;#? review - set instruction lf_start ;#specific instruction for newline at column 1 - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } elseif {$overflow_idx != -1 && $idx == $overflow_idx} { - #linefeed after final column - #puts "---c at overflow_idx=$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } else { - #linefeed occurred in middle or at end of text - #puts "---mid-or-end-text-linefeed idx:$idx overflow_idx:$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to opt_overflow = 1|2 - set instruction lf_mid - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } - - } - "" { - #will we want/need to use raw for keypresses in terminal? (terminal with LNM in standard reset mode means enter= this is the usual config for terminals) - #So far we are assuming the caller has translated to and handle above.. REVIEW. - - #consider also the old space-carriagereturn softwrap convention used in some terminals. - #In the context of rendering to a block of text - this works similarly in that the space gets eaten so programs emitting space-cr at the terminal width col will pretty much get what they expect. - set idx [expr {$opt_colstart -1}] - set cursor_column $opt_colstart ;#? - } - "" { - #literal backspace char - not necessarily from keyboard - #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype - #(important for -transparent option - hence replacement chars for half-exposed etc) - #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR) - if {$idx > ($opt_colstart -1)} { - incr idx -1 - incr cursor_column -1 - } else { - set flag 0 - if $flag { - #review - conflicting requirements? Need a different sequence for destructive interactive backspace? - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction backspace_at_start - break - } - } - } - "" { - #literal del character - some terminals send just this for what is generally expected to be a destructive backspace - #We instead treat this as a pure delete at current cursor position - it is up to the repl or terminal to remap backspace key to a sequence that has the desired effect. - priv::render_delchar $idx - } - "" { - #end processing this overline. rest of line is remainder. cursor for column as is. - #REVIEW - this theoretically depends on terminal's vertical tabulation setting (name?) - #e.g it could be configured to jump down 6 rows. - #On the other hand I've seen indications that some modern terminal emulators treat it pretty much as a linefeed. - #todo? - incr cursor_row - set overflow_idx $idx - #idx_over has already been incremented as this is both a movement-control and in some sense a grapheme - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction vt - break - } - default { - if {$overflow_idx != -1} { - #review - how to check arbitrary length item such as tab is going to overflow .. before we get to overflow_idx? - #call grapheme_width_cached on each ch, or look for tab specifically as it's currently the only known reason to have a grapheme width > 2? - #we need to decide what a tab spanning the overflow_idx means and how it affects wrap etc etc - if {$idx == $overflow_idx-1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 2} { - #review split 2w overflow? - #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line - #better to consider the overlay char as unable to be applied to the line - #render empty string to column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied - #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #change the overflow_idx - set overflow_idx $idx - incr idx - incr idx_over -1 ;#set overlay grapheme index back one so that sgr stack from previous overlay grapheme used - priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#note $gci-1 instead of just gci - #throw back to caller's loop - add instruction to caller as this is not the usual case - #caller may for example choose to render a single replacement char to this line and omit the grapheme, or wrap it to the next line - set instruction overflow_splitchar - break - } elseif {$owidth > 2} { - #? tab? - #TODO! - puts stderr "overtype::renderline long overtext grapheme '[ansistring VIEW -lf 1 -vt 1 $ch]' not handled" - #tab of some length dependent on tabstops/elastic tabstop settings? - } - } elseif {$idx == $overflow_idx} { - #jmn? - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci-1]] - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #don't incr idx beyond the overflow_idx - #idx_over already incremented - decrement so current overlay grapheme stacks go to unapplied - incr idx_over -1 - #priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#back one index here too - priv::render_this_unapplied $overlay_grapheme_control_list $gci ;# - set instruction overflow - break - } - } else { - #review. - #This corresponds to opt_overflow being true (at least until overflow_idx is in some cases forced to a value when throwing back to calling loop) - } - - if {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - #dict set understacks $idx [list] ;#review - use idx-1 codestack? - lset understacks $idx [list] - incr idx - incr cursor_column - } else { - #todo - punk::char::char_width - set g [lindex $outcols $idx] - set uwidth [grapheme_width_cached $g] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - incr cursor_column - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - incr cursor_column - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - incr idx - incr cursor_column - if {$owidth > 1} { - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - if {[grapheme_width_cached $ch] == 1} { - if {!$insert_mode} { - #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - incr cursor_column - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - incr cursor_column - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [dict get $overstacks $idx_over] [dict get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } else { - #? todo - decide what transparency even means for insert mode - incr idx - incr cursor_column - } - } else { - #2wide transparency over 2wide in underlay - review - incr idx - incr cursor_column - } - } - } - } else { - - set idxchar [lindex $outcols $idx] - #non-transparent char in overlay or empty cell - if {$idxchar eq "\u0000"} { - #empty/erased cell indicator - set uwidth 1 - } else { - set uwidth [grapheme_width_cached $idxchar] - } - if {$within_undercols} { - if {$idxchar eq ""} { - #2nd col of 2wide char in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??) - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 - } - incr idx - } else { - set prevcolinfo [lindex $outcols $idx-1] - #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right - #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?) - #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char - #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises - #It is perhaps best avoided at another level and try to make renderline do exactly as it's told - #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect. - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index - if {$prevcolinfo ne ""} { - #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 ;#replace not insert - } ;# else?? - incr idx - } - if {$cursor_column < [llength $outcols] || $overflow_idx == -1} { - incr cursor_column - } - } elseif {$uwidth == 0} { - #what if this is some other c0/c1 control we haven't handled specifically? - - #by emitting a preceding empty-string column - we associate whatever this char is with the preceeding non-zero-length character and any existing zero-lengths that follow it - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - #if we can get a proper grapheme_split function - this should be easier to tidy up. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column 2 - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } elseif {$uwidth == 1} { - #includes null empty cells - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {([llength $outcols] >= $idx +2) && [lindex $outcols $idx+1] eq ""} { - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks $idx+1] [lindex $understacks_gx $idx+1] $insert_mode - } - incr idx - } - if {($cursor_column < [llength $outcols]) || $overflow_idx == -1 || $test_mode} { - incr cursor_column - } - } elseif {$uwidth > 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - #1wide over 2wide in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #insert mode just pushes all to right - no exposition char here - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx 2 - incr cursor_column 2 - } - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - if {$overflow_idx !=-1 && !$test_mode} { - #overflow - if {$cursor_column > [llength $outcols]} { - set cursor_column [llength $outcols] - } - } - } - } - } - } ;# end switch - - - } - other { - set code $item - #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' - - set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM - set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - set re_row_move {\x1b\[([0-9]*)(A|B)$} - set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? - set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! - set re_cursor_restore {\x1b\[u$} - set re_cursor_save_dec {\x1b7$} - set re_cursor_restore_dec {\x1b8$} - set re_other_single {\x1b(D|M|E)$} - set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins - set matchinfo [list] - - #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI - #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping - #review - cost/benefit of function calls within these switch-arms instead of inline code? - - #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. - #we should probably therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore - set code [string map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $code] - - - set c1 [string index $code 0] - set c1c2c3 [string range $code 0 2] - #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} - set leadernorm [string range [string map [list\ - \x1b\[< 1006\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\] 7OSC\ - \x9d 8OSC\ - \x1b 7ESC\ - ] $c1c2c3] 0 3] ;#leadernorm is 1st 2 chars mapped to 4char normalised indicator - or is original 2 chars - - #we leave the tail of the code unmapped for now - switch -- $leadernorm { - 1006 { - #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html - #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release - set codenorm [string cat $leadernorm [string range $code 3 end]] - } - 7CSI - 7OSC { - set codenorm [string cat $leadernorm [string range $code 2 end]] - } - 7ESC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - 8CSI - 8OSC { - set codenorm [string cat $leadernorm [string range $code 1 end]] - } - default { - #we haven't made a mapping for this - set codenorm $code - } - } - - #we've mapped 7 and 8bit escapes to values we can handle as literals in switch statements to take advantange of jump tables. - switch -- $leadernorm { - 1006 { - #TODO - # - switch -- [string index $codenorm end] { - M { - puts stderr "mousedown $codenorm" - } - m { - puts stderr "mouseup $codenorm" - } - } - - } - {7CSI} - {8CSI} { - set param [string range $codenorm 4 end-1] - #puts stdout "--> CSI [string index $leadernorm 0] bit param:$param" - switch -- [string index $codenorm end] { - D { - #Col move - #puts stdout "<-back" - #cursor back - #left-arrow/move-back when ltr mode - set num $param - if {$num eq ""} {set num 1} - - set version 2 - if {$version eq "2"} { - #todo - startcolumn offset! - if {$cursor_column - $num >= 1} { - incr idx -$num - incr cursor_column -$num - } else { - if {!$autowrap_mode} { - set cursor_column 1 - set idx 0 - } else { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr cursor_column -$num - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmovebackward - break - } - } - } else { - incr idx -$num - incr cursor_column -$num - if {$idx < $opt_colstart-1} { - #wrap to previous line and position cursor at end of data - set idx [expr {$opt_colstart-1}] - set cursor_column $opt_colstart - } - } - } - C { - #Col move - #puts stdout "->forward" - #todo - consider right-to-left cursor mode (e.g Hebrew).. some day. - #cursor forward - #right-arrow/move forward - set num $param - if {$num eq ""} {set num 1} - - #todo - retrict to moving 1 position past datalen? restrict to column width? - #should ideally wrap to next line when interactive and not on last row - #(some ansi art seems to expect this behaviour) - #This presumably depends on the terminal's wrap mode - #e.g DECAWM autowrap mode - # CSI ? 7 h - set: autowrap (also tput smam) - # CSI ? 7 l - reset: no autowrap (also tput rmam) - set version 2 - if {$version eq "2"} { - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$test_mode && $cursor_column == $max+1} { - #move_forward while in overflow - incr cursor_column -1 - } - - if {($cursor_column + $num) <= $max} { - incr idx $num - incr cursor_column $num - } else { - if {$autowrap_mode} { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #jmn - if {$idx == $overflow_idx} { - incr num - } - - #horizontal movement beyond line extent needs to wrap - throw back to caller - #we may have both overflow_rightand unapplied data - #(can have overflow_right if we were in insert_mode and processed chars prior to this movement) - #leave row as is - caller will need to determine how many rows the column-movement has consumed - incr cursor_column $num ;#give our caller the necessary info as columns from start of row - #incr idx_over - #should be gci following last one applied - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmoveforward - break - } else { - set cursor_column $max - set idx [expr {$cursor_column -1}] - } - } - } else { - if {!$opt_overflow || ($cursor_column + $num) <= [llength $outcols+1]} { - incr idx $num - incr cursor_column $num - } else { - if {!$insert_mode} { - #block editing style with arrow keys - #overtype mode - set idxstart $idx - set idxend [llength $outcols] - set moveend [expr {$idxend - $idxstart}] - if {$moveend < 0} {set moveend 0} ;#sanity? - #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" - incr idx $moveend - incr cursor_column $moveend - #if {[dict exists $understacks $idx]} { - # set stackinfo [dict get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #} else { - # set stackinfo [list] - #} - if {$idx < [llength $understacks]} { - set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - } else { - set stackinfo [list] - } - if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [dict get $understacks_gx $idx] - set gxstackinfo [lindex $understacks_gx $idx] - } else { - set gxstackinfo [list] - } - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - incr cursor_column - priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode - } - } else { - #normal - insert - incr idx $num - incr cursor_column $num - if {$idx > [llength $outcols]} { - set idx [llength $outcols];#allow one beyond - for adding character at end of line - set cursor_column [expr {[llength $outcols]+1}] - } - } - } - } - } - G { - #Col move - #move absolute column - #adjust to colstart - as column 1 is within overlay - #??? - set idx [expr {$param + $opt_colstart -1}] - set cursor_column $param - error "renderline absolute col move ESC G unimplemented" - } - A { - #Row move - up - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set num $param - if {$num eq ""} {set num 1} - incr cursor_row -$num - - if {$cursor_row < 1} { - set cursor_row 1 - } - - #ensure rest of *overlay* is emitted to remainder - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up - #retain cursor_column - break - } - B { - #Row move - down - set num $param - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move down - if {$num eq ""} {set num 1} - incr cursor_row $num - - - incr idx_over ;#idx_over hasn't encountered a grapheme and hasn't advanced yet - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - H - f { - #$re_both_move - lassign [split $param {;}] row col - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #lassign $matchinfo _match row col - - if {$col eq ""} {set col 1} - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$col > $max} { - set cursor_column $max - } else { - set cursor_column $col - } - set idx [expr {$cursor_column -1}] - - if {$row eq ""} {set row 1} - set cursor_row $row - if {$cursor_row < 1} { - set cursor_row 1 - } - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - - } - X { - puts stderr "X - $param" - #ECH - erase character - if {$param eq "" || $param eq "0"} {set param 1}; #param=count of chars to erase - priv::render_erasechar $idx $param - #cursor position doesn't change. - } - r { - #$re_decstbm - #https://www.vt100.net/docs/vt510-rm/DECSTBM.html - #This control function sets the top and bottom margins for the current page. You cannot perform scrolling outside the margins - lassign [split $param {;}] margin_top margin_bottom - - #todo - return these for the caller to process.. - puts stderr "overtype::renderline DECSTBM set top and bottom margin not implemented" - #Also moves the cursor to col 1 line 1 of the page - set cursor_column 1 - set cursor_row 1 - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move ;#own instruction? decstbm? - break - } - s { - # - todo - make ansi.sys CSI s cursor save only apply for certain cases? - may need to support DECSLRM instead which uses same code - - #$re_cursor_save - #cursor save could come after last column - if {$overflow_idx != -1 && $idx == $overflow_idx} { - #bartman2.ans test file - fixes misalignment at bottom of dialog bubble - #incr cursor_row - #set cursor_column 1 - #bwings1.ans test file - breaks if we actually incr cursor (has repeated saves) - set cursor_saved_position [list row [expr {$cursor_row+1}] column 1] - } else { - set cursor_saved_position [list row $cursor_row column $cursor_column] - } - #there may be overlay stackable codes emitted that aren't in the understacks because they come between the last emmited character and the cursor_save control. - #we need the SGR and gx overlay codes prior to the cursor_save - - #a real terminal would not be able to know the state of the underlay.. so we should probably ignore it. - #set sgr_stack [lindex $understacks $idx] - #set gx_stack [lindex $understacks_gx $idx] ;#not actually a stack - just a boolean state (for now?) - - set sgr_stack [list] - set gx_stack [list] - - #we shouldn't need to scan for intermediate cursor save/restores - as restores would throw-back to the calling loop - so our overlay 'line' is since those. - #The overlay_grapheme_control_list had leading resets from previous lines - so we go back to the beginning not just the first grapheme. - - foreach gc [lrange $overlay_grapheme_control_list 0 $gci-1] { - lassign $gc type code - #types g other sgr gx0 - switch -- $type { - gx0 { - #code is actually a stand-in for the graphics on/off code - not the raw code - #It is either gx0_on or gx0_off - set gx_stack [list $code] - } - sgr { - #code is the raw code - if {[punk::ansi::codetype::is_sgr_reset $code]} { - #jmn - set sgr_stack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set sgr_stack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #often we don't get resets - and codes just pile up. - #as a first step to simplifying - at least remove earlier straight up dupes - set dup_posns [lsearch -all -exact $sgr_stack $code] ;#needs -exact - codes have square-brackets (glob chars) - set sgr_stack [lremove $sgr_stack {*}$dup_posns] - lappend sgr_stack $code - } - } - } - } - set cursor_saved_attributes "" - switch -- [lindex $gx_stack 0] { - gx0_on { - append cursor_saved_attributes "\x1b(0" - } - gx0_off { - append cursor_saved_attributes "\x1b(B" - } - } - #append cursor_saved_attributes [join $sgr_stack ""] - append cursor_saved_attributes [punk::ansi::codetype::sgr_merge_list {*}$sgr_stack] - - #as there is apparently only one cursor storage element we don't need to throw back to the calling loop for a save. - - #don't incr index - or the save will cause cursor to move to the right - #carry on - - } - u { - #$re_cursor_restore - #we are going to jump somewhere.. for now we will assume another line, and process accordingly. - #The caller has the cursor_saved_position/cursor_saved_attributes if any (?review - if we always pass it back it, we could save some calls for moves in same line) - #don't set overflow at this point. The existing underlay to the right must be preserved. - #we only want to jump and render the unapplied at the new location. - - #lset overstacks $idx_over [list] - #set replay_codes_overlay "" - - #if {$cursor_saved_attributes ne ""} { - # set replay_codes_overlay $cursor_saved_attributes ;#empty - or last save if it happend in this input chunk - #} else { - #jj - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set replay_codes_overlay "" - #} - - #like priv::render_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code - incr idx_over - - set unapplied "" - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - append unapplied "\x1b(0" - } elseif {$item eq "gx0_off"} { - append unapplied "\x1b(B" - } - } else { - append unapplied $item - } - #incr idx_over - } - #if the save occured within this line - that's ok - it's in the return value list and caller can prepend for the next loop. - set instruction restore_cursor - break - } - ~ { - #$re_vt_sequence - #lassign $matchinfo _match key mod - lassign [split $param {;}] key mod - - #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~ - # - #e.g esc \[2~ insert esc \[2;2~ shift-insert - #mod - subtract 1, and then use bitmask - #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?) - #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]" - if {$key eq "1"} { - #home - } elseif {$key eq "2"} { - #Insert - if {$mod eq ""} { - #no modifier key - set insert_mode [expr {!$insert_mode}] - #rather than set the cursor - we return the insert mode state so the caller can decide - } - } elseif {$key eq "3"} { - #Delete - presumably this shifts other chars in the line, with empty cells coming in from the end - switch -- $mod { - "" { - priv::render_delchar $idx - } - "5" { - #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?) - } - } - } elseif {$key eq "4"} { - #End - } elseif {$key eq "5"} { - #pgup - } elseif {$key eq "6"} { - #pgDn - } elseif {$key eq "7"} { - #Home - #?? - set idx [expr {$opt_colstart -1}] - set cursor_column 1 - } elseif {$key eq "8"} { - #End - } elseif {$key eq "11"} { - #F1 - or ESCOP or e.g shift F1 ESC\[1;2P - } elseif {$key eq "12"} { - #F2 - or ESCOQ - } elseif {$key eq "13"} { - #F3 - or ESCOR - } elseif {$key eq "14"} { - #F4 - or ESCOS - } elseif {$key eq "15"} { - #F5 or shift F5 ESC\[15;2~ - } elseif {$key eq "17"} { - #F6 - } elseif {$key eq "18"} { - #F7 - } elseif {$key eq "19"} { - #F8 - } elseif {$key eq "20"} { - #F9 - } elseif {$key eq "21"} { - #F10 - } elseif {$key eq "23"} { - #F11 - } elseif {$key eq "24"} { - #F12 - } - - } - h - l { - #we are matching only last char to get to this arm - but are there other sequences ending in h|l we need to handle? - - #$re_mode if first after CSI is "?" - #some docs mention ESC=h|l - not seen on windows terminals.. review - #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html - if {[string index $codenorm 4] eq "?"} { - set num [string range $codenorm 5 end-1] ;#param between ? and h|l - #lassign $matchinfo _match num type - switch -- $num { - 5 { - #DECSNM - reverse video - #How we simulate this to render within a block of text is an open question. - #track all SGR stacks and constantly flip based on the current SGR reverse state? - #It is the job of the calling loop to do this - so at this stage we'll just set the states - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set reverse_mode 1 - } else { - #reset (disable) - set reverse_mode 0 - } - - } - 7 { - #DECAWM autowrap - if {$type eq "h"} { - #set (enable) - set autowrap_mode 1 - if {$opt_width ne "\uFFEF"} { - set overflow_idx $opt_width - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - #review - can idx ever be beyond overflow_idx limit when we change e.g with a width setting and cursor movements? presume not - but sanity check for now. - if {$idx >= $overflow_idx} { - puts stderr "renderline error - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected" - } - } else { - #reset (disable) - set autowrap_mode 0 - set overflow_idx -1 - } - } - 25 { - if {$type eq "h"} { - #visible cursor - - } else { - #invisible cursor - - } - } - } - - } else { - puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - default { - puts stderr "overtype::renderline CSI code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - 7ESC { - #$re_other_single - switch -- [string index $codenorm end] { - D { - #\x84 - #index (IND) - #vt102-docs: "Moves cursor down one line in same column. If cursor is at bottom margin, screen performs a scroll-up" - puts stderr "ESC D not fully implemented" - incr cursor_row - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - M { - #\x8D - #Reverse Index (RI) - #vt102-docs: "Moves cursor up one line in same column. If cursor is at top margin, screen performs a scroll-down" - puts stderr "ESC M not fully implemented" - - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move up - incr cursor_row -1 - if {$cursor_row < 1} { - set cursor_row 1 - } - #ensure rest of *overlay* is emitted to remainder - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up ;#need instruction for scroll-down? - #retain cursor_column - break - } - E { - #\x85 - #review - is behaviour different to lf? - #todo - possibly(?) same logic as handling above. i.e return instruction depends on where column_cursor is at the time we get NEL - #leave implementation until logic for is set in stone... still under review - #It's arguable NEL is a pure cursor movement as opposed to the semantic meaning of crlf or lf in a file. - # - #Next Line (NEL) "Move the cursor to the left margin on the next line. If the cursor is at the bottom margin, scroll the page up" - puts stderr "ESC E unimplemented" - - } - default { - puts stderr "overtype::renderline ESC code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - - } - } - - #switch -regexp -matchvar matchinfo -- $code\ - #$re_mode { - #}\ - #default { - # puts stderr "overtype::renderline code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - #} - - } - default { - #don't need to handle sgr or gx0 types - #we have our sgr gx0 codes already in stacks for each overlay grapheme - } - } - } - - #-------- - if {$opt_overflow == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - #set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - if {$overflow_idx == -1} { - #overflow was initially unlimited and hasn't been overridden - } else { - - } - #-------- - - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" ;#output prior to overflow - set overflow_right "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set prev_g0 [list] - #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves - set in_overflow 0 ;#used to stop char-width scanning once in overflow - if {$overflow_idx == 0} { - #how does caller avoid an infinite loop if they have autowrap on and keep throwing graphemes to the next line? REVIEW - set in_overflow 1 - } - foreach ch $outcols { - #puts "---- [ansistring VIEW $ch]" - - set gxleader "" - if {$i < [llength $understacks_gx]} { - #set g0 [dict get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {$g0 ne $prev_g0} { - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } else { - set gxleader "\x1b(B" - } - } - set prev_g0 $g0 - } else { - set prev_g0 [list] - } - - set sgrleader "" - if {$i < [llength $understacks]} { - #set cstack [dict get $understacks $i] - set cstack [lindex $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack] && ![llength $cstack]} { - #This reset is important e.g testfile fruit.ans - we get overhang on rhs without it. But why is cstack empty? - append sgrleader \033\[m - } else { - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - } - } - set prevstack $cstack - } else { - set prevstack [list] - } - - - - if {$in_overflow} { - if {$i == $overflow_idx} { - set 0 [lindex $understacks_gx $i] - set gxleader "" - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } elseif {$g0 eq [list "gx0_off"]} { - set gxleader "\x1b(B" - } - append overflow_right $gxleader - set cstack [lindex $understacks $i] - set sgrleader "" - #whether cstack is same or differs from previous char's stack - we must have an output at the start of the overflow_right - #if {[llength $prevstack] && ![llength $cstack]} { - # append sgrleader \033\[m - #} - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - append overflow_right $sgrleader - append overflow_right $ch - } else { - append overflow_right $gxleader - append overflow_right $sgrleader - append overflow_right $ch - } - } else { - if {$overflow_idx != -1 && $i+1 == $overflow_idx} { - #one before overflow - #will be in overflow in next iteration - set in_overflow 1 - if {[grapheme_width_cached $ch]> 1} { - #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide) - set ch $opt_exposed1 - } - } - append outstring $gxleader - append outstring $sgrleader - if {$idx+1 < $cursor_column} { - append outstring [string map [list "\u0000" " "] $ch] - } else { - append outstring $ch - } - } - incr i - } - #flower.ans good test for null handling - reverse line building - if {![ansistring length $overflow_right]} { - set outstring [string trimright $outstring "\u0000"] - } - set outstring [string map [list "\u0000" " "] $outstring] - set overflow_right [string trimright $overflow_right "\u0000"] - set overflow_right [string map [list "\u0000" " "] $overflow_right] - - set replay_codes "" - if {[llength $understacks] > 0} { - if {$overflow_idx == -1} { - #set tail_idx [dict size $understacks] - set tail_idx [llength $understacks] - } else { - set tail_idx [llength $undercols] - } - if {$tail_idx-1 < [llength $understacks]} { - #set replay_codes [join [lindex $understacks $tail_idx-1] ""] ;#tail replay codes - set replay_codes [punk::ansi::codetype::sgr_merge_list {*}[lindex $understacks $tail_idx-1]] ;#tail replay codes - } - if {$tail_idx-1 < [llength $understacks_gx]} { - set gx0 [lindex $understacks_gx $tail_idx-1] - if {$gx0 eq [list "gx0_on"]} { - #if it was on, turn gx0 off at the point we stop processing overlay - append outstring "\x1b(B" - } - } - } - if {[string length $overflow_right]} { - #puts stderr "remainder:$overflow_right" - } - #pdict $understacks - if {[punk::ansi::ta::detect_sgr $outstring]} { - append outstring [punk::ansi::a] - #close off any open gx? - #probably should - and overflow_right reopen? - } - - if {$opt_returnextra} { - #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review - #replay_codes_underlay is the set of codes in effect at the very end of the original underlay - - #review - #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied) - #todo - replay_codes for gx0 mode - - #overflow_idx may change during ansi & character processing - if {$overflow_idx == -1} { - set overflow_right_column "" - } else { - set overflow_right_column [expr {$overflow_idx+1}] - } - set result [dict create\ - result $outstring\ - visualwidth [punk::ansi::printing_length $outstring]\ - instruction $instruction\ - stringlen [string length $outstring]\ - overflow_right_column $overflow_right_column\ - overflow_right $overflow_right\ - unapplied $unapplied\ - insert_mode $insert_mode\ - autowrap_mode $autowrap_mode\ - insert_lines_above $insert_lines_above\ - insert_lines_below $insert_lines_below\ - cursor_saved_position $cursor_saved_position\ - cursor_saved_attributes $cursor_saved_attributes\ - cursor_column $cursor_column\ - cursor_row $cursor_row\ - opt_overflow $opt_overflow\ - replay_codes $replay_codes\ - replay_codes_underlay $replay_codes_underlay\ - replay_codes_overlay $replay_codes_overlay\ - ] - if {$opt_returnextra == 1} { - return $result - } else { - #human/debug - map special chars to visual glyphs - set viewop VIEW - switch -- $opt_returnextra { - 2 { - #codes and character data - set viewop VIEWCODES ;#ansi colorisation of codes - green for SGR, blue/blue reverse for cursor_save/cursor_restore, cyan for movements, orange for others - } - 3 { - set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ. - } - } - dict set result result [ansistring $viewop -lf 1 -vt 1 [dict get $result result]] - dict set result overflow_right [ansistring VIEW -lf 1 -vt 1 [dict get $result overflow_right]] - dict set result unapplied [ansistring VIEW -lf 1 -vt 1 [dict get $result unapplied]] - dict set result replay_codes [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes]] - dict set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_underlay]] - dict set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [dict get $result replay_codes_overlay]] - dict set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [dict get $result cursor_saved_attributes]] - return $result - } - } else { - return $outstring - } - #return [join $out ""] -} -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [dict create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - if {[string first \t $textblock] >= 0} { - if {[info exists punk::console::tabwidth]} { - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - set textblock [textutil::tabify::untabify2 $textblock $tw] - } - #stripansi on entire block in one go rather than line by line - result should be the same - review - make tests - if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::stripansi $textblock] - } - if {[string first \n $textblock] >= 0} { - set num_le [expr {[string length $textblock]-[string length [string map [list \n {}] $textblock]]}] ;#faster than splitting into single-char list - set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] - } else { - set num_le 0 - set width [punk::char::ansifreestring_width $textblock] - } - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [dict create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -namespace eval overtype::priv { - variable cache_is_sgr [dict create] - - #we are likely to be asking the same question of the same ansi codes repeatedly - #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS - #todo - test if still worthwhile after a large cache is built up. (limit cache size?) - proc is_sgr {code} { - variable cache_is_sgr - if {[dict exists $cache_is_sgr $code]} { - return [dict get $cache_is_sgr $code] - } - set answer [punk::ansi::codetype::is_sgr $code] - dict set cache_is_sgr $code $answer - return $answer - } - proc render_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - #append unapplied [join [lindex $overstacks $idx_over] ""] - #append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]] - append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - append unapplied "\x1b(0" - } - "gx0_off" { - append unapplied "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - append unapplied "\x1b(0" - } elseif {$item eq "gx0_off"} { - append unapplied "\x1b(B" - } - } else { - append unapplied $item - } - } - } - - #clearer - renders the specific gci forward as unapplied - prefixed with it's merged sgr stack - proc render_this_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - append unapplied "\x1b(0" - } - "gx0_off" { - append unapplied "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - append unapplied "\x1b(0" - } elseif {$item eq "gx0_off"} { - append unapplied "\x1b(B" - } - } else { - append unapplied $item - } - } - } - proc render_delchar {i} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - set nxt [llength $o] - if {$i < $nxt} { - set o [lreplace $o $i $i] - set ustacks [lreplace $ustacks $i $i] - set gxstacks [lreplace $gxstacks $i $i] - } else { - - } - } - proc render_erasechar {i count} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - #ECH clears character attributes from erased character positions - #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller should do that mapping and only supply 1 or greater. - if {![string is integer -strict $count] || $count < 1} { - error "render_erasechar count must be integer >= 1" - } - set start $i - set end [expr {$i + $count -1}] - #we restrict ECH to current line - as some terminals do - review - is that the only way it's implemented? - if {$i > [llength $o]-1} { - return - } - if {$end > [llength $o]-1} { - set end [expr {[llength $o]-1}] - } - set num [expr {$end - $start + 1}] - set o [lreplace $o $start $end {*}[lrepeat $num \u0000]] ;#or space? - set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list]]] - set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] - return - } - proc render_setchar {i c } { - upvar outcols o - lset o $i $c - } - #is actually addgrapheme? - proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - - if 0 { - if {$c eq "c"} { - puts "i:$i c:$c sgrstack:[ansistring VIEW $sgrstack]" - puts "understacks:[ansistring VIEW $ustacks]" - upvar overstacks overstacks - puts "overstacks:[ansistring VIEW $overstacks]" - puts "info level 0:[info level 0]" - } - } - - set nxt [llength $o] - if {!$insert_mode} { - if {$i < $nxt} { - #These lists must always be in sync - lset o $i $c - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - lset ustacks $i $sgrstack - lset gxstacks $i $gx0stack - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } else { - #insert of single-width vs double-width when underlying is double-width? - if {$i < $nxt} { - set o [linsert $o $i $c] - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - set ustacks [linsert $ustacks $i $sgrstack] - set gxstacks [linsert $gxstacks $i $gx0stack] - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -namespace eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [namespace eval overtype { - variable version - set version 1.6.0 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/modules/flagfilter-0.3.tm b/src/modules/flagfilter-0.3.tm index 6497be7f..c6064ea5 100644 --- a/src/modules/flagfilter-0.3.tm +++ b/src/modules/flagfilter-0.3.tm @@ -2078,10 +2078,11 @@ namespace eval flagfilter { #todo - add flaggednew to required if all was specified? #check invalid flags if not indicated in -extras , either explicitly or with 'extra' set flags_from_required [get_flagged_only $required {}] - set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] + #set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] ;#why -nocase? why should -l and -L collapse to the uppercase version? + set known_flags [punk::lib::lunique_unordered [concat [dict keys $defaults] $flags_from_required $soloflags ]] foreach spec $command_specs { lassign $spec parentname pinfo - if {[string match -* $parentname]} { + if {[string match -* $parentname] && $parentname ni $known_flags} { lappend known_flags $parentname } if {[dict exists $pinfo sub]} { diff --git a/src/modules/funcl-0.1.tm b/src/modules/funcl-0.1.tm index 2ee1ef56..ccdc9d99 100644 --- a/src/modules/funcl-0.1.tm +++ b/src/modules/funcl-0.1.tm @@ -38,7 +38,7 @@ namespace eval funcl { set end [lindex $args end] if {[llength $end] == 1 && [arg_is_script_shaped $end]} { - set endfunc [string map [list $end] {uplevel 1 [list if 1 ]}] + set endfunc [string map " $end" {uplevel 1 [list if 1 ]}] } else { set endfunc $end } @@ -232,7 +232,7 @@ namespace eval funcl { } set comp [list] ;#composition list set end [lindex $args end] - if {[lindex $end 0] in [list "_fn" "_call"]} { + if {[lindex $end 0] in {_fn _call}]} { #is_funcl set endfunc [lindex $args end] } else { diff --git a/src/modules/natsort-0.1.1.6.tm b/src/modules/natsort-0.1.1.6.tm index d15015e9..92bb7e74 100644 --- a/src/modules/natsort-0.1.1.6.tm +++ b/src/modules/natsort-0.1.1.6.tm @@ -242,7 +242,7 @@ namespace eval natsort { proc hex2dec {largeHex} { #todo - use punk::lib::hex2dec - (scan supports ll so can do larger hex values directly) set res 0 - set largeHex [string map [list _ ""] $largeHex] + set largeHex [string map {_ {}} $largeHex] if {[string length $largeHex] <=7} { #scan can process up to FFFFFFF and does so quickly return [scan $largeHex %x] @@ -392,7 +392,7 @@ namespace eval natsort { proc get_char_count {str char} { #faster than lsearch on split for str of a few K - expr {[string length $str]-[string length [string map [list $char {}] $str]]} + expr {[tcl::string::length $str]-[tcl::string::length [tcl::string::map "$char {}" $str]]} } proc build_key {chunk splitchars topdict tagconfig debug} { diff --git a/src/modules/patternpunk-1.1.tm b/src/modules/patternpunk-1.1.tm index c90d7ede..8c72e673 100644 --- a/src/modules/patternpunk-1.1.tm +++ b/src/modules/patternpunk-1.1.tm @@ -92,7 +92,7 @@ set ::punk::bannerTemplate [string trim { / \ _+ +_ } \n] ->punk .. Property front_2003 [string trim [string map [list % \u2003] { +>punk .. Property front_2003 [string trim [string map "% \u2003" { _|_ @%v%@ %~% @@ -119,7 +119,7 @@ set ::punk::bannerTemplate [string trim { / \ _+ +_ } \n] ->punk .. Property rhs_2003 [string trim [string map [list % \u2003] { +>punk .. Property rhs_2003 [string trim [string map "% \u2003" { \\\_ \@%%> |%~ @@ -143,7 +143,7 @@ set ::punk::bannerTemplate [string trim { / \ _+ +_ } \n] ->punk .. Property lhs_2003 [string trim [string map [list % \u2003] { +>punk .. Property lhs_2003 [string trim [string map "% \u2003" { _/// <%%@/ ~%| diff --git a/src/modules/poshinfo-999999.0a1.0.tm b/src/modules/poshinfo-999999.0a1.0.tm new file mode 100644 index 00000000..79e6b2c1 --- /dev/null +++ b/src/modules/poshinfo-999999.0a1.0.tm @@ -0,0 +1,276 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2024 +# +# @@ Meta Begin +# Application poshinfo 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_poshinfo 0 999999.0a1.0] +#[copyright "2024"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require poshinfo] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of poshinfo +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by poshinfo +#[list_begin itemized] + +package require Tcl 8.6- +package require punk::config +package require json ;#tcllib +#toml, yaml? + +#*** !doctools +#[item] [package {Tcl 8.6}] +#[item] [package {punk::config}] +#[item] [package {json}] + +# #package require frobz +# #*** !doctools +# #[item] [package {frobz}] + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# oo::class namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#tcl::namespace::eval poshinfo::class { + #*** !doctools + #[subsection {Namespace poshinfo::class}] + #[para] class definitions + #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { + #*** !doctools + #[list_begin enumerated] + + # oo::class create interface_sample1 { + # #*** !doctools + # #[enum] CLASS [class interface_sample1] + # #[list_begin definitions] + + # method test {arg1} { + # #*** !doctools + # #[call class::interface_sample1 [method test] [arg arg1]] + # #[para] test method + # puts "test: $arg1" + # } + + # #*** !doctools + # #[list_end] [comment {-- end definitions interface_sample1}] + # } + + #*** !doctools + #[list_end] [comment {--- end class enumeration ---}] + #} +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Base namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval poshinfo { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + #variable xyz + + #*** !doctools + #[subsection {Namespace poshinfo}] + #[para] Core API functions for poshinfo + #[list_begin definitions] + + + + #proc sample1 {p1 n args} { + # #*** !doctools + # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] + # #[para]Description of sample1 + # #[para] Arguments: + # # [list_begin arguments] + # # [arg_def tring p1] A description of string argument p1. + # # [arg_def integer n] A description of integer argument n. + # # [list_end] + # return "ok" + #} + + proc info_from_filename {fname} { + #string based filename processing: we are deliberately avoiding test of file existence etc here + if {$fname eq ""} { + error "poshinfo::info_from_filename unable to determine name from empty string" + } + if {[string first . $fname] < 0} { + #theoretically we could have a file without dots - but it's more likely an error in this context + error "poshinfo::info_from_filename supplied value '$fname' doesn't look like a filename. Cowardly refusing to guess a shortname." + } + set ftail [file tail $fname] + set rootname [file rootname $ftail] + set format [string trimleft [file extension $ftail] .] + set parts [split $rootname .] + if {[lindex $parts end] eq "omp"} { + set type omp + set shortname [join [lrange $parts 0 end-1] .] + } else { + if {$rootname eq "schema"} { + set type schema + } else { + set type unknown + } + set shortname $rootname + } + return [dict create shortname $shortname format $format type $type] + } + + proc themes_dict {{globfor *}} { + set running_config [punk::config::get running-config] + set posh_themes_path [tcl::dict::get $running_config posh_themes_path] + #posh_themes_path_extra ?? + + set themes [tcl::dict::create] + + if {[string length $posh_themes_path]} { + if {[file exists $posh_themes_path]} { + set files [glob -nocomplain -directory $posh_themes_path -tails $globfor] + foreach ftail $files { + set themeinfo [info_from_filename $ftail] + set shortname [dict get $themeinfo shortname] + dict set themeinfo path [file join $posh_themes_path $ftail] + if {![dict exists $themes $shortname]} { + dict set themes $shortname [list $themeinfo] + } else { + dict lappend themes $shortname $themeinfo + } + } + } + } + return $themes + } + proc themes {{globfor *}} { + set themes [themes_dict $globfor] + set posh_theme [file normalize [punk::config::get_running_global posh_theme]] + set t [textblock::class::table new "Posh Themes"] + $t configure -show_header 1 -show_hseps 0 + $t add_column -headers Shortname + $t add_column -headers Path + dict for {shortname themeinfolist} $themes { + #hack - support just one for now + set themeinfo [lindex $themeinfolist 0] + + set path [dict get $themeinfo path] + $t add_row [list $shortname $path] + set fg "" + set bg "" + switch -- [dict get $themeinfo type] { + schema { + set bg Web-orange + } + omp {} + unknown { + set bg Web-red + } + default { + #we shouldn't be getting other values + set bg Web-yellow + } + } + if {$posh_theme eq [file normalize $path]} { + set fg web-limegreen + } + if {"$fg$bg" ne ""} { + $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fg {*}$bg] + } + } + set result [$t print] + $t destroy + return $result + } + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace poshinfo ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval poshinfo::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace poshinfo::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace poshinfo::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[section Internal] +#tcl::namespace::eval poshinfo::system { + #*** !doctools + #[subsection {Namespace poshinfo::system}] + #[para] Internal functions that are not part of the API + + + +#} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide poshinfo [tcl::namespace::eval poshinfo { + variable pkg poshinfo + variable version + set version 999999.0a1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/modules/poshinfo-buildversion.txt b/src/modules/poshinfo-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/poshinfo-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index b2d70db9..fbadc295 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -1154,14 +1154,14 @@ namespace eval punk { set script "proc $cmdname {leveldata} {" append script \n [string map [list $selector] {set selector ""}] ;# script should only need for error msgs set subindices [split $selector /] - append script \n [string map [list [list $subindices] ] {set subindices }] + append script \n [string map [list [list $subindices]] {set subindices }] set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break append script \n {set action ?match} #append script \n {set assigned ""} ;#review set active_key_type "" append script \n {# set activey_key_type ""} set lhs $selector - append script \n [string map [list $selector ] {set lhs ""}] + append script \n [string map [list $selector] {set lhs ""}] set rhs "" append script \n {set rhs ""} @@ -1816,7 +1816,7 @@ namespace eval punk { if {$end < 0} { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on] } - append script \n [string map [list $end] { + append script \n [string map " $end" { set end if {$end+1 > $len} { set action ?mismatch-list-index-out-of-range @@ -1831,7 +1831,7 @@ namespace eval punk { if {$endoffset > 0} { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } - append script \n [string map [list $endoffset] { + append script \n [string map " $endoffset" { set endoffset if {abs($endoffset) >= $len} { set action ?mismatch-list-index-out-of-range @@ -3426,11 +3426,11 @@ namespace eval punk { #exclude quoted whitespace proc arg_is_script_shaped {arg} { - if {[string first \n $arg] >= 0} { + if {[tcl::string::first \n $arg] >= 0} { return 1 - } elseif {[string first ";" $arg] >= 0} { + } elseif {[tcl::string::first ";" $arg] >= 0} { return 1 - } elseif {[string first " " $arg] >= 0 || [string first \t $arg] >= 0} { + } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found return [expr {$part2 ne ""}] } else { @@ -3478,7 +3478,7 @@ namespace eval punk { } incr i } - set tail [string range $fullrhs $i end] + set tail [tcl::string::range $fullrhs $i end] return [list $equalsrhs $tail] } @@ -4577,7 +4577,7 @@ namespace eval punk { #---------------- #can't use know - because we don't want to return before original unknown body is called. - proc ::unknown {args} [string map [list] { + proc ::unknown {args} [string cat { package require base64 #set ::punk::last_run_display [list] #set ::repl::last_unknown [lindex $args 0] ;#jn @@ -5135,31 +5135,6 @@ namespace eval punk { return $fullpath } - #todo - something better - 'previous' rather than reverting to startup - proc channelcolors {{onoff {}}} { - upvar ::punk::config::running running_config - upvar ::punk::config::startup startup_config - - if {![string length $onoff]} { - return [list stdout [dict get $running_config color_stdout] stderr [dict get $running_config color_stderr]] - } else { - set lower_onoff [string tolower $onoff] - switch -- $lower_onoff { - true - on - 1 { - dict set running_config color_stdout [dict get $startup_config color_stdout] - dict set running_config color_stderr [dict get $startup_config color_stderr] - } - false - off - 0 { - dict set running_config color_stdout "" - dict set running_config color_stderr "" - } - default { - error "channelcolors: invalid value $onoff - expected true|false|on|off|1|0" - } - } - } - return [list stdout [dict get $running_config color_stdout] stderr [dict get $running_config color_stderr]] - } #useful for aliases e.g treemore -> xmore tree proc xmore {args} { @@ -5525,6 +5500,9 @@ namespace eval punk { #maint - punk::args has similar + #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args + #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? #JMN #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. @@ -5636,10 +5614,11 @@ namespace eval punk { #dirfiles assumes we don't have glob chars in the filenames or paths - dirfiles_dict can be called directly with explicit -tailglob in the rare case that assumption doesn't hold # dirfiles will test last segment (tail) of supplied searchspecs for fileness vs folderness (when no globchars present in tail) so that it can pass the appropriate flags downstream proc dirfiles {args} { - set defaults [list\ - -stripbase 1\ - ] - lassign [dict values [get_leading_opts_and_values $defaults $args]] opts searchspecs ;#implicit merge of opts over defaults + set argspecs { + -stripbase -default 1 -type boolean + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts searchspecs set opt_stripbase [dict get $opts -stripbase] @@ -5716,13 +5695,18 @@ namespace eval punk { #if caller supplies a tailglob as empty string - presume the caller hasn't set location to parentdir - and that last element is the search pattern. # -searchbase is always passed through - and is only used to construct a location path if a relative searchspec was supplied proc dirfiles_dict {args} { - set defaults [dict create\ - -searchbase ""\ - -tailglob "\uFFFF"\ - -with_sizes "\uFFFF"\ - -with_times "\uFFFF"\ - ] - lassign [dict values [get_leading_opts_and_values $defaults $args]] opts searchspecs + set argspecs { + *opts -any 0 + -searchbase -default "" + -tailglob -default "\uFFFF" + #with_sizes & with_times must accept 0|1|f|d|l where f = files d = dirs l = links (punk::du) + -with_sizes -default "\uFFFF" -type string + -with_times -default "\uFFFF" -type string + *values -min 0 -max -1 -type string + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] #puts stderr "searchspecs: $searchspecs [llength $searchspecs]" #puts stdout "arglist: $opts" @@ -5798,13 +5782,13 @@ namespace eval punk { #leave up to listing-provider defaults set next_opt_with_sizes "" } else { - set next_opt_with_sizes "-with_sizes $opt_with_sizes" + set next_opt_with_sizes [list -with_sizes $opt_with_sizes] } if {$opt_with_times eq "\uFFFF"} { #leave up to listing-provider defaults set next_opt_with_times "" } else { - set next_opt_with_times "-with_times $opt_with_times" + set next_opt_with_times [list -with_times $opt_with_times] } if {$in_vfs} { set listing [punk::du::lib::du_dirlisting_tclvfs $location -glob $glob {*}$next_opt_with_sizes {*}$next_opt_with_times] @@ -5855,10 +5839,13 @@ namespace eval punk { #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden if {$::tcl_platform(platform) ne "windows"} { lappend flaggedhidden {*}[lsearch -all -inline [concat $dirs $files] ".*"] - set flaggedhidden [lsort -unique $flaggedhidden] + #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs + #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely + #set flaggedhidden [lsort -unique $flaggedhidden] + set flaggedhidden [punk::lib::lunique_unordered $flaggedhidden] } - set dirs [lsort $dirs] ;#todo - natsort + set dirs [lsort $dirs] ;#todo - natsort @@ -5908,10 +5895,15 @@ namespace eval punk { proc dirfiles_dict_as_lines {args} { package require overtype - set defaults [list\ - -stripbase 0\ - ] - lassign [dict values [get_leading_opts_and_values $defaults $args]] opts list_of_dicts ;#implicit merge of opts over defaults + set argspecs { + -stripbase -default 0 -type boolean + *values -min 1 -max -1 -type dict + } + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set list_of_dicts [dict values $vals] + + # -- --- --- --- --- --- --- --- --- --- --- --- set opt_stripbase [dict get $opts -stripbase] @@ -6176,7 +6168,7 @@ namespace eval punk { proc delimit_number {unformattednumber {delim ","} {GroupSize 3}} { set number [punk::objclone $unformattednumber] - set number [string map [list _ ""] $number] + set number [string map {_ ""} $number] #normalize using expr - e.g 2e4 -> 20000.0 set number [expr {$number}] # First, extract right hand part of number, up to and including decimal point @@ -6565,15 +6557,6 @@ namespace eval punk { return $result } - #proc list_as_lines {args} { - # set defaults [dict create\ - # -joinchar "\n"\ - # ] - # lassign [dict values [get_leading_opts_and_values $defaults $args -minvalues 1 -maxvalues 1]] opts values - # set opt_joinchar [dict get $opts -joinchar] - # set list [lindex $values 0] - # join $list $opt_joinchar - #} #-------------------------------------------------- #some haskell-like operations @@ -6895,13 +6878,16 @@ namespace eval punk { #An implementation of a notoriously controversial metric. proc LOC {args} { - set defaults [dict create\ - -dir "\uFFFF"\ - -exclude_dupfiles 1\ - -exclude_punctlines 1\ - -punctchars [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?]\ - ] - lassign [dict values [get_leading_opts_and_values $defaults $args]] opts searchspecs ;#implicit merge of opts over defaults + set argspecs [subst { + -dir -default "\uFFFF" + -exclude_dupfiles -default 1 -type boolean + -exclude_punctlines -default 1 -type boolean + -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } + }] + set argd [punk::args::get_dict $argspecs $args] + lassign [dict values $argd] opts vals + set searchspecs [dict values $vals] + # -- --- --- --- --- --- set opt_dir [dict get $opts -dir] if {$opt_dir eq "\uFFFF"} { @@ -7299,16 +7285,26 @@ namespace eval punk { if {[punk::lib::system::has_script_var_bug]} { append warningblock \n "minor warning: punk::lib::system::has_script_var_bug returned true! (string rep for list variable in script generated when script changed)" } + if {[punk::lib::system::has_safeinterp_compile_bug]} { + set indent " " + append warningblock \n "[a+ web-red]warning: punk::lib::system::has_safeinterp_compile_bug returned true!" \n + append warningblock "${indent}(ensemble commands not compiled in safe interps - heavy performance impact in safe interps)" \n + append warningblock "${indent}see https://core.tcl-lang.org/tcl/tktview/1095bf7f75" + append warningblock [a] + } } set text "" if {$topic in [list env environment]} { - set known $::punk::config::known_punk_env_vars + #todo - move to punk::config? + + set known_punk $::punk::config::known_punk_env_vars + set known_other $::punk::config::known_other_env_vars append text \n set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] - foreach v $known { + foreach v $known_punk { if {[info exists ::env($v)]} { set c2 [set ::env($v)] } else { @@ -7319,8 +7315,24 @@ namespace eval punk { $t configure_column 0 -headers [list "Punk environment vars"] $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {all} - append text [$t print]\n + set punktable [$t print] + $t destroy + + set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] + foreach v $known_other { + if {[info exists ::env($v)]} { + set c2 [set ::env($v)] + } else { + set c2 "(NOT SET)" + } + $t add_row [list $v $c2] + } + $t configure_column 0 -headers [list "Other environment vars"] + $t configure_column 0 -minwidth [expr {[$t column_datawidth 0]+4}] -blockalign left -textalign left -header_colspans {all} + + set othertable [$t print] $t destroy + append text [textblock::join $punktable " " $othertable]\n } else { append text $linesep\n @@ -7328,7 +7340,7 @@ namespace eval punk { append text $linesep\n set col1 [string repeat " " 25] set col2 [string repeat " " 50] - foreach v $known { + foreach v $known_punk { set c1 [overtype::left $col1 $v] if {[info exists ::env($v)]} { set c2 [overtype::left $col2 [set ::env($v)] @@ -7434,7 +7446,7 @@ namespace eval punk { #NOTE: an alias may match in a namespace - but not have a corresponding command that matches that name (alias renamed) proc aliases {{glob *}} { set ns [uplevel 1 {::namespace current}] ;#must use :: - we can find ourselves in a namespace with a different 'namespace' command - set ns_mapped [string map [list :: \uFFFF] $ns] + set ns_mapped [string map {:: \uFFFF} $ns] #puts stderr "aliases ns: $ns_mapped" set segments [split $ns_mapped \uFFFF] ;#include empty string before leading :: if {![string length [lindex $segments end]]} { @@ -7454,7 +7466,7 @@ namespace eval punk { set abs $a } - set asegs [split [string map [list :: \uFFFF] $abs] \uFFFF] + set asegs [split [string map {:: \uFFFF} $abs] \uFFFF] set acount [llength $asegs] #puts "alias $abs acount:$acount asegs:$asegs segcount:$segcount segments: $segments" if {[expr {$acount - 1}] == $segcount} { @@ -7570,7 +7582,8 @@ namespace eval punk { #file normalize {//host/share} -> //host/share #To get back to some consistent cross platform behaviour - we will treat //something as a root/volume i.e we can't backtrack above it with .. proc filepath_dotted_minimal {path} { - set path [string map [list \\ /] $path] + #set path [string map [list \\ /] $path] + set path [string map "\\\\ /" $path] set doubleslash1_posn [string first // $path] if {[punk::winpath::is_dos_device_path $path]} { @@ -7583,7 +7596,7 @@ namespace eval punk { #e.g on freebsd: -> / sharehost share path etc #however..also on windows: file split //sharehost -> / sharehost #normalize by dropping leading slash before split - and then treating first 2 segments as a root - set normtail [string map [list //]] + set normtail [string map {// ""} $path] set parts [file split [string range $path 1 end]] diff --git a/src/modules/punk/ansi-999999.0a1.0.tm b/src/modules/punk/ansi-999999.0a1.0.tm index 096c34d0..29a4fad7 100644 --- a/src/modules/punk/ansi-999999.0a1.0.tm +++ b/src/modules/punk/ansi-999999.0a1.0.tm @@ -2406,7 +2406,9 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } underextendedoff { #lremove any existing 4:1 etc - set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + #NOTE struct::set result order can differ depending on whether tcl/critcl imp used + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } undersingle { @@ -2759,7 +2761,9 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } underextendedoff { #lremove any existing 4:1 etc - set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } undersingle { diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index fffd3cf2..589ab69f 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -191,9 +191,10 @@ #https://core.tcl-lang.org/tcl/tktview/1095bf7f75 #as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. #(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary chan clock dict encoding info namespace string +#ensembles: array binary clock dict info namespace string #possibly file too, although that is generally hidden/modified in a safe interp - +#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc +#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp #*** !doctools #[subsection dependencies] @@ -304,6 +305,7 @@ tcl::namespace::eval punk::args { set opt_required [list] set val_required [list] set arg_info [tcl::dict::create] + set arg_checks [tcl::dict::create] set opt_defaults [tcl::dict::create] set opt_names [list] ;#defined opts set val_defaults [tcl::dict::create] @@ -411,7 +413,29 @@ tcl::namespace::eval punk::args { -nominlen - -nomaxlen - -norange - -nochoices - -nochoicelabels { tcl::dict::unset optspec_defaults $k } - -type - + -type { + switch -- $v { + int - integer { + set v int + } + char - character { + set v char + } + bool - boolean { + set v bool + } + dict - dictionary { + set v dict + } + any - ansistring { + + } + default { + #todo - disallow unknown types unless prefixed with custom- + } + } + tcl::dict::set optspec_defaults $k $v + } -optional - -allow_ansi - -validate_without_ansi - @@ -444,7 +468,26 @@ tcl::namespace::eval punk::args { -nominlen - -nomaxlen - -norange - -nochoices - -nochoicelabels { tcl::dict::unset valspec_defaults $k } - -type - + -type { + switch -- $v { + int - integer { + set v int + } + char - character { + set v char + } + bool - boolean { + set v bool + } + dict - dictionary { + set v dict + } + default { + #todo - disallow unknown types unless prefixed with custom- + } + } + tcl::dict::set valspec_defaults $k $v + } -allow_ansi - -validate_without_ansi - -strip_ansi - @@ -480,7 +523,11 @@ tcl::namespace::eval punk::args { } #assert - we only get here if it is a value or flag specification line. #assert argspecs has been set to the value of linespecs - set merged $optspec_defaults + if {$is_opt} { + set spec_merged $optspec_defaults + } else { + set spec_merged $valspec_defaults + } foreach {spec specval} $argspecs { #literal-key switch - bytecompiled to jumpTable switch -- $spec { @@ -488,31 +535,38 @@ tcl::namespace::eval punk::args { #normalize here so we don't have to test during actual args parsing in main function switch -- [tcl::string::tolower $specval] { int - integer { - tcl::dict::set merged -type int + tcl::dict::set spec_merged -type int } bool - boolean { - tcl::dict::set merged -type bool + tcl::dict::set spec_merged -type bool } char - character { - tcl::dict::set merged -type char + tcl::dict::set spec_merged -type char + } + dict - dictionary { + tcl::dict::set spec_merged -type dict } "" - none { if {$is_opt} { - tcl::dict::set merged -type none - tcl::dict::set merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + tcl::dict::set spec_merged -type none + tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. lappend opt_solos $argname } else { #-solo only valid for flags error "punk::args::get_dict - invalid -type 'none' for positional argument positional argument '$argname'" } } + any - ansistring { + tcl::dict::set spec_merged -type dict + } default { - tcl::dict::set merged -type [tcl::string::tolower $specval] + #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW + tcl::dict::set spec_merged -type [tcl::string::tolower $specval] } } } -default - -solo - -range - -choices - -choicelabels - -minlen - -maxlen - -nocase - -optional - -multiple - -validate_without_ansi - -allow_ansi - -strip_ansi - -help - -ARGTYPE { - tcl::dict::set merged $spec $specval + tcl::dict::set spec_merged $spec $specval } default { set known_argspecs [list -default -type -range -choices -nocase -optional -multiple -validate_without_ansi -allow_ansi -strip_ansi -help] @@ -520,12 +574,12 @@ tcl::namespace::eval punk::args { } } } - set argspecs $merged - #if {$is_opt} { + set argspecs $spec_merged + if {$is_opt} { set argchecks [tcl::dict::remove $argspecs -type -default -multiple -strip_ansi -validate_without_ansi -allow_ansi] ;#leave things like -range -minlen - #} else { - # set argchecks [tcl::dict::remove $argspecs -type -default -multiple -strip_ansi -validate_without_ansi -allow_ansi] ;#leave things like -range -minlen - #} + } else { + set argchecks [tcl::dict::remove $argspecs -type -default -multiple -strip_ansi -validate_without_ansi -allow_ansi] ;#leave things like -range -minlen + } tcl::dict::set arg_info $argname $argspecs tcl::dict::set arg_checks $argname $argchecks if {![tcl::dict::get $argspecs -optional]} { @@ -555,6 +609,10 @@ tcl::namespace::eval punk::args { set spec_id "autoid_[incr id_counter]" } + + set opt_checks_defaults [tcl::dict::remove $optspec_defaults -type -default -multiple -strip_ansi -validate_without_ansi -allow_ansi] ;#leave things like -range -minlen + set val_checks_defaults [tcl::dict::remove $valspec_defaults -type -default -multiple -strip_ansi -validate_without_ansi -allow_ansi] ;#leave things like -range -minlen + set result [tcl::dict::create\ id $spec_id\ arg_info $arg_info\ @@ -565,13 +623,14 @@ tcl::namespace::eval punk::args { opt_any $opt_any\ opt_solos $opt_solos\ optspec_defaults $optspec_defaults\ - valspec_defaults $valspec_defaults\ + opt_checks_defaults $opt_checks_defaults\ val_defaults $val_defaults\ val_required $val_required\ val_names $val_names\ val_min $val_min\ val_max $val_max\ valspec_defaults $valspec_defaults\ + val_checks_defaults $val_checks_defaults\ proc_info $proc_info\ ] tcl::dict::set argspec_cache $cache_key $result @@ -760,6 +819,7 @@ tcl::namespace::eval punk::args { set newval [lindex $rawargs $i+1] #opt was unspecified but is allowed due to *opt -any 1 - 'adhoc/passthrough' option tcl::dict::set arg_info $a $optspec_defaults ;#use default settings for unspecified opt + tcl::dict::set arg_checks $a $opt_checks_defaults if {[tcl::dict::get $arg_info $a -type] ne "none"} { if {[tcl::dict::get $arg_info $a -multiple]} { tcl::dict::lappend opts $a $newval @@ -822,6 +882,7 @@ tcl::namespace::eval punk::args { } else { tcl::dict::set values_dict $validx $val tcl::dict::set arg_info $validx $valspec_defaults + tcl::dict::set arg_checks $validx $val_checks_defaults lappend valnames_received $validx } } @@ -856,13 +917,23 @@ tcl::namespace::eval punk::args { #safe interp note - cannot avoid struct::set difference ensemble as it could be c or tcl implementation and we don't have an option to call directly? #example timing difference: #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs save interp 9.4us - if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { + #normal interp 0.18 u2 vs safe interp 9.4us + #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { + # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" + #} + #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { + # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" + #} + #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us + if {[llength [set missing [punk::lib::ldiff $opt_required $flagsreceived]]]} { error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" } - if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { + if {[llength [set missing [punk::lib::ldiff $val_required $valnames_received]]]} { error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" } + + + #todo - allow defaults outside of choices/ranges #check types,ranges,choices @@ -1004,6 +1075,13 @@ tcl::namespace::eval punk::args { } } } + dict { + foreach e_check $vlist_check { + if {[llength $e_check] %2 != 0} { + error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" + } + } + } alnum - alpha - ascii - diff --git a/src/modules/punk/cap-999999.0a1.0.tm b/src/modules/punk/cap-999999.0a1.0.tm index 7a3cf2ac..c6c7a3b7 100644 --- a/src/modules/punk/cap-999999.0a1.0.tm +++ b/src/modules/punk/cap-999999.0a1.0.tm @@ -48,12 +48,12 @@ package require oolib # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::cap { - variable pkgcapsdeclared [dict create] - variable pkgcapsaccepted [dict create] - variable caps [dict create] +tcl::namespace::eval punk::cap { + variable pkgcapsdeclared [tcl::dict::create] + variable pkgcapsaccepted [tcl::dict::create] + variable caps [tcl::dict::create] namespace eval class { - if {[info commands [namespace current]::interface_caphandler.registry] eq ""} { + if {[tcl::info::commands ::punk::cap::class::interface_caphandler.registry] eq ""} { #*** !doctools #[subsection {Namespace punk::cap::class}] #[para] class definitions @@ -62,7 +62,7 @@ namespace eval punk::cap { # [para] [emph {handler_classes}] # [list_begin enumerated] - oo::class create [namespace current]::interface_caphandler.registry { + oo::class create ::punk::cap::class::interface_caphandler.registry { #*** !doctools #[enum] CLASS [class interface_caphandler.registry] #[list_begin definitions] @@ -83,7 +83,7 @@ namespace eval punk::cap { #[list_end] } - oo::class create [namespace current]::interface_caphandler.sysapi { + oo::class create ::punk::cap::class::interface_caphandler.sysapi { #*** !doctools #[enum] CLASS [class interface_caphandler.sysapi] #[list_begin definitions] @@ -103,7 +103,7 @@ namespace eval punk::cap { # [list_begin enumerated] #Provider classes - oo::class create [namespace current]::interface_capprovider.registration { + oo::class create ::punk::cap::class::interface_capprovider.registration { #*** !doctools # [enum] CLASS [class interface_cappprovider.registration] # [para]Your provider package will need to instantiate this object under a sub-namespace called [namespace capsystem] within your package namespace. @@ -140,7 +140,7 @@ namespace eval punk::cap { # [list_end] } - oo::class create [namespace current]::interface_capprovider.provider { + oo::class create ::punk::cap::class::interface_capprovider.provider { #*** !doctools # [enum] CLASS [class interface_capprovider.provider] # [para] Your provider package will need to instantiate this directly under it's own namespace with the command name of [emph {provider}] @@ -157,7 +157,7 @@ namespace eval punk::cap { #*** !doctools #[call class::interface_capprovider.provider [method constructor] [arg providerpkg]] variable provider_pkg - if {$providerpkg in [list "" "::"]} { + if {$providerpkg in {"" "::"}} { error "interface_capprovider.provider constructor error. Invalid provider '$providerpkg'" } if {![namespace exists ::$providerpkg]} { @@ -165,12 +165,12 @@ namespace eval punk::cap { } set registrationobj ::${providerpkg}::capsystem::capprovider.registration - if {[info commands $registrationobj] eq ""} { + if {[tcl::info::commands $registrationobj] eq ""} { error "capprovider.provider constructor error. Missing capprovider.registration interface at '$obj' (command not found) interface_capprovider.regstration instantiation must precede interface_capprovider.provider" } - set provider_pkg [string trim $providerpkg ""] - + #review - what are we trying to achieve here? + set provider_pkg [tcl::string::trim $providerpkg ""] } method register {{capabilityname_glob *}} { #*** !doctools @@ -232,13 +232,13 @@ namespace eval punk::cap { #such unregistered capabilitynames may be used just to flag something, or have datamembers significant to callers cooperatively interested in that capname. #we allow registering a capability with an empty handler (capnamespace) - but this means another handler could be registered later. proc register_capabilityname {capname capnamespace} { - puts stderr "REGISTER_CAPABILITYNAME $capname $capnamespace" + #puts stderr "REGISTER_CAPABILITYNAME $capname $capnamespace" variable caps variable pkgcapsdeclared variable pkgcapsaccepted if {$capnamespace ne ""} { #normalize with leading :: in case caller passed in package name rather than fully qualified namespace - if {![string match ::* $capnamespace]} { + if {![tcl::string::match ::* $capnamespace]} { set capnamespace ::$capnamespace } } @@ -250,20 +250,21 @@ namespace eval punk::cap { return } #assertion: capnamespace may or may not be empty string, capname may or may not already exist in caps dict, caps $capname providers may have existing entries. - dict set caps $capname handler $capnamespace - if {![dict exists $caps $capname providers]} { - dict set caps $capname providers [list] + tcl::dict::set caps $capname handler $capnamespace + if {![tcl::dict::exists $caps $capname providers]} { + tcl::dict::set caps $capname providers [list] } - if {[llength [set providers [dict get $caps $capname providers]]]} { + if {[llength [set providers [tcl::dict::get $caps $capname providers]]]} { #some provider(s) were in place before the handler was registered if {[set capreg [punk::cap::capsystem::get_caphandler_registry $capname]] ne ""} { foreach pkg $providers { - set fullcapabilitylist [dict get $pkgcapsdeclared $pkg] - foreach capspec $fullcapabilitylist { + set fullcapabilitylist [tcl::dict::get $pkgcapsdeclared $pkg] + set capname_capabilitylist [lsearch -all -inline -index 0 $fullcapabilitylist $capname] + foreach capspec $capname_capabilitylist { lassign $capspec cn capdict - if {$cn ne $capname} { - continue - } + #if {$cn ne $capname} { + # continue + #} if {[catch {$capreg pkg_register $pkg $capdict $fullcapabilitylist} do_register]} { puts stderr "punk::cap::register_capabilityname '$capname' '$capnamespace' failed to register provider package '$pkg' - possible error in handler or provider" puts stderr "error message:" @@ -271,22 +272,22 @@ namespace eval punk::cap { set do_register 0 } - set list_accepted [dict get $pkgcapsaccepted $pkg] + set list_accepted [tcl::dict::get $pkgcapsaccepted $pkg] if {$do_register} { if {$capspec ni $list_accepted} { - dict lappend pkgcapsaccepted $pkg $capspec + tcl::dict::lappend pkgcapsaccepted $pkg $capspec } } else { set posn [lsearch $list_accepted $capspec] if {$posn >=0} { set list_accepted [lreplace $list_accepted $posn $posn] - dict set pkgcapsaccepted $pkg $list_accepted + tcl::dict::set pkgcapsaccepted $pkg $list_accepted } } } #check if any accepted for this cap and remove from caps as necessary set count 0 - foreach accepted_capspec [dict get $pkgcapsaccepted $pkg] { + foreach accepted_capspec [tcl::dict::get $pkgcapsaccepted $pkg] { if {[lindex $accepted_capspec 0] eq $capname} { incr count } @@ -295,7 +296,7 @@ namespace eval punk::cap { set pkgposn [lsearch $providers $pkg] if {$pkgposn >= 0} { set updated_providers [lreplace $providers $posn $posn] - dict set caps $capname providers $updated_providers + tcl::dict::set caps $capname providers $updated_providers } } } @@ -309,14 +310,14 @@ namespace eval punk::cap { # [call [fun capability_exists] [arg capname]] # Return a boolean indicating if the named capability exists (0|1) variable caps - return [dict exists $caps $capname] + return [tcl::dict::exists $caps $capname] } proc capability_has_handler {capname} { #*** !doctools # [call [fun capability_has_handler] [arg capname]] #Return a boolean indicating if the named capability has a handler package installed (0|1) variable caps - return [expr {[dict exists $caps $capname handler] && [dict get $caps $capname handler] ne ""}] + return [expr {[tcl::dict::exists $caps $capname handler] && [tcl::dict::get $caps $capname handler] ne ""}] } proc capability_get_handler {capname} { #*** !doctools @@ -324,8 +325,8 @@ namespace eval punk::cap { #Return the base namespace of the active handler package for the named capability. #[para] The base namespace for a handler will always be the package name, but prefixed with :: variable caps - if {[dict exists $caps $capname]} { - return [dict get $caps $capname handler] + if {[tcl::dict::exists $caps $capname]} { + return [tcl::dict::get $caps $capname handler] } return "" } @@ -338,8 +339,8 @@ namespace eval punk::cap { } proc get_providers {capname} { variable caps - if {[dict exists $caps $capname]} { - return [dict get $caps $capname providers] + if {[tcl::dict::exists $caps $capname]} { + return [tcl::dict::get $caps $capname providers] } return [list] } @@ -356,26 +357,26 @@ namespace eval punk::cap { foreach {k v} $args { switch -- $k { -nowarnings { - dict set opts $k $v + tcl::dict::set opts $k $v } default { - error "Unrecognized option $k. Known options [dict keys $opts]" + error "Unrecognized option $k. Known options [tcl::dict::keys $opts]" } } } - set warnings [expr {! [dict get $opts -nowarnings]}] + set warnings [expr {! [tcl::dict::get $opts -nowarnings]}] - if {[string match ::* $pkg]} { - set pkg [string range $pkg 2 end] + if {[tcl::string::match ::* $pkg]} { + set pkg [tcl::string::range $pkg 2 end] } - if {[dict exists $pkgcapsaccepted $pkg]} { - set pkg_already_accepted [dict get $pkgcapsaccepted $pkg] + if {[tcl::dict::exists $pkgcapsaccepted $pkg]} { + set pkg_already_accepted [tcl::dict::get $pkgcapsaccepted $pkg] } else { set pkg_already_accepted [list] } package require $pkg set providerapi ::${pkg}::provider - if {[info commands $providerapi] eq ""} { + if {[tcl::info::commands $providerapi] eq ""} { error "register_package error. pkg '$pkg' doesn't seem to be a punk::cap capability provider (no object found at $providerapi)" } set defined_caps [$providerapi capabilities] @@ -397,13 +398,13 @@ namespace eval punk::cap { if {[llength $capname] !=1} { puts stderr "register_package error. pkg: '$pkg' An entry in the capability list doesn't appear to have a single-word name. Problematic entry:'$capspec'" set reason "First element of capspec not a single-word name" - lappend errorlist [dict create msg $reason capspec $capspec] + lappend errorlist [tcl::dict::create msg $reason capspec $capspec] continue } if {[expr {[llength $capdict] %2 != 0}]} { puts stderr "register_package error. pkg:'$pkg' The second element for capname:'$capname' doesn't appear to be a valid dict. Problematic entry: '$capspec'" set reason "The second element of the capspec isn't a valid dict" - lappend errorlist [dict create msg $reason capspec $capspec] + lappend errorlist [tcl::dict::create msg $reason capspec $capspec] continue } if {$capspec in $pkg_already_accepted} { @@ -411,13 +412,13 @@ namespace eval punk::cap { if {$warnings} { puts stderr "WARNING: register_package pkg $pkg already has capspec marked as accepted: $capspec" } - lappend warninglist [dict create msg "pkg $pkg already has this capspec marked as accepted" capspec $capspec] + lappend warninglist [tcl::dict::create msg "pkg $pkg already has this capspec marked as accepted" capspec $capspec] continue } - if {[dict exists $caps $capname]} { - set cap_pkgs [dict get $caps $capname providers] + if {[tcl::dict::exists $caps $capname]} { + set cap_pkgs [tcl::dict::get $caps $capname providers] } else { - dict set caps $capname [dict create handler "" providers [list]] + dict set caps $capname [tcl::dict::create handler "" providers [list]] set cap_pkgs [list] } #todo - if there's a caphandler - call it's init/validation callback for the pkg @@ -429,31 +430,31 @@ namespace eval punk::cap { if {$do_register} { if {$pkg ni $cap_pkgs} { lappend cap_pkgs $pkg - dict set caps $capname providers $cap_pkgs + tcl::dict::set caps $capname providers $cap_pkgs } - dict lappend pkgcapsaccepted $pkg $capspec ;#if pkg is being registered prior to handler-registration - the handler may undo this entry + tcl::dict::lappend pkgcapsaccepted $pkg $capspec ;#if pkg is being registered prior to handler-registration - the handler may undo this entry } } #another call to register_pkg with same pkg may have been made (most likely with different capname) so we must append - but check not already present #dict lappend pkgcapsdeclared $pkg $capabilitylist - if {[dict exists $pkgcapsdeclared $pkg]} { + if {[tcl::dict::exists $pkgcapsdeclared $pkg]} { #review - untested - set mergecapspecs [dict get $pkgcapsdeclared $pkg] + set mergecapspecs [tcl::dict::get $pkgcapsdeclared $pkg] foreach spec $capabilitylist { if {$spec ni $mergecapspecs} { lappend mergecapspecs $spec } } - dict set pkgcapsdeclared $pkg $mergecapspecs + tcl::dict::set pkgcapsdeclared $pkg $mergecapspecs } else { - dict set pkgcapsdeclared $pkg $capabilitylist + tcl::dict::set pkgcapsdeclared $pkg $capabilitylist } set resultdict [list num_capabilities $capabilitylist_count num_accepted $accepted_count] if {[llength $errorlist]} { - dict set resultdict errors $errorlist + tcl::dict::set resultdict errors $errorlist } if {[llength $warninglist]} { - dict set resultdict warnings $warninglist + tcl::dict::set resultdict warnings $warninglist } return $resultdict } diff --git a/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm b/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm index 25ab3ed2..9ce510f0 100644 --- a/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm +++ b/src/modules/punk/cap/handlers/templates-999999.0a1.0.tm @@ -61,7 +61,7 @@ namespace eval punk::cap::handlers::templates { set path [dict get $capdict path] - set cname [string map [list . _] $capname] + set cname [string map {. _} $capname] set multivendor_package_whitelist [list punk::mix::templates] @@ -226,7 +226,7 @@ namespace eval punk::cap::handlers::templates { method pkg_unregister {pkg} { upvar ::punk::cap::handlers::templates::handled_caps hcaps foreach capname $hcaps { - set cname [string map [list . _] $capname] + set cname [string map {. _} $capname] upvar ::punk::cap::handlers::templates::provider_info_$cname my_provider_info dict unset my_provider_info $pkg #destroy api objects? @@ -249,7 +249,7 @@ namespace eval punk::cap::handlers::templates { constructor {capname} { variable capabilityname variable cname - set cname [string map [list . _] $capname] + set cname [string map {. _} $capname] set capabilityname $capname } method folders {args} { diff --git a/src/modules/punk/config-0.1.tm b/src/modules/punk/config-0.1.tm index bb7b237e..44db9a73 100644 --- a/src/modules/punk/config-0.1.tm +++ b/src/modules/punk/config-0.1.tm @@ -4,106 +4,314 @@ tcl::namespace::eval punk::config { variable startup ;#include env overrides variable running variable known_punk_env_vars + variable known_other_env_vars + + variable vars #todo - XDG_DATA_HOME etc #https://specifications.freedesktop.org/basedir-spec/latest/ # see also: http://hiphish.github.io/blog/2020/08/30/dotfiles-were-a-mistake/ - variable vars - set vars [list \ - apps \ - config \ - configset \ - scriptlib \ - color_stdout \ - color_stderr \ - logfile_stdout \ - logfile_stderr \ - syslog_stdout \ - syslog_stderr \ - syslog_active \ - exec_unknown \ - ] - #todo pkg punk::config - - #defaults - - tcl::dict::set startup configset .punkshell - tcl::dict::set startup exec_unknown true ;#whether to use exec instead of experimental shellfilter::run - #tcl::dict::set startup color_stdout [list cyan bold] ;#not a good idea to default - tcl::dict::set startup color_stdout [list] - - #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. - tcl::dict::set startup color_stderr [list red bold] - - tcl::dict::set startup syslog_stdout "127.0.0.1:514" - tcl::dict::set startup syslog_stderr "127.0.0.1:514" - tcl::dict::set startup syslog_active 0 - #default file logs to logs folder at same location as exe if writable, or empty string - tcl::dict::set startup logfile_stdout "" - tcl::dict::set startup logfile_stderr "" - set exename "" - catch { - #catch for safe interps - #safe base will return empty string, ordinary safe interp will raise error - set exename [tcl::info::nameofexecutable] - } - if {$exename ne ""} { - set exefolder [file dirname $exename] - set log_folder $exefolder/logs - tcl::dict::set startup scriptlib $exefolder/scriptlib - tcl::dict::set startup apps $exefolder/../../punkapps - if {[file exists $log_folder]} { + proc init {} { + variable defaults + variable startup + variable running + variable known_punk_env_vars + variable known_other_env_vars + + set exename "" + catch { + #catch for safe interps + #safe base will return empty string, ordinary safe interp will raise error + set exename [tcl::info::nameofexecutable] + } + if {$exename ne ""} { + set exefolder [file dirname $exename] + #default file logs to logs folder at same level as exe if writable, or empty string + set log_folder [file normalize $exefolder/../logs] + #tcl::dict::set startup scriptlib $exefolder/scriptlib + #tcl::dict::set startup apps $exefolder/../../punkapps + + #todo - use punk main.tcl location instead - exefolder doesn't work if system tclsh used etc + set default_scriptlib $exefolder/scriptlib + set default_apps $exefolder/../../punkapps if {[file isdirectory $log_folder] && [file writable $log_folder]} { - tcl::dict::set startup logfile_stdout $log_folder/repl-exec-stdout.txt - tcl::dict::set startup logfile_stderr $log_folder/repl-exec-stderr.txt + #tcl::dict::set startup logfile_stdout $log_folder/repl-exec-stdout.txt + #tcl::dict::set startup logfile_stderr $log_folder/repl-exec-stderr.txt + set default_logfile_stdout $log_folder/repl-exec-stdout.txt + set default_logfile_stderr $log_folder/repl-exec-stderr.txt + } else { + set default_logfile_stdout "" + set default_logfile_stderr "" } + } else { + #probably a safe interp - which cannot access info nameofexecutable even if access given to the location via punk::island + #review - todo? + #tcl::dict::set startup scriptlib "" + #tcl::dict::set startup apps "" + set default_scriptlib "" + set default_apps "" + set default_logfile_stdout "" + set default_logfile_stderr "" } - } else { - #probably a safe interp - which cannot access info nameofexecutable even if access given to the location via punk::island - #review - todo? - tcl::dict::set startup scriptlib "" - tcl::dict::set startup apps "" - } + # exec_unknown ;#whether to use exec instead of experimental shellfilter::run + #startup color_stdout - parameters as suitable for punk::ansi::a+ (test with 'punk::ansi::a?') e.g "cyan bold" ;#not a good idea to default + set default_color_stdout "" + #This wraps the stderr stream as it comes in with Ansi - probably best to default to empty.. but it's useful. + set default_color_stderr "red bold" - #todo - load/write config file - - #env vars override the configuration - - #todo - define which configvars are settable in env - set known_punk_env_vars [list \ - PUNK_APPS \ - PUNK_CONFIG \ - PUNK_CONFIGSET \ - PUNK_SCRIPTLIB \ - PUNK_EXECUNKNOWN \ - PUNK_COLOR_STDERR \ - PUNK_COLOR_STDOUT \ - PUNK_LOGFILE_STDOUT \ - PUNK_LOGFILE_STDERR \ - PUNK_SYSLOG_STDOUT \ - PUNK_SYSLOG_STDERR \ - PUNK_SYSLOG_ACTIVE \ + set homedir "" + if {[catch { + #depending on which build of tcl - some safe interps prior to bugfix https://core.tcl-lang.org/tcl/info/3aa487993f will return a homedir value in an unmodified safe interp + #other 'safe' interps may have explicitly made this available - we shouldn't override that decision here using interp issafe so we can't compensate for versions which shouldn't really be returning this in the safe interp + set homedir [file home] + } errM]} { + #tcl 8.6 doesn't have file home.. try again + if {[info exists ::env(HOME)]} { + set homedir $::env(HOME) + } + } + + + # per user xdg vars + # --- + set default_xdg_config_home "" ;#config data - portable + set default_xdg_data_home "" ;#data the user likely to want to be portable + set default_xdg_cache_home "" ;#local cache + set default_xdg_state_home "" ;#persistent user data such as logs, but not as important or as portable as those in xdg_data_home + # --- + set default_xdg_data_dirs "" ;#non-user specific + #xdg_config_dirs ? + #xdg_runtime_dir ? + + + #review. we are assuming if we can't get a home dir - then all the xdg vars including xdg_data_dirs aren't likely to be useful (as presumably filesystem access is absent) + #(safe interp generally won't have access to ::env either) + #This coupling doesn't necessarily hold - its possible the relevant env vars were copied to a safe interp - although that would be a policy that would make disabling 'info home' inconsistent. + if {$homedir ne ""} { + if {"windows" eq $::tcl_platform(platform)} { + #as much as I'd prefer to use ~/.local/share and ~/.config to keep them more consistent with unixlike platforms - the vast majority of apps put them where microsoft wants them. + #we have a choice of LOCALAPPDATA vs APPDATA (local to machine vs potentially roaming/redirected in a corporate environment) + #using the roaming location should not impact users who aren't using a domain controller but is potentially much more convenient for those who do. + if {[info exists ::env(APPDATA)]} { + set default_xdg_config_home $::env(APPDATA) + set default_xdg_data_home $::env(APPDATA) + } + + #The xdg_cache_home should be kept local + if {[info exists ::env(LOCALAPPDATA)]} { + set default_xdg_cache_home $::env(LOCALAPPDATA) + set default_xdg_state_home $::env(LOCALAPPDATA) + } + + if {[info exists ::env(PROGRAMDATA)]} { + #- equiv env(ALLUSERSPROFILE) ? + set default_xdg_data_dirs $::env(PROGRAMDATA) + } + + } else { + #follow defaults as specified on freedesktop.org e.g https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html + set default_xdg_config_home [file join $homedir .config] + set default_xdg_data_home [file join $homedir .local share] + set default_xdg_cache_home [file join $homedir .cache] + set default_xdg_state_home [file join $homedir .local state] + set default_xdg_data_dirs /usr/local/share + } + } + + set defaults [dict create\ + apps $default_apps\ + config ""\ + configset ".punkshell"\ + scriptlib $default_scriptlib\ + color_stdout $default_color_stdout\ + color_stderr $default_color_stderr\ + logfile_stdout $default_logfile_stdout\ + logfile_stderr $default_logfile_stderr\ + logfile_active 0\ + syslog_stdout "127.0.0.1:514"\ + syslog_stderr "127.0.0.1:514"\ + syslog_active 0\ + exec_unknown true\ + xdg_config_home $default_xdg_config_home\ + xdg_data_home $default_xdg_data_home\ + xdg_cache_home $default_xdg_cache_home\ + xdg_state_home $default_xdg_state_home\ + xdg_data_dirs $default_xdg_data_dirs\ + theme_posh_override ""\ + posh_theme ""\ + posh_themes_path ""\ ] - #override with env vars if set - variable evar - foreach evar $known_punk_env_vars { - if {[info exists ::env($evar)]} { - set f [set ::env($evar)] - if {$f ne "default"} { - #e.g PUNK_SCRIPTLIB -> scriptlib - set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] - tcl::dict::set startup $varname $f + set startup $defaults + #load values from saved config file - $xdg_config_home/punk/punk.config ? + #typically we want env vars to override the stored config - as env vars conventionally used on some commandlines. + #that's possibly ok for the PUNK_ vars + #however.. others like the xdg vars and NOCOLOR may apply to other apps.. and we may want to override them from the saved config? + #making some env vars override saved config values and some not would be potentially confusing. may need one/more specific settings or env vars to determine which takes precedence? + #simpler is probably just to let env vars take precedence - and warn when saving or viewing config that the saved values are being overridden + #- requiring user to manually unset any unwanted env vars when launching? + + #we are likely to want the saved configs for subshells/decks to override them however. + + #todo - load/save config file + + #todo - define which configvars are settable in env + set known_punk_env_vars [list \ + PUNK_APPS\ + PUNK_CONFIG\ + PUNK_CONFIGSET\ + PUNK_SCRIPTLIB\ + PUNK_EXECUNKNOWN\ + PUNK_COLOR_STDERR\ + PUNK_COLOR_STDOUT\ + PUNK_LOGFILE_STDOUT\ + PUNK_LOGFILE_STDERR\ + PUNK_LOGFILE_ACTIVE\ + PUNK_SYSLOG_STDOUT\ + PUNK_SYSLOG_STDERR\ + PUNK_SYSLOG_ACTIVE\ + PUNK_THEME_POSH_OVERRIDE\ + ] + + #override with env vars if set + foreach evar $known_punk_env_vars { + if {[info exists ::env($evar)]} { + set f [set ::env($evar)] + if {$f ne "default"} { + #e.g PUNK_SCRIPTLIB -> scriptlib + set varname [tcl::string::tolower [tcl::string::range $evar 5 end]] + tcl::dict::set startup $varname $f + } + } + } + + # https://no-color.org + #if {[info exists ::env(NO_COLOR)]} { + # if {$::env(NO_COLOR) ne ""} { + # set colour_disabled 1 + # } + #} + set known_other_env_vars [list\ + NO_COLOR\ + XDG_CONFIG_HOME\ + XDG_DATA_HOME\ + XDG_CACHE_HOME\ + XDG_STATE_HOME\ + XDG_DATA_DIRS\ + POSH_THEME\ + POSH_THEMES_PATH\ + ] + foreach evar $known_other_env_vars { + if {[info exists ::env($evar)]} { + set f [set ::env($evar)] + if {$f ne "default"} { + set varname [tcl::string::tolower $evar] + tcl::dict::set startup $varname $f + } + } + } + + + #unset -nocomplain vars + + set running [tcl::dict::create] + set running [tcl::dict::merge $running $startup] + } + init + + #todo - consider how to divide up settings, categories, 'devices', decks etc + proc get_running_global {varname} { + variable running + if {[dict exists $running $varname]} { + return [dict get $running $varname] + } + error "No such global configuration item '$varname' found in running config" + } + proc get_startup_global {varname} { + variable startup + if {[dict exists $startup $varname]} { + return [dict get $startup $varname] + } + error "No such global configuration item '$varname' found in startup config" + } + + proc get {whichconfig} { + variable startup + variable running + switch -- $whichconfig { + config - startup - startup-config - startup-configuration { + #show *startup* config - different behaviour may be confusing to those used to router startup and running configs + return $startup + } + running - running-config - running-configuration { + return $running + } + } + } + + proc show {whichconfig} { + #todo - tables for console + variable startup + variable running + switch -- $whichconfig { + config - startup - startup-config - startup-configuration { + #show *startup* config - different behaviour may be confusing to those used to router startup and running configs + return [punk::print_dict $startup] } + running - running-config - running-configuration { + return [punk::print_dict $running] + } + } + + } + + #e.g + # copy running-config startup-config + # copy startup-config test-config.cfg + # copy backup-config.cfg running-config + #review - consider the merge vs overwrite feature of some routers.. where copy to running-config does a merge rather than an overwrite ? + proc copy {fromconfig toconfig} { + error "sorry - unimplemented" + switch -- $toconfig { + } } - unset -nocomplain evar - unset -nocomplain vars - set running [tcl::dict::create] - set running [tcl::dict::merge $running $startup] + + + + +} + + + + + +#todo - move to cli? +::tcl::namespace::eval punk::config { + #todo - something better - 'previous' rather than reverting to startup + proc channelcolors {{onoff {}}} { + variable running + variable startup + + if {![string length $onoff]} { + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } else { + if {![string is boolean $onoff]} { + error "channelcolors: invalid value $onoff - expected boolean: true|false|on|off|1|0|yes|no" + } + if {$onoff} { + dict set running color_stdout [dict get $startup color_stdout] + dict set running color_stderr [dict get $startup color_stderr] + } else { + dict set running color_stdout "" + dict set running color_stderr "" + } + } + return [list stdout [dict get $running color_stdout] stderr [dict get $running color_stderr]] + } } package provide punk::config [tcl::namespace::eval punk::config { diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 7be0509f..29926f41 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -765,6 +765,7 @@ namespace eval punk::console { } ;#end namespace eval internal variable colour_disabled 0 + #todo - move to punk::config # https://no-color.org if {[info exists ::env(NO_COLOR)]} { if {$::env(NO_COLOR) ne ""} { diff --git a/src/modules/punk/du-999999.0a1.0.tm b/src/modules/punk/du-999999.0a1.0.tm index 3470774f..fb958eb6 100644 --- a/src/modules/punk/du-999999.0a1.0.tm +++ b/src/modules/punk/du-999999.0a1.0.tm @@ -901,7 +901,7 @@ namespace eval punk::du { set hlinks [glob -nocomplain -dir $folderpath -types {hidden l} * .*] #set hlinks {} - set links [glob -nocomplain -dir $folderpath -types l * .*] ;#links may have dupes - we don't care. struct::set difference will remove + set links [glob -nocomplain -dir $folderpath -types l * .*] ;#links may have dupes - we don't care. struct::set difference will remove (?) #set links [lsort -unique [concat $hlinks $links[unset links]]] set hfiles [glob -nocomplain -dir $folderpath -types {hidden f} * .*] @@ -913,18 +913,20 @@ namespace eval punk::du { set dirs [glob -nocomplain -dir $folderpath -types d $opt_glob] set hlinks [glob -nocomplain -dir $folderpath -types {hidden l} $opt_glob] - set links [glob -nocomplain -dir $folderpath -types l $opt_glob] ;#links may have dupes - we don't care. struct::set difference will remove + set links [glob -nocomplain -dir $folderpath -types l $opt_glob] ;#links may have dupes - we don't care. struct::set difference will remove (?) set hfiles [glob -nocomplain -dir $folderpath -types {hidden f} $opt_glob] set files [glob -nocomplain -dir $folderpath -types f $opt_glob] } #note struct::set difference produces unordered result - #struct::set difference removes duplicates + #struct::set difference removes duplicates (but not always.. e.g if using tcl impl and 2nd element empty!) + #relying on struct::set to remove dupes is somewhat risky. It is not well documented - and behaviour of dupes in inputs is underspecified as it appears to be intended for mathematical 'sets' #remove links and . .. from directories, remove links from files + #struct::set will affect order: tcl vs critcl give different ordering! set files [struct::set difference [concat $hfiles $files[unset files]] $links] set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]] - set links [lsort -unique [concat $links $hlinks]] + #set links [lsort -unique [concat $links $hlinks]] #---- diff --git a/src/modules/punk/fileline-999999.0a1.0.tm b/src/modules/punk/fileline-999999.0a1.0.tm index 19cfed6c..3c5339e6 100644 --- a/src/modules/punk/fileline-999999.0a1.0.tm +++ b/src/modules/punk/fileline-999999.0a1.0.tm @@ -1555,10 +1555,10 @@ namespace eval punk::fileline::lib { } proc range_boundaries {start end chunksizes args} { - lassign [punk::get_leading_opts_and_values {\ - -offset 0\ - } $args] _opts opts _vals remainingargs - + set argd [punk::args::get_dict { + -offset -default 0 + } $args] + lassign [dict values $argd] opts remainingargs } diff --git a/src/modules/punk/lib-999999.0a1.0.tm b/src/modules/punk/lib-999999.0a1.0.tm index 7194ccc1..5472295f 100644 --- a/src/modules/punk/lib-999999.0a1.0.tm +++ b/src/modules/punk/lib-999999.0a1.0.tm @@ -356,7 +356,62 @@ namespace eval punk::lib { return [expr {[llength $i] == 0}] } - + #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist + #struct::set difference may happen to preserve ordering when items are integers, but order can't be relied on, + # especially as struct::list has 2 differing implementations (tcl vs critcl) which return results with different ordering to each other. + proc ldiff {fromlist removeitems} { + set doomed [list] + foreach item $removeitems { + lappend doomed {*}[lsearch -all -exact $fromlist $item] + } + lremove $fromlist {*}$doomed + } + package require struct::set + if {[struct::set equal [struct::set union {a a} {}] {a}]} { + proc lunique_unordered {list} { + struct::set union $list {} + } + } else { + puts stderr "WARNING: struct::set union no longer dedupes!" + proc lunique_unordered {list} { + tailcall lunique $list + } + } + #order-preserving + proc lunique {list} { + set doomed [list] + #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) + for {set i 0} {$i < [llength $list]} {} { + set item [lindex $list $i] + lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] + while {[incr i] in $doomed} {} + } + lremove $list {*}$doomed + } + proc lunique1 {list} { + set doomed [list] + #expr 'in' probably faster than using a dict - for lists approx < 20,000 items. (wiki wisdom - url?) + set i 0 + foreach item $list { + if {$i in $doomed} { + incr i + continue + } + lappend doomed {*}[lrange [lsearch -all -exact -start $i $list $item] 1 end] + incr i + } + puts --->doomed:$doomed + lremove $list {*}$doomed + } + proc lunique2 {list} { + set new {} + foreach item $list { + if {$item ni $new} { + lappend new $item + } + } + return $new + } #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] @@ -447,8 +502,15 @@ namespace eval punk::lib { return $result } - proc lmapflat {varnames list script} { - concat {*}[uplevel 1 [list lmap $varnames $list $script]] + #proc lmapflat {varnames list script} { + # concat {*}[uplevel 1 [list lmap $varnames $list $script]] + #} + #lmap can accept multiple var list pairs + proc lmapflat {args} { + concat {*}[uplevel 1 [list lmap {*}$args]] + } + proc lmapflat2 {args} { + concat {*}[uplevel 1 lmap {*}$args] } proc dict_getdef {dictValue args} { @@ -1647,6 +1709,12 @@ namespace eval punk::lib { return $result } + proc temperature_f_to_c {deg_fahrenheit} { + return [expr {($deg_fahrenheit -32) * (5/9.0)}] + } + proc temperature_c_to_f {deg_celsius} { + return [expr {($deg_celsius * (9/5.0)) + 32}] + } #*** !doctools #[list_end] [comment {--- end definitions namespace punk::lib ---}] } @@ -1685,6 +1753,51 @@ namespace eval punk::lib::system { return false } } + proc has_safeinterp_compile_bug {{show 0}} { + #ensemble calls within safe interp not compiled + namespace eval [namespace current]::testcompile { + proc ensembletest {} {string index a 0} + } + + set has_bug 0 + + set bytecode_outer [tcl::unsupported::disassemble proc [namespace current]::testcompile::ensembletest] + if {$show} { + puts outer: + puts $bytecode_outer + } + if {![interp issafe]} { + #test of safe subinterp only needed if we aren't already in a safe interp + if {![catch { + interp create x -safe + } errMsg]} { + x eval {proc ensembletest {} {string index a 0}} + set bytecode_safe [x eval {tcl::unsupported::disassemble proc ::ensembletest}] + if {$show} { + puts safe: + puts $bytecode_safe + } + interp delete x + #mainly we expect the safe interp might contain invokeStk - indicating not byte compiled (or we would see strindex instead) + #It's possible the interp we're running in is also not compiling ensembles. + #we could then get a result of 2 - which still indicates a problem + if {[string last "invokeStk" $bytecode_safe] >= 1} { + incr has_bug + } + } else { + #our failure to create a safe interp here doesn't necessarily mean the Tcl version doesn't have the problem - but we could end up returning zero if somehow safe interp can't be created from unsafe interp? + #unlikely - but we should warn + puts stderr "Unable to create a safe sub-interp to test - result only indicates status of current interpreter" + } + } + + namespace delete [namespace current]::testcompile + + if {[string last "invokeStk" $bytecode_outer] >= 1} { + incr has_bug + } + return $has_bug + } proc mostFactorsBelow {n} { ##*** !doctools diff --git a/src/modules/punk/mix-0.2.tm b/src/modules/punk/mix-0.2.tm index f24deb65..24ef156c 100644 --- a/src/modules/punk/mix-0.2.tm +++ b/src/modules/punk/mix-0.2.tm @@ -1,25 +1,31 @@ package require punk::cap -package require punk::cap::handlers::templates ;#handler for templates cap -punk::cap::register_capabilityname punk.templates ::punk::cap::handlers::templates - -package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap -if {[catch {punk::mix::templates::provider register *} errM]} { - puts stderr "punk::mix failure during punk::mix::templates::provider register *" - puts stderr $errM - puts stderr "-----" - puts stderr $::errorInfo + +tcl::namespace::eval punk::mix { + proc init {} { + package require punk::cap::handlers::templates ;#handler for templates cap + punk::cap::register_capabilityname punk.templates ::punk::cap::handlers::templates ;#time taken should generally be sub 200us + + package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap + set t [time { + if {[catch {punk::mix::templates::provider register *} errM]} { + puts stderr "punk::mix failure during punk::mix::templates::provider register *" + puts stderr $errM + puts stderr "-----" + puts stderr $::errorInfo + } + }] + puts stderr "->punk::mix::templates::provider register * t=$t" + } + init + } package require punk::mix::base package require punk::mix::cli -namespace eval punk::mix { - -} - -package provide punk::mix [namespace eval punk::mix { +package provide punk::mix [tcl::namespace::eval punk::mix { variable version set version 0.2 diff --git a/src/modules/punk/mix/base-0.1.tm b/src/modules/punk/mix/base-0.1.tm index 0a13ad3a..a9795574 100644 --- a/src/modules/punk/mix/base-0.1.tm +++ b/src/modules/punk/mix/base-0.1.tm @@ -394,7 +394,7 @@ namespace eval punk::mix::base { proc module_subpath {modulename} { set modulename [string trim $modulename :] set nsq [namespace qualifiers $modulename] - return [string map [list :: /] $nsq] + return [string map {:: /} $nsq] } proc get_build_workdir {path} { diff --git a/src/modules/punk/mix/cli-0.3.tm b/src/modules/punk/mix/cli-0.3.tm index 3e941e43..2d47efe9 100644 --- a/src/modules/punk/mix/cli-0.3.tm +++ b/src/modules/punk/mix/cli-0.3.tm @@ -123,8 +123,9 @@ namespace eval punk::mix::cli { } #review - why can't we be anywhere in the project? + #also - if no make.tcl - can we use the running shell's make.tcl ? (after prompting user?) if {([file tail $sourcefolder] ne "src") || (![file exists $sourcefolder/make.tcl])} { - puts stderr "deck make must be run from src folder containing make.tcl - unable to proceed (cwd: [pwd])" + puts stderr "dev make must be run from src folder containing make.tcl - unable to proceed (cwd: [pwd])" if {[string length $project_base]} { if {[file exists $project_base/src] && [string tolower [pwd]] ne [string tolower $project_base/src]} { puts stderr "Try cd to $project_base/src" @@ -224,7 +225,7 @@ namespace eval punk::mix::cli { # -- --- --- --- --- --- --- --- --- --- --- --- --- --- validate_name_not_empty_or_spaced $modulename -errorprefix $opt_errorprefix - set testname [string map [list :: ""] $modulename] + set testname [string map {:: {}} $modulename] if {[string first : $testname] >=0} { error "$opt_errorprefix '$modulename' can only contain paired colons" } @@ -372,7 +373,7 @@ namespace eval punk::mix::cli { } set timeline [exec fossil timeline -n 5 -t ci] - set timeline [string map [list \r\n \n] $timeline] + set timeline [string map {\r\n \n} $timeline] append result $timeline if {$opt_v} { set repostate [punk::repo::workingdir_state $repopath -repopaths $repopaths -repotypes fossil] diff --git a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm index 0a6150ff..3788dc16 100644 --- a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm @@ -251,7 +251,7 @@ namespace eval punk::mix::commandset::loadedlib { } set loadinfo [package ifneeded $libfound $ver] - set loadinfo [string map [list \r\n \n] $loadinfo] + set loadinfo [string map {\r\n \n} $loadinfo] set loadinfo_lines [split $loadinfo \n] if {[catch {llength $loadinfo}]} { set loadinfo_is_listshaped 0 @@ -316,7 +316,7 @@ namespace eval punk::mix::commandset::loadedlib { #we only follow one level of package require redirection - seems unlikely/imprudent to follow arbitrarily in a while loop(?) set libfound $lib_diversion_name set loadinfo [package ifneeded $libfound $ver] - set loadinfo [string map [list \r\n \n] $loadinfo] + set loadinfo [string map {\r\n \n} $loadinfo] set loadinfo_lines [split $loadinfo \n] if {[catch {llength $loadinfo}]} { set loadinfo_is_listshaped 0 diff --git a/src/modules/punk/mix/templates/layouts/project/src/make.tcl b/src/modules/punk/mix/templates/layouts/project/src/make.tcl index f4eef65f..c53315e9 100644 --- a/src/modules/punk/mix/templates/layouts/project/src/make.tcl +++ b/src/modules/punk/mix/templates/layouts/project/src/make.tcl @@ -281,7 +281,7 @@ if {$::punkmake::command eq "bootsupport"} { foreach {relpath module} $bootsupport_modules { set module [string trim $module :] - set module_subpath [string map [list :: /] [namespace qualifiers $module]] + set module_subpath [string map {:: /} [namespace qualifiers $module]] set srclocation [file join $projectroot $relpath $module_subpath] #puts stdout "$relpath $module $module_subpath $srclocation" set pkgmatches [glob -nocomplain -dir $srclocation -tail [namespace tail $module]-*] @@ -617,7 +617,7 @@ if {[file exists $mapfile]} { fconfigure $fdmap -translation binary set mapdata [read $fdmap] close $fdmap - set mapdata [string map [list \r\n \n] $mapdata] + set mapdata [string map {\r\n \n} $mapdata] set missing [list] foreach ln [split $mapdata \n] { set ln [string trim $ln] diff --git a/src/modules/punk/mix/templates/modules/template_anyname-0.0.2.tm b/src/modules/punk/mix/templates/modules/template_anyname-0.0.2.tm index 4bca651b..0a070c15 100644 --- a/src/modules/punk/mix/templates/modules/template_anyname-0.0.2.tm +++ b/src/modules/punk/mix/templates/modules/template_anyname-0.0.2.tm @@ -24,7 +24,7 @@ apply {code { #auto determine package name and version from name and placement o } set ver [join [lassign [split [file rootname [file tail [info script] ]] -] pkgtail] -] set pkgns ${nsprefix}${pkgtail} - namespace eval $pkgns [string map [list $pkgns $ver] $code] + tcl::namespace::eval $pkgns [string map [list $pkgns $ver] $code] package provide $pkgns $ver;# only provide package if code evaluated without error } ::} { #-------------------------------------- @@ -40,7 +40,7 @@ apply {code { #auto determine package name and version from name and placement o - namespace eval [namespace current]::lib { + tcl::namespace::eval [tcl::namespace::current]::lib { #proc test {args} {puts "[namespace current]::test got args: $args"} } diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index 42f3f0a5..ef4a0eb0 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -20,12 +20,12 @@ package require punk::lib package require punk::args -namespace eval ::punk_dynamic::ns { +tcl::namespace::eval ::punk_dynamic::ns { } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval punk::ns { +tcl::namespace::eval punk::ns { variable ns_current "::" variable ns_re_cache [dict create] ;#cache regular expressions used in globmatchns namespace export nsjoin nsprefix nstail nsparts nseval nsimport_noclobber corp @@ -58,7 +58,7 @@ namespace eval punk::ns { set has_globchars [regexp {[*?]} $ns_or_glob] if {$is_absolute} { if {!$has_globchars} { - if {![namespace exists $ns_or_glob]} { + if {![tcl::namespace::exists $ns_or_glob]} { error "cannot change to namespace $ns_or_glob" } set ns_current $ns_or_glob @@ -71,7 +71,7 @@ namespace eval punk::ns { } else { if {!$has_globchars} { set nsnext [nsjoin $ns_current $ns_or_glob] - if {![namespace exists $nsnext]} { + if {![tcl::namespace::exists $nsnext]} { error "cannot change to namespace $ns_or_glob" } set ns_current $nsnext @@ -86,7 +86,7 @@ namespace eval punk::ns { set ns_display "\n$ns_queried" if {$ns_current eq $ns_queried} { if {$ns_current in [info commands $ns_current] } { - if {![catch [list namespace ensemble configure $ns_current] ensemble_info]} { + if {![catch [list tcl::namespace::ensemble configure $ns_current] ensemble_info]} { if {[llength $ensemble_info] > 0} { #this namespace happens to match ensemble command. #todo - keep cache of encountered ensembles from commands.. and examine namespace in the configure info. @@ -119,13 +119,13 @@ namespace eval punk::ns { set nspath [nsjoinall $ns_current {*}$args] } - set ns_exists [nseval [nsprefix $nspath] [list ::namespace exists [nstail $nspath] ]] + set ns_exists [nseval [nsprefix $nspath] [list ::tcl::namespace::exists [nstail $nspath] ]] if {$ns_exists} { error "Namespace $nspath already exists" } - #namespace eval [nsprefix $nspath] [list namespace eval [nstail $nspath] {}] - nseval [nsprefix $nspath] [list ::namespace eval [nstail $nspath] {}] + #tcl::namespace::eval [nsprefix $nspath] [list tcl::namespace::eval [nstail $nspath] {}] + nseval [nsprefix $nspath] [list ::tcl::namespace::eval [nstail $nspath] {}] n/ $nspath } @@ -157,7 +157,7 @@ namespace eval punk::ns { } #recursive nseval - for introspection of weird namespace trees - #approx 10x slower than normal namespace eval - but still only a few microseconds.. fine for repl introspection + #approx 10x slower than normal tcl::namespace::eval - but still only a few microseconds.. fine for repl introspection proc nseval_script {location} { set parts [nsparts $location] if {[lindex $parts 0] eq ""} { @@ -171,7 +171,7 @@ namespace eval punk::ns { set i 0 set tails [lrepeat [llength $parts] ""] foreach ns $parts { - set cmdlist [list ::namespace eval $ns] + set cmdlist [list ::tcl::namespace::eval $ns] set t "" if {$i > 0} { append body " " @@ -194,7 +194,7 @@ namespace eval punk::ns { set scr {[::list ::eval [::uplevel {::set script}]]} set up [expr {$i - 1}] - set scr [string map [list $up] $scr] + set scr [string map " $up" $scr] set body [string map [list