You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5370 lines
303 KiB
5370 lines
303 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
|
# |
|
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
|
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# (C) Julian Noble 2003-2023 |
|
# |
|
# @@ Meta Begin |
|
# Application overtype 999999.0a1.0 |
|
# Meta platform tcl |
|
# Meta license BSD |
|
# @@ Meta End |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# doctools header |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[manpage_begin overtype_module_overtype 0 999999.0a1.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] |
|
|
|
#PERFORMANCE notes |
|
#overtype is very performance sensitive - used in ansi output all over the place ie needs to be optimised |
|
#NOTE use of tcl::dict::for tcl::string::range etc instead of ensemble versions. This is for the many tcl 8.6/8.7 interps which don't compile ensemble commands when in safe interps |
|
#similar for tcl::namespace::eval - but this is at least on some versions of Tcl - faster even in a normal interp. Review to see if that holds for Tcl 9. |
|
#for string map: when there are exactly 2 elements - it is faster to use a literal which has a special case optimisation in the c code |
|
#ie use tcl::string::map {\n ""} ... instead of tcl::string::map [list \n ""] ... |
|
#note that we can use unicode (e.g \uFF31) and other escapes such as \t within these curly braces - we don't have to use double quotes |
|
#generally using 'list' is preferred for the map as less error prone. |
|
#can also use: tcl::string::map "token $var" .. but be careful regarding quoting and whitespace in var. This should be used sparingly if at all. |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
#*** !doctools |
|
#[section API] |
|
|
|
|
|
#Julian Noble <julian@precisium.com.au> - 2003 |
|
#Released under standard 'BSD license' conditions. |
|
# |
|
#todo - ellipsis truncation indicator for center,right |
|
|
|
#v1.4 2023-07 - naive ansi color handling - todo - fix tcl::string::range |
|
# - need to extract and replace ansi codes? |
|
|
|
tcl::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 "..." |
|
tcl::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 "ANSI capable text formatting. Author JMN. BSD-License" |
|
} |
|
|
|
tcl::namespace::eval overtype { |
|
variable grapheme_widths [tcl::dict::create] |
|
|
|
variable escape_terminals |
|
#single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). |
|
tcl::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 "\{" "\}"] |
|
#tcl::dict::set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic |
|
tcl::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 [tcl::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::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::ansistrip. Alternatively try punk::ansi::printing_length" |
|
set text [punk::ansi::ansistrip $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) |
|
tcl::namespace::eval overtype::priv { |
|
} |
|
|
|
#could return larger than renderwidth |
|
proc _get_row_append_column {row} { |
|
#obsolete? |
|
upvar outputlines outputlines |
|
set idx [expr {$row -1}] |
|
if {$row <= 1 || $row > [llength $outputlines]} { |
|
return 1 |
|
} else { |
|
upvar opt_expand_right expand_right |
|
upvar renderwidth renderwidth |
|
set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]] |
|
set endpos [expr {$existinglen +1}] |
|
if {$expand_right} { |
|
return $endpos |
|
} else { |
|
if {$endpos > $renderwidth} { |
|
return [expr {$renderwidth + 1}] |
|
} else { |
|
return $endpos |
|
} |
|
} |
|
} |
|
} |
|
|
|
tcl::namespace::eval overtype { |
|
#*** !doctools |
|
#[subsection {Namespace overtype}] |
|
#[para] Core API functions for overtype |
|
#[list_begin definitions] |
|
|
|
namespace eval argdoc { |
|
variable PUNKARGS |
|
#non-colour SGR codes |
|
set I "\x1b\[3m" ;# [a+ italic] |
|
set NI "\x1b\[23m" ;# [a+ noitalic] |
|
set B "\x1b\[1m" ;# [a+ bold] |
|
set N "\x1b\[22m" ;# [a+ normal] |
|
set T "\x1b\[1\;4m" ;# [a+ bold underline] |
|
set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] |
|
interp alias "" ::overtype::example "" ::punk::args::helpers::example |
|
} |
|
|
|
namespace eval argdoc { |
|
variable PUNKARGS |
|
lappend PUNKARGS [list { |
|
@id -id ::overtype::renderspace |
|
@cmd -name overtype::renderspace\ |
|
-summary\ |
|
{}\ |
|
-help\ |
|
{} |
|
@opts |
|
#because underblocks value is optional - restrict opts to flag pairs (no solos) |
|
#We don't use punk::args::parse in the actual function to parse args - so keep it simpler. |
|
-bias -default left -type string -choices {left right} -help ignored |
|
-width -default \uFFEF -type integer |
|
-height -default \uFFEF -type integer |
|
-startcolumn -default 1 -type integer |
|
-startrow -default 1 -type integer |
|
-ellipsis -default 0 -type boolean |
|
-ellipsistext -default ${$::overtype::default_ellipsis_horizontal} -type char |
|
-ellipsiswhitespace -default 0 -type boolean |
|
-expand_right -default 0 -type boolean |
|
-appendlines -default 1 -type boolean |
|
-transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ |
|
"0 to disable transparency processing |
|
1 to enable space characters in the |
|
overlay to be transparent, or a regex |
|
to match the character(s) required to be |
|
transparent in the overlay." |
|
-exposed1 -default \uFFFD -type char -help\ |
|
{A character of single terminal column width to use |
|
as replacement when first-half of an underlying char |
|
is exposed due to overlay positioning/transparency |
|
which obscures the second-half of the char. May be ANSI |
|
coloured as this doesn't affect the display width. |
|
Default is \uFFFD - the unicode replacement char.} |
|
-exposed2 -default \uFFFD -type char -help\ |
|
{A character of single terminal column width to use |
|
as replacement when second-half of an underlying char |
|
is exposed due to overlay positioning/transparency |
|
which obscures the first-half of the char. May be ANSI |
|
coloured as this doesn't affect the display width. |
|
Default is \uFFFD - the unicode replacement char.} |
|
|
|
-experimental -default 0 |
|
-cp437 -default 0 -type boolean |
|
-looplimit -default \uFFEF\ -type integer -help\ |
|
"internal failsafe - experimental" |
|
-crm_mode -default 0 -type boolean |
|
-reverse_mode -default 0 -type boolean |
|
-insert_mode -default 1 -type boolean |
|
-wrap -default 0 -type boolean |
|
-info -default 0 -type boolean -help\ |
|
"When set to 1, return a dictionary (experimental)" |
|
-binarytext -default "" -type string -choices {"" bios ice} |
|
-console -default {stdin stdout stderr} -type list |
|
|
|
@values -min 1 -max 2 |
|
underblock -type string -optional 1 |
|
overblock -type string -optional 0 |
|
}] |
|
} |
|
|
|
|
|
#tcl::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 renderspace {args} { |
|
#*** !doctools |
|
#[call [fun overtype::renderspace] [arg args] ] |
|
#[para] usage: ?-transparent [lb]0|1[rb]? ?-expand_right [lb]1|0[rb]? ?-ellipsis [lb]1|0[rb]? ?-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] < 1} { |
|
error {usage: ?-width <int>? ?-startcolumn <int>? ?-transparent [0|1|<char>]? ?-expand_right [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} |
|
} |
|
set optargs [lrange $args 0 end-2] |
|
if {[llength $optargs] % 2 == 0} { |
|
set overblock [lindex $args end] |
|
set underblock [lindex $args end-1] |
|
#lassign [lrange $args end-1 end] underblock overblock |
|
set argsflags [lrange $args 0 end-2] |
|
} else { |
|
#no solo flags - so we assume only an overblock was supplied |
|
set overblock [lindex $args end] |
|
set underblock "" |
|
set argsflags [lrange $args 0 end-1] |
|
|
|
#set optargs [lrange $args 0 end-1] |
|
#if {[llength $optargs] %2 == 0} { |
|
# set overblock [lindex $args end] |
|
# set underblock "" |
|
# set argsflags [lrange $args 0 end-1] |
|
#} else { |
|
# error "renderspace expects opt-val pairs followed by: <underblock> <overblock> or just <overblock>" |
|
#} |
|
} |
|
set opts [tcl::dict::create\ |
|
-bias ignored\ |
|
-width \uFFEF\ |
|
-height \uFFEF\ |
|
-startcolumn 1\ |
|
-startrow 1\ |
|
-ellipsis 0\ |
|
-ellipsistext $default_ellipsis_horizontal\ |
|
-ellipsiswhitespace 0\ |
|
-expand_right 0\ |
|
-appendlines 1\ |
|
-transparent 0\ |
|
-exposed1 \uFFFD\ |
|
-exposed2 \uFFFD\ |
|
-experimental 0\ |
|
-cp437 0\ |
|
-looplimit \uFFEF\ |
|
-crm_mode 0\ |
|
-reverse_mode 0\ |
|
-insert_mode 0\ |
|
-wrap 0\ |
|
-info 0\ |
|
-binarytext ""\ |
|
-console {stdin stdout stderr}\ |
|
] |
|
#expand_right is perhaps consistent with the idea of the page_size being allowed to grow horizontally.. |
|
# it does not necessarily mean the viewport grows. (which further implies need for horizontal scrolling) |
|
# - it does need to be within some concept of terminal width - as columns must be addressable by ansi sequences. |
|
# - This implies the -width option value must grow if it is tied to the concept of renderspace terminal width! REVIEW. |
|
# - further implication is that if expand_right grows the virtual renderspace terminal width - |
|
# then some sort of reflow/rerender needs to be done for preceeding lines? |
|
# possibly not - as expand_right is distinct from a normal terminal-width change event, |
|
# expand_right being primarily to support other operations such as textblock::table |
|
|
|
#todo - viewport width/height as separate concept to terminal width/height? |
|
#-ellipsis args not used if -wrap is true |
|
foreach {k v} $argsflags { |
|
switch -- $k { |
|
-looplimit - -width - -height - -startcolumn - -startrow - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace |
|
- -transparent - -exposed1 - -exposed2 - -experimental |
|
- -expand_right - -appendlines |
|
- -reverse_mode - -crm_mode - -insert_mode |
|
- -cp437 |
|
- -info - -binarytext - -console { |
|
tcl::dict::set opts $k $v |
|
} |
|
-wrap - -autowrap_mode { |
|
#temp alias -autowrap_mode for consistency with renderline |
|
#todo - |
|
tcl::dict::set opts -wrap $v |
|
} |
|
default { |
|
error "overtype::renderspace unknown option '$k'. Known options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
#set opts [tcl::dict::merge $defaults $argsflags] |
|
# -- --- --- --- --- --- |
|
#review - expand_left for RTL text? |
|
set opt_expand_right [tcl::dict::get $opts -expand_right] |
|
#for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. |
|
set opt_width [tcl::dict::get $opts -width] |
|
set opt_height [tcl::dict::get $opts -height] |
|
set opt_startcolumn [tcl::dict::get $opts -startcolumn] |
|
set opt_startrow [tcl::dict::get $opts -startrow] |
|
set opt_appendlines [tcl::dict::get $opts -appendlines] |
|
set opt_transparent [tcl::dict::get $opts -transparent] |
|
set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] |
|
set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] |
|
set opt_exposed1 [tcl::dict::get $opts -exposed1] ;#widechar_exposed_left - todo |
|
set opt_exposed2 [tcl::dict::get $opts -exposed2] ;#widechar_exposed_right - todo |
|
# -- --- --- --- --- --- |
|
set opt_crm_mode [tcl::dict::get $opts -crm_mode] |
|
set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] |
|
set opt_insert_mode [tcl::dict::get $opts -insert_mode] |
|
##### |
|
# review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. |
|
set opt_autowrap_mode [tcl::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) |
|
##### |
|
# -- --- --- --- --- --- |
|
set opt_cp437 [tcl::dict::get $opts -cp437] |
|
set opt_info [tcl::dict::get $opts -info] |
|
set opt_binarytext [tcl::dict::get $opts -binarytext] |
|
|
|
|
|
|
|
# ---------------------------- |
|
# -experimental dev flag to set flags etc |
|
# ---------------------------- |
|
set data_mode 0 |
|
set edit_mode 0 |
|
set opt_experimental [tcl::dict::get $opts -experimental] |
|
foreach o $opt_experimental { |
|
switch -- $o { |
|
data_mode { |
|
set data_mode 1 |
|
} |
|
edit_mode { |
|
set edit_mode 1 |
|
} |
|
} |
|
} |
|
# ---------------------------- |
|
|
|
set underblock [tcl::string::map {\r\n \n} $underblock] |
|
set overblock [tcl::string::map {\r\n \n} $overblock] |
|
if {$opt_startrow > 1} { |
|
set down [expr {$opt_startrow -1}] |
|
set overblock [punk::ansi::move_down $down]$overblock |
|
} |
|
|
|
|
|
#set underlines [split $underblock \n] |
|
|
|
#underblock is a 'rendered' block - so width height make sense |
|
#only non-cursor affecting and non-width occupying ANSI codes should be present. |
|
#ie SGR codes and perhaps things such as PM - although generally those should have been pushed to the application already |
|
#renderwidth & renderheight were originally used with reference to rendering into a 'column' of output e.g a table column - before cursor row/col was implemented. |
|
|
|
if {$opt_width eq "\uFFEF" || $opt_height eq "\uFFEF"} { |
|
lassign [blocksize $underblock] _w renderwidth _h renderheight |
|
if {$opt_width ne "\uFFEF"} { |
|
set renderwidth $opt_width |
|
} |
|
if {$opt_height ne "\uFFEF"} { |
|
set renderheight $opt_height |
|
} |
|
} else { |
|
set renderwidth $opt_width |
|
set renderheight $opt_height |
|
} |
|
#initial state for renderspace 'terminal' reset |
|
set initial_state [dict create\ |
|
renderwidth $renderwidth\ |
|
renderheight $renderheight\ |
|
crm_mode $opt_crm_mode\ |
|
reverse_mode $opt_reverse_mode\ |
|
insert_mode $opt_insert_mode\ |
|
autowrap_mode $opt_autowrap_mode\ |
|
cp437 $opt_cp437\ |
|
] |
|
#modes |
|
#e.g insert_mode can be toggled by insert key or ansi IRM sequence CSI 4 h|l |
|
#opt_startcolumn ?? - DECSLRM ? |
|
set vtstate $initial_state |
|
|
|
# -- --- --- --- |
|
#REVIEW - do we need ansi resets in the underblock? |
|
if {$underblock eq ""} { |
|
set underlines [lrepeat $renderheight ""] |
|
} else { |
|
set underblock [textblock::join_basic -- $underblock] ;#ensure properly rendered - ansi per-line resets & replays |
|
set underlines [split $underblock \n] |
|
} |
|
#if {$underblock eq ""} { |
|
# set blank "\x1b\[0m\x1b\[0m" |
|
# #set underlines [list "\x1b\[0m\x1b\[0m"] |
|
# set underlines [lrepeat $renderheight $blank] |
|
#} else { |
|
# #lines_as_list -ansiresets 1 will do nothing if -ansiresets 1 isn't specified - REVIEW |
|
# 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 renderwidth |
|
#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 |
|
|
|
#a hack until we work out how to avoid infinite loops... |
|
# |
|
set looplimit [tcl::dict::get $opts -looplimit] |
|
if {$looplimit eq "\uFFEF"} { |
|
#looping for each char is worst case (all newlines?) - anything over that is an indication of something broken? |
|
#do we need any margin above the length? (telnet mapscii.me test) |
|
set looplimit [expr {[tcl::string::length $overblock] + 10}] |
|
} |
|
|
|
#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 scheme 4 |
|
switch -- $scheme { |
|
0 { |
|
#one big chunk |
|
set inputchunks [list mixed $overblock] |
|
} |
|
1 { |
|
#todo |
|
set inputchunks [punk::ansi::ta::split_codes $overblock] |
|
} |
|
2 { |
|
#todo |
|
#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 [tcl::string::cat $lastpt [lindex $sequence_split 0]] |
|
lappend inputchunks {*}[lrange $sequence_split 1 end] |
|
incr i |
|
} |
|
} |
|
3 { |
|
#todo |
|
#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 [tcl::string::range [lindex $lflines end] 0 end-1] |
|
} |
|
#set inputchunks $lflines[unset lflines] |
|
set inputchunks [lindex [list $lflines [unset lflines]] 0] |
|
|
|
} |
|
4 { |
|
set inputchunks [list] |
|
switch -- $opt_binarytext { |
|
"" { |
|
foreach ln [split $overblock \n] { |
|
lappend inputchunks [list mixed $ln\n] |
|
} |
|
if {[llength $inputchunks]} { |
|
lset inputchunks end 1 [tcl::string::range [lindex $inputchunks end 1] 0 end-1] |
|
} |
|
} |
|
bios { |
|
#16 fg, 8 fg + possible blink |
|
set input "" |
|
set ansisplit [list ""] |
|
set charpair 0 |
|
foreach {ch at} [split $overblock ""] { |
|
#review - does binarytext only apply to cp437??? we need to know the original encoding |
|
set at [encoding convertto cp437 $at] |
|
if {[catch {punk::ansi::colour::byteAnsi $at} code]} { |
|
puts stderr "renderspace err at charpair: $charpair [punk::ansi::ansistring VIEW ${ch}${at}]" |
|
#append input [punk::ansi::a+ brightred White] \uFFef |
|
set code [punk::ansi::a+ brightred White] |
|
set ch \uFFeF |
|
} |
|
append input $code $ch |
|
lappend ansisplit $code $ch |
|
incr charpair |
|
} |
|
#lappend inputchunks [list mixed $input] |
|
lappend inputchunks [list ansisplit $ansisplit] |
|
} |
|
ice { |
|
#16 fg, 16 bg (no blink) |
|
set input "" |
|
foreach {ch at} [split $overblock ""] { |
|
set at [encoding convertto cp437 $at] |
|
append input [punk::ansi::colour::byteAnsiIce $at]$ch |
|
} |
|
lappend inputchunks [list mixed $input] |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
set replay_codes_underlay [tcl::dict::create 1 ""] |
|
#lappend replay_codes_overlay "" |
|
set replay_codes_overlay "[punk::ansi::a]" |
|
set unapplied "" |
|
set cursor_saved_position [tcl::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 $opt_startcolumn |
|
#} |
|
|
|
set instruction_stats [tcl::dict::create] |
|
|
|
set loop 0 |
|
#while {$overidx < [llength $inputchunks]} { } |
|
set renderedrow "" |
|
while {[llength $inputchunks]} { |
|
#set overtext [lpop inputchunks 0] ;#could be a list 'ansisplit' or text 'plain|mixed' |
|
lassign [lpop inputchunks 0] overtext_type overtext |
|
|
|
#use eq test with emptystring instead of 'string length' - test for emptiness shouldn't cause shimmering if popped inputchunks member if an 'ansisplit' list |
|
if {$overtext eq ""} { |
|
incr loop |
|
continue |
|
} |
|
#puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----" |
|
set undertext [lindex $outputlines [expr {$row -1}]] |
|
|
|
#renderline pads each underaly line to width with spaces and should track where end of data is |
|
|
|
switch -- $overtext_type { |
|
mixed { |
|
set overtext $replay_codes_overlay$overtext |
|
} |
|
ansisplit { |
|
ledit overtext -1 -1 "" $replay_codes_overlay |
|
} |
|
default { |
|
error "renderspace unsupported overtext type: $overtext_type overtext: $overtext" |
|
} |
|
} |
|
|
|
|
|
###################### |
|
#debug |
|
#set partinfo "" |
|
#if {$overtext_type eq "ansisplit"} { |
|
# set partinfo [llength $overtext] |
|
#} else { |
|
# set partinfo [string length $overtext] |
|
#} |
|
#if {$renderedrow eq $row} { |
|
# puts -nonewline stderr <$row>$overtext_type$partinfo |
|
#} else { |
|
# puts -nonewline stderr \n<$row>$overtext_type$partinfo |
|
#} |
|
#if {$overtext_type eq "mixed"} { |
|
# puts -nonewline stderr "\n[ansistring VIEW $overtext]\n" |
|
#} |
|
###################### |
|
|
|
set renderedrow $row |
|
|
|
if {[tcl::dict::exists $replay_codes_underlay $row]} { |
|
set undertext [tcl::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 renderopts [list -experimental $opt_experimental\ |
|
-cp437 $opt_cp437\ |
|
-info 1\ |
|
-crm_mode [tcl::dict::get $vtstate crm_mode]\ |
|
-insert_mode [tcl::dict::get $vtstate insert_mode]\ |
|
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\ |
|
-reverse_mode [tcl::dict::get $vtstate reverse_mode]\ |
|
-cursor_restore_attributes $cursor_saved_attributes\ |
|
-transparent $opt_transparent\ |
|
-width [tcl::dict::get $vtstate renderwidth]\ |
|
-exposed1 $opt_exposed1\ |
|
-exposed2 $opt_exposed2\ |
|
-expand_right $opt_expand_right\ |
|
-cursor_column $col\ |
|
-cursor_row $row\ |
|
-overtext_type $overtext_type\ |
|
] |
|
set rinfo [renderline {*}$renderopts $undertext $overtext] |
|
|
|
set instruction [tcl::dict::get $rinfo instruction] |
|
tcl::dict::set vtstate crm_mode [tcl::dict::get $rinfo crm_mode] |
|
tcl::dict::set vtstate insert_mode [tcl::dict::get $rinfo insert_mode] |
|
tcl::dict::set vtstate autowrap_mode [tcl::dict::get $rinfo autowrap_mode] ;# |
|
tcl::dict::set vtstate reverse_mode [tcl::dict::get $rinfo reverse_mode] |
|
#how to support reverse_mode in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? |
|
# - review - the answer is probably that we don't need to - it is set/reset only during application of overtext |
|
|
|
#Note carefully the difference betw overflow_right and unapplied. |
|
#overflow_right may need to be included in next run before the unapplied data |
|
#overflow_right most commonly has data when in insert_mode |
|
set rendered [tcl::dict::get $rinfo result] |
|
set overflow_right [tcl::dict::get $rinfo overflow_right] |
|
set overflow_right_column [tcl::dict::get $rinfo overflow_right_column] |
|
set unapplied [tcl::dict::get $rinfo unapplied] |
|
set unapplied_list [tcl::dict::get $rinfo unapplied_list] |
|
set unapplied_ansisplit [tcl::dict::get $rinfo unapplied_ansisplit] |
|
set post_render_col [tcl::dict::get $rinfo cursor_column] |
|
set post_render_row [tcl::dict::get $rinfo cursor_row] |
|
set c_saved_pos [tcl::dict::get $rinfo cursor_saved_position] |
|
set c_saved_attributes [tcl::dict::get $rinfo cursor_saved_attributes] |
|
set visualwidth [tcl::dict::get $rinfo visualwidth] ;#column width of what is 'rendered' for the line |
|
set insert_lines_above [tcl::dict::get $rinfo insert_lines_above] |
|
set insert_lines_below [tcl::dict::get $rinfo insert_lines_below] |
|
tcl::dict::set replay_codes_underlay [expr {$renderedrow+1}] [tcl::dict::get $rinfo replay_codes_underlay] |
|
|
|
#lset replay_codes_overlay [expr $overidx+1] [tcl::dict::get $rinfo replay_codes_overlay] |
|
set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] |
|
if {0 && [tcl::dict::get $vtstate reverse_mode]} { |
|
#test branch - todo - prune |
|
puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered" |
|
#review |
|
#JMN3 |
|
set existing_reverse_state 0 |
|
#split_codes_single is single esc sequence - but could have multiple sgr codes within one esc sequence |
|
#e.g \x1b\[0;31;7m has a reset,colour red and reverse |
|
set codeinfo [punk::ansi::codetype::sgr_merge [list $replay_codes_overlay] -info 1] |
|
set codestate_reverse [dict get $codeinfo codestate reverse] |
|
switch -- $codestate_reverse { |
|
7 { |
|
set existing_reverse_state 1 |
|
} |
|
27 { |
|
set existing_reverse_state 0 |
|
} |
|
"" { |
|
} |
|
} |
|
if {$existing_reverse_state == 0} { |
|
set rflip [a+ reverse] |
|
} else { |
|
#reverse of reverse |
|
set rflip [a+ noreverse] |
|
} |
|
#note that mergeresult can have multiple esc (due to unmergeables or non sgr codes) |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]] |
|
puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered" |
|
} |
|
|
|
|
|
|
|
#-- todo - detect looping properly |
|
if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} { |
|
puts stderr "overtype::renderspace loop?" |
|
puts [ansistring VIEW $rinfo] |
|
break |
|
} |
|
#-- |
|
|
|
if {[tcl::dict::size $c_saved_pos] >= 1} { |
|
set cursor_saved_position $c_saved_pos |
|
set cursor_saved_attributes $c_saved_attributes |
|
} |
|
|
|
|
|
set overflow_handled 0 |
|
|
|
|
|
|
|
set nextprefix_list [list] |
|
|
|
|
|
#todo - handle potential insertion mode as above for cursor restore? |
|
#keeping separate branches for debugging - review and merge as appropriate when stable |
|
set instruction_type [lindex $instruction 0] ;#some instructions have params |
|
tcl::dict::incr instruction_stats $instruction_type |
|
switch -- $instruction_type { |
|
reset { |
|
#reset the 'renderspace terminal' (not underlying terminal) |
|
set row 1 |
|
set col 1 |
|
set vtstate [tcl::dict::merge $vtstate $initial_state] |
|
#todo - clear screen |
|
} |
|
{} { |
|
#end of supplied line input |
|
#lf included in data |
|
set row $post_render_row |
|
set col $post_render_col |
|
if {![llength $unapplied_list]} { |
|
if {$overflow_right ne ""} { |
|
incr row |
|
} |
|
} else { |
|
puts stderr "renderspace end of input line - has unapplied: [ansistring VIEW $unapplied] (review)" |
|
} |
|
set col $opt_startcolumn |
|
} |
|
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 < $renderwidth} { |
|
set col [expr {$lastdatacol+1}] |
|
} else { |
|
set col $renderwidth |
|
} |
|
} |
|
|
|
#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 "" |
|
} |
|
} |
|
# ---- |
|
# review |
|
set col $post_render_col |
|
#just because it's out of range of the renderwidth - doesn't mean a move down should jump to witin the range - 2025 |
|
#---- |
|
|
|
#set existingdata [lindex $outputlines [expr {$post_render_row -1}]] |
|
#set lastdatacol [punk::ansi::printing_length $existingdata] |
|
|
|
#set col [expr {$lastdatacol+1}] |
|
|
|
#if {$lastdatacol < $renderwidth} { |
|
# set col [expr {$lastdatacol+1}] |
|
#} else { |
|
# set col $renderwidth |
|
#} |
|
|
|
} |
|
} |
|
restore_cursor { |
|
#testfile belinda.ans uses this |
|
|
|
#puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" |
|
if {[tcl::dict::exists $cursor_saved_position row]} { |
|
set row [tcl::dict::get $cursor_saved_position row] |
|
set col [tcl::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 [tcl::dict::get $rinfo replay_codes_overlay]$cursor_saved_attributes |
|
#set replay_codes_overlay $cursor_saved_attributes |
|
set cursor_saved_position [tcl::dict::create] |
|
set cursor_saved_attributes "" |
|
} else { |
|
#TODO |
|
#?restore without save? |
|
#should move to home position and reset ansi SGR? |
|
#puts stderr "overtype::renderspace 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 [tcl::dict::get $vtstate renderwidth]\ |
|
-insert_mode [tcl::dict::get $vtstate insert_mode]\ |
|
-autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\ |
|
-expand_right [tcl::dict::get $opts -expand_right]\ |
|
""\ |
|
$overflow_right\ |
|
] |
|
set foldline [tcl::dict::get $sub_info result] |
|
tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..? |
|
tcl::dict::set vtstate autowrap_mode [tcl::dict::get $sub_info autowrap_mode] ;#nor this.. |
|
|
|
#todo!!! |
|
# 2025 fix - this does nothing - so what uses it?? create a test! |
|
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? |
|
} |
|
clear_and_move { |
|
#e.g 2J |
|
if {$post_render_row > [llength $outputlines]} { |
|
set row [llength $outputlines] |
|
} else { |
|
set row $post_render_row |
|
} |
|
set col $post_render_col |
|
set overflow_right "" ;#if we're clearing - any overflow due to insert_mode is irrelevant |
|
set clearedlines [list] |
|
foreach ln $outputlines { |
|
lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $renderwidth]\x1b\[0m |
|
if 0 { |
|
|
|
set lineparts [punk::ansi::ta::split_codes $ln] |
|
set numcells 0 |
|
foreach {pt _code} $lineparts { |
|
if {$pt ne ""} { |
|
foreach grapheme [punk::char::grapheme_split $pt] { |
|
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 { |
|
incr numcells 1 |
|
} |
|
default { |
|
if {$grapheme eq "\u0000"} { |
|
incr numcells 1 |
|
} else { |
|
incr numcells [grapheme_width_cached $grapheme] |
|
} |
|
} |
|
} |
|
|
|
} |
|
} |
|
} |
|
#replays/resets each line |
|
lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $numcells]\x1b\[0m |
|
} |
|
} |
|
set outputlines $clearedlines |
|
#todo - determine background/default to be in effect - DECECM ? |
|
puts stderr "replay_codes_overlay: [ansistring VIEW $replay_codes_overlay]" |
|
#lset outputlines 0 $replay_codes_overlay[lindex $outputlines 0] |
|
|
|
} |
|
lf_start { |
|
#raw newlines |
|
# ---------------------- |
|
#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 $opt_startcolumn |
|
# ---------------------- |
|
} |
|
lf_mid { |
|
|
|
set edit_mode 0 |
|
if {$edit_mode} { |
|
#set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied] |
|
#JMN |
|
#ledit inputchunks -1 -1 $overflow_right$unapplied |
|
|
|
set pt_ansi_pt [punk::ansi::ta::split_codes_single $overflow_right] |
|
#join the trailing and leading pt parts of the 2 lists |
|
ledit pt_ansi_pt end end "[lindex $pt_ansi_pt end][lindex $unapplied_ansisplit 0]" |
|
lappend pt_ansi_pt [lrange $unapplied_ansisplit 1 end] |
|
|
|
ledit inputchunks -1 -1 [list ansisplit $pt_ansi_pt] ;#combined overflow_right and unapplied - in ansisplit form |
|
|
|
set overflow_right "" |
|
set unapplied "" |
|
set unapplied_list [list] |
|
set unapplied_ansisplit [list] |
|
|
|
set row $post_render_row |
|
#set col $post_render_col |
|
set col $opt_startcolumn |
|
if {$row > [llength $outputlines]} { |
|
lappend outputlines {*}[lrepeat 1 ""] |
|
} |
|
} else { |
|
if 1 { |
|
if {$overflow_right ne ""} { |
|
if {$opt_expand_right} { |
|
append rendered $overflow_right |
|
set overflow_right "" |
|
} else { |
|
#review - we should really make renderline do the work...? |
|
set overflow_width [punk::ansi::printing_length $overflow_right] |
|
if {$visualwidth + $overflow_width <= $renderwidth} { |
|
append rendered $overflow_right |
|
set overflow_right "" |
|
} else { |
|
if {[tcl::dict::get $vtstate autowrap_mode]} { |
|
set outputlines [linsert $outputlines $renderedrow $overflow_right] |
|
set overflow_right "" |
|
set row [expr {$renderedrow + 2}] |
|
} else { |
|
set overflow_right "" ;#abandon |
|
} |
|
|
|
if {0 && $visualwidth < $renderwidth} { |
|
puts stderr "visualwidth: $visualwidth < renderwidth:$renderwidth" |
|
error "incomplete - abandon?" |
|
set overflowparts [punk::ansi::ta::split_codes $overflow_right] |
|
set remaining_overflow $overflowparts |
|
set filled 0 |
|
foreach {pt code} $overflowparts { |
|
lpop remaining_overflow 0 |
|
if {$pt ne ""} { |
|
set graphemes [punk::char::grapheme_split $pt] |
|
set add "" |
|
set addlen $visualwidth |
|
foreach g $graphemes { |
|
set w [overtype::grapheme_width_cached $g] |
|
if {$addlen + $w <= $renderwidth} { |
|
append add $g |
|
incr addlen $w |
|
} else { |
|
set filled 1 |
|
break |
|
} |
|
} |
|
append rendered $add |
|
} |
|
if {!$filled} { |
|
lpop remaining_overflow 0 ;#pop code |
|
} |
|
} |
|
set overflow_right [join $remaining_overflow ""] |
|
} |
|
} |
|
} |
|
} |
|
set row $post_render_row |
|
set col $opt_startcolumn |
|
if {$row > [llength $outputlines]} { |
|
lappend outputlines {*}[lrepeat 1 ""] |
|
} |
|
} else { |
|
#old version - known to work with various ansi graphics - e.g fruit.ans |
|
# - but fails to limit lines to renderwidth when expand_right == 0 |
|
append rendered $overflow_right |
|
set overflow_right "" |
|
set row $post_render_row |
|
set col $opt_startcolumn |
|
if {$row > [llength $outputlines]} { |
|
lappend outputlines {*}[lrepeat 1 ""] |
|
} |
|
} |
|
} |
|
} |
|
lf_overflow { |
|
#linefeed after renderwidth 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 |
|
} |
|
if {![tcl::dict::get $vtstate insert_mode]} { |
|
assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right when not insert_mode |
|
} |
|
|
|
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 $opt_startcolumn |
|
|
|
} |
|
newlines_above { |
|
#we get a newlines_above instruction when <lf> 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 <lf> at column 1 the same as any other <lf> |
|
|
|
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 ""]] |
|
ledit outputlines $renderedrow-1 $renderedrow-2 {*}[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} { |
|
#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 $opt_startcolumn |
|
} |
|
} 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 < $renderwidth} { |
|
set col [expr {$lastdatacol+1}] |
|
} else { |
|
set col $renderwidth |
|
} |
|
} |
|
} |
|
} |
|
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 $renderwidth |
|
set r $post_render_row |
|
if {$post_render_col > $renderwidth} { |
|
set i $c |
|
while {$i <= $post_render_col} { |
|
if {$c == $renderwidth+1} { |
|
incr r |
|
if {$opt_appendlines} { |
|
if {$r < [llength $outputlines]} { |
|
lappend outputlines "" |
|
} |
|
} |
|
set c $opt_startcolumn |
|
} 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 $renderwidth |
|
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 $renderwidth |
|
} 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 stderr "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]" |
|
|
|
|
|
#renderspace gives us an overflow when there is a grapheme followed by a non-grapheme |
|
#This gives us some possible(probable) leading ANSI (which is probably SGR, or it would have triggered something else) |
|
#followed by a sequence of 1 or more graphemes and some more unprocessed ANSI (which could be anything: SGR,movement etc) |
|
#we want to strip out this leading run of graphemes |
|
#NOTE: 2025 - comment is obsolete/inaccurate. We only ever get 1 grapheme - as prior were consumed/ignored by renderline |
|
#REVIEW!!! |
|
|
|
#example trigger: textblock::frame -width 80 [ansicat us-pump_up_the_jam-355.ans 355x100] |
|
|
|
set row $post_render_row ;#renderline will not advance row when reporting overflow char |
|
if {[tcl::dict::get $vtstate autowrap_mode]} { |
|
incr row |
|
set col $opt_startcolumn ;#whether wrap or not - next data is at column 1 ?? |
|
} else { |
|
set col $post_render_col |
|
#The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs |
|
#There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate |
|
#We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow' |
|
|
|
set drop_graphemes [list] ;#list of contiguous grapheme indices |
|
set new_unapplied_list [list] |
|
set unapplied_ansisplit [list ""] |
|
set idx 0 |
|
|
|
foreach u $unapplied_list { |
|
if {![punk::ansi::ta::detect $u]} { |
|
#puts stderr "g$idx:$u" |
|
if {![llength $drop_graphemes] || $idx == [lindex $drop_graphemes end]+1} { |
|
#we are in the first run of uninterrupted graphemes |
|
#drop by doing nothing with it here |
|
lappend drop_graphemes $idx |
|
} else { |
|
lappend new_unapplied_list $u |
|
ledit unapplied_ansisplit end end "[lindex $unapplied_ansisplit end]$u" |
|
} |
|
} else { |
|
lappend new_unapplied_list $u |
|
lappend unapplied_ansisplit $u "" |
|
} |
|
incr idx |
|
} |
|
#debug |
|
if {[llength $drop_graphemes]} { |
|
set idx0 [lindex $drop_graphemes 0] |
|
set dbg "" |
|
if {$idx0 > 0} { |
|
for {set i 0} {$i < $idx0} {incr i} { |
|
#leading SGR |
|
append dbg [lindex $unapplied_list $i] |
|
} |
|
} |
|
foreach idx $drop_graphemes { |
|
append dbg [lindex $unapplied_list $idx] |
|
} |
|
puts stderr "dropped[llength $drop_graphemes]:$dbg\x1b\[m" |
|
} |
|
set unapplied [join $new_unapplied_list ""] |
|
if {[llength $unapplied_ansisplit] == 1 && [lindex $unapplied_ansisplit 0] eq ""} { |
|
set unapplied_ansisplit [list] |
|
} |
|
set unapplied_list $new_unapplied_list |
|
|
|
#we need to run the reduced unapplied on the same line - further graphemes will just overflow again, but codes or control chars could trigger jumps to other lines |
|
|
|
set overflow_handled 1 |
|
#handled by dropping overflow if any |
|
} |
|
} |
|
overflow_splitchar { |
|
set row $post_render_row ;#renderline will not advance row when reporting overflow char |
|
|
|
#2nd half of grapheme would overflow - treggering grapheme is 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 |
|
if {[tcl::dict::get $vtstate autowrap_mode]} { |
|
if {$renderwidth < 2} { |
|
#edge case of rendering to a single column output - any 2w char will just cause a loop if we don't substitute with something, or drop the character |
|
set idx 0 |
|
set triggering_grapheme_index -1 |
|
foreach u $unapplied_list { |
|
if {![punk::ansi::ta::detect $u]} { |
|
set triggering_grapheme_index $idx |
|
break |
|
} |
|
incr idx |
|
} |
|
#set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] |
|
ledit unapplied_list $triggering_grapheme_index $triggering_grapheme_index |
|
set unapplied [join $unapplied_list ""] |
|
#review - inefficient to re-split (return unapplied_tagged instead of unapplied_ansisplit?) |
|
set unapplied_ansisplit [punk::ansi::ta::split_codes_single $unapplied] |
|
} else { |
|
set col $opt_startcolumn |
|
incr row |
|
} |
|
} else { |
|
set overflow_handled 1 |
|
#handled by dropping entire overflow if any |
|
if {$renderwidth < 2} { |
|
set idx 0 |
|
set triggering_grapheme_index -1 |
|
foreach u $unapplied_list { |
|
if {![punk::ansi::ta::detect $u]} { |
|
set triggering_grapheme_index $idx |
|
break |
|
} |
|
incr idx |
|
} |
|
#set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] |
|
ledit unapplied_list $triggering_grapheme_index $triggering_grapheme_index |
|
set unapplied [join $unapplied_list ""] |
|
#review - inefficient |
|
puts -nonewline stderr . |
|
set unapplied_ansisplit [punk::ansi::ta::split_codes_single $unapplied] |
|
} |
|
} |
|
|
|
} |
|
vt { |
|
|
|
#can vt add a line like a linefeed can? |
|
set row $post_render_row |
|
set col $post_render_col |
|
} |
|
set_window_title { |
|
set newtitle [lindex $instruction 1] |
|
puts stderr "overtype::renderspace set_window_title [ansistring VIEW $newtitle] instruction '$instruction'" |
|
# |
|
} |
|
reset_colour_palette { |
|
puts stderr "overtype::renderspace instruction '$instruction' unimplemented" |
|
} |
|
default { |
|
puts stderr "overtype::renderspace unhandled renderline instruction '$instruction'" |
|
} |
|
|
|
} |
|
|
|
|
|
if {!$opt_expand_right && ![tcl::dict::get $vtstate autowrap_mode]} { |
|
#not allowed to overflow column or wrap therefore we get overflow data to truncate |
|
if {[tcl::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 {[tcl::string::trim $lostdata] eq ""} { |
|
set show_ellipsis 0 |
|
} |
|
#set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] |
|
if {[tcl::string::trim [punk::ansi::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 |
|
} |
|
} |
|
|
|
|
|
|
|
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 |
|
set overflow_right_pt_code_pt [punk::ansi::ta::split_codes_single $overflow_right] |
|
if {![llength $nextprefix_list]} { |
|
set nextprefix_list $overflow_right_pt_code_pt |
|
} else { |
|
#merge tail and head |
|
ledit nextprefix_list end end "[lindex $nextprefix_list end][lindex $overflow_right_pt_code_pt 0]" |
|
lappend nextprefix_list {*}[lrange $overflow_right_pt_code_pt 1 end] |
|
} |
|
} |
|
|
|
#append nextprefix $unapplied |
|
if {![llength $nextprefix_list]} { |
|
set nextprefix_list $unapplied_ansisplit |
|
} else { |
|
#merge tail and head |
|
ledit nextprefix_list end end "[lindex $nextprefix_list end][lindex $unapplied_ansisplit 0]" |
|
lappend nextprefix_list {*}[lrange $unapplied_ansisplit 1 end] |
|
} |
|
|
|
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 {[llength $nextprefix_list]} { |
|
#set inputchunks [linsert $inputchunks 0 $nextprefix] |
|
#JMN - assume backwards compat ledit available from punk::lib (for tcl <9) |
|
ledit inputchunks -1 -1 [list ansisplit $nextprefix_list] |
|
} |
|
|
|
|
|
incr overidx |
|
incr loop |
|
if {$loop >= $looplimit} { |
|
puts stderr "overtype::renderspace looplimit reached ($looplimit)" |
|
lappend outputlines "[a+ red bold]<truncated> - looplimit $looplimit reached[a]" |
|
set Y [a+ yellow bold] |
|
set RST [a] |
|
set sep_header ----DEBUG----- |
|
set debugmsg "" |
|
append debugmsg "${Y}${sep_header}${RST}" \n |
|
append debugmsg "looplimit $looplimit reached\n" |
|
append debugmsg "data_mode:$data_mode\n" |
|
append debugmsg "opt_appendlines:$opt_appendlines\n" |
|
append debugmsg "prev_row :[tcl::dict::get $renderopts -cursor_row]\n" |
|
append debugmsg "prev_col :[tcl::dict::get $renderopts -cursor_column]\n" |
|
tcl::dict::for {k v} $rinfo { |
|
append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n |
|
} |
|
append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n |
|
|
|
puts stdout $debugmsg |
|
#todo - config regarding error dumps rather than just dumping in working dir |
|
set fd [open [pwd]/error_overtype.txt w] |
|
puts $fd $debugmsg |
|
close $fd |
|
error $debugmsg |
|
break |
|
} |
|
} |
|
|
|
set result [join $outputlines \n] |
|
if {!$opt_info} { |
|
return $result |
|
} else { |
|
#emit to debug window like basictelnet does? make debug configurable as syslog or even a telnet server to allow on 2nd window? |
|
#append result \n$instruction_stats\n |
|
set inforesult [dict create\ |
|
result $result\ |
|
last_instruction $instruction\ |
|
instruction_stats $instruction_stats\ |
|
] |
|
if {$opt_info == 2} { |
|
return [pdict -channel none inforesult] |
|
} else { |
|
return $inforesult |
|
} |
|
} |
|
} |
|
|
|
#todo - left-right ellipsis ? |
|
proc 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 opts [tcl::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] |
|
foreach {k v} $argsflags { |
|
switch -- $k { |
|
-bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
set known_opts [tcl::dict::keys $opts] |
|
error "overtype::centre unknown option '$k'. Known options: $known_opts" |
|
} |
|
} |
|
} |
|
#set opts [tcl::dict::merge $defaults $argsflags] |
|
# -- --- --- --- --- --- |
|
set opt_transparent [tcl::dict::get $opts -transparent] |
|
set opt_ellipsis [tcl::dict::get $opts -ellipsis] |
|
set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] |
|
set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] |
|
set opt_exposed1 [tcl::dict::get $opts -exposed1] |
|
set opt_exposed2 [tcl::dict::get $opts -exposed2] |
|
# -- --- --- --- --- --- |
|
|
|
|
|
set underblock [tcl::string::map {\r\n \n} $underblock] |
|
set overblock [tcl::string::map {\r\n \n} $overblock] |
|
|
|
set underlines [split $underblock \n] |
|
#set renderwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] |
|
lassign [blocksize $underblock] _w renderwidth _h renderheight |
|
set overlines [split $overblock \n] |
|
lassign [blocksize $overblock] _w overblock_width _h overblock_height |
|
set under_exposed_max [expr {$renderwidth - $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 {[tcl::string::tolower [tcl::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 < $renderwidth} { |
|
set udiff [expr {$renderwidth - $ulen}] |
|
set undertext "$undertext[string repeat { } $udiff]" |
|
} |
|
set undertext $replay_codes_underlay$undertext |
|
set overtext $replay_codes_overlay$overtext |
|
|
|
set overflowlength [expr {$overtext_datalen - $renderwidth}] |
|
#review - right-to-left langs should elide on left! - extra option required |
|
|
|
if {$overflowlength > 0} { |
|
#overlay line wider or equal |
|
#review - we still expand_right for centred for now.. possibly should do something like -expand_leftright with ellipsis each end? |
|
set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right [tcl::dict::get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] |
|
set rendered [tcl::dict::get $rinfo result] |
|
set overflow_right [tcl::dict::get $rinfo overflow_right] |
|
set unapplied [tcl::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 {![tcl::dict::get $opts -overflow]} { |
|
#lappend outputlines [tcl::string::range $overtext 0 [expr {$renderwidth - 1}]] |
|
#set overtext [tcl::string::range $overtext 0 $renderwidth-1 ] |
|
if {$opt_ellipsis} { |
|
set show_ellipsis 1 |
|
if {!$opt_ellipsiswhitespace} { |
|
#we don't want ellipsis if only whitespace was lost |
|
#don't use tcl::string::range on ANSI data |
|
#set lostdata [tcl::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 {[tcl::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 [tcl::dict::get $rinfo result] |
|
} |
|
set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] |
|
set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] |
|
} |
|
return [join $outputlines \n] |
|
} |
|
|
|
#overtype::right is for a rendered ragged underblock and a rendered ragged overblock |
|
#ie we can determine the block width for bost by examining the lines and picking the longest. |
|
# |
|
proc 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|<char>? undertext overtext} |
|
} |
|
foreach {underblock overblock} [lrange $args end-1 end] break |
|
|
|
set opts [tcl::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] |
|
tcl::dict::for {k v} $argsflags { |
|
switch -- $k { |
|
-bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
set known_opts [tcl::dict::keys $opts] |
|
error "overtype::centre unknown option '$k'. Known options: $known_opts" |
|
} |
|
} |
|
} |
|
#set opts [tcl::dict::merge $defaults $argsflags] |
|
# -- --- --- --- --- --- |
|
set opt_transparent [tcl::dict::get $opts -transparent] |
|
set opt_ellipsis [tcl::dict::get $opts -ellipsis] |
|
set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] |
|
set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] |
|
set opt_overflow [tcl::dict::get $opts -overflow] |
|
set opt_exposed1 [tcl::dict::get $opts -exposed1] |
|
set opt_exposed2 [tcl::dict::get $opts -exposed2] |
|
set opt_align [tcl::dict::get $opts -align] |
|
# -- --- --- --- --- --- |
|
|
|
set underblock [tcl::string::map {\r\n \n} $underblock] |
|
set overblock [tcl::string::map {\r\n \n} $overblock] |
|
|
|
set underlines [split $underblock \n] |
|
lassign [blocksize $underblock] _w renderwidth _h renderheight |
|
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,$renderwidth - $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 < $renderwidth} { |
|
set udiff [expr {$renderwidth - $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 $replay_codes_underlay$undertext |
|
set overtext $replay_codes_overlay$overtext |
|
|
|
set overflowlength [expr {$overtext_datalen - $renderwidth}] |
|
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 [tcl::dict::get $rinfo replay_codes] |
|
set rendered [tcl::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 [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] |
|
if {[tcl::string::trim $lostdata] eq ""} { |
|
set show_ellipsis 0 |
|
} |
|
} |
|
if {$show_ellipsis} { |
|
set ellipsis $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(exapnd_right) 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 -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] |
|
lappend outputlines [tcl::dict::get $rinfo result] |
|
} |
|
set replay_codes [tcl::dict::get $rinfo replay_codes] |
|
set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] |
|
set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] |
|
} |
|
|
|
return [join $outputlines \n] |
|
} |
|
|
|
proc left {args} { |
|
overtype::block -blockalign left {*}$args |
|
} |
|
#overtype a (possibly ragged) underblock with a (possibly ragged) overblock |
|
proc block {args} { |
|
variable default_ellipsis_horizontal |
|
# @d !todo - implement overflow, length checks etc |
|
|
|
if {[llength $args] < 2} { |
|
error {usage: ?-blockalign left|centre|right? ?-textalign left|centre|right? ?-overflow [1|0]? ?-transparent 0|<char>? undertext overtext} |
|
} |
|
#foreach {underblock overblock} [lrange $args end-1 end] break |
|
lassign [lrange $args end-1 end] underblock overblock |
|
|
|
set opts [tcl::dict::create\ |
|
-ellipsis 0\ |
|
-ellipsistext $default_ellipsis_horizontal\ |
|
-ellipsiswhitespace 0\ |
|
-overflow 0\ |
|
-transparent 0\ |
|
-exposed1 \uFFFD\ |
|
-exposed2 \uFFFD\ |
|
-textalign "left"\ |
|
-textvertical "top"\ |
|
-blockalign "left"\ |
|
-blockalignbias left\ |
|
-blockvertical "top"\ |
|
] |
|
set argsflags [lrange $args 0 end-2] |
|
tcl::dict::for {k v} $argsflags { |
|
switch -- $k { |
|
-blockalignbias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -textalign - -blockalign - -blockvertical { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "overtype::block unknown option '$k'. Known options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
# -- --- --- --- --- --- |
|
set opt_transparent [tcl::dict::get $opts -transparent] |
|
set opt_ellipsis [tcl::dict::get $opts -ellipsis] |
|
set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] |
|
set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] |
|
set opt_overflow [tcl::dict::get $opts -overflow] |
|
set opt_exposed1 [tcl::dict::get $opts -exposed1] |
|
set opt_exposed2 [tcl::dict::get $opts -exposed2] |
|
set opt_textalign [tcl::dict::get $opts -textalign] |
|
set opt_blockalign [tcl::dict::get $opts -blockalign] |
|
if {$opt_blockalign eq "center"} { |
|
set opt_blockalign "centre" |
|
} |
|
# -- --- --- --- --- --- |
|
|
|
set underblock [tcl::string::map {\r\n \n} $underblock] |
|
set overblock [tcl::string::map {\r\n \n} $overblock] |
|
|
|
set underlines [split $underblock \n] |
|
lassign [blocksize $underblock] _w renderwidth _h renderheight |
|
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,$renderwidth - $overblock_width)}] |
|
|
|
switch -- $opt_blockalign { |
|
left { |
|
set left_exposed 0 |
|
} |
|
right { |
|
set left_exposed $under_exposed_max |
|
} |
|
centre { |
|
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 {[tcl::string::tolower [tcl::dict::get $opts -blockalignbias]] eq "left"} { |
|
set left_exposed $beforehalf |
|
} else { |
|
#bias to the right |
|
set left_exposed [expr {$beforehalf + 1}] |
|
} |
|
} |
|
} else { |
|
set left_exposed 0 |
|
} |
|
} |
|
default { |
|
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 < $renderwidth} { |
|
set udiff [expr {$renderwidth - $ulen}] |
|
#puts xxx |
|
append undertext [string repeat { } $udiff] |
|
} |
|
if {$overtext_datalen < $overblock_width} { |
|
set odiff [expr {$overblock_width - $overtext_datalen}] |
|
switch -- $opt_textalign { |
|
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 $replay_codes_underlay$undertext |
|
set overtext $replay_codes_overlay$overtext |
|
|
|
set overflowlength [expr {$overtext_datalen - $renderwidth}] |
|
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 -expand_right $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] |
|
set replay_codes [tcl::dict::get $rinfo replay_codes] |
|
set rendered [tcl::dict::get $rinfo result] |
|
set overflow_right [tcl::dict::get $rinfo overflow_right] |
|
set unapplied [tcl::dict::get $rinfo unapplied] |
|
if {!$opt_overflow} { |
|
if {$opt_ellipsis} { |
|
set show_ellipsis 1 |
|
if {!$opt_ellipsiswhitespace} { |
|
#we don't want ellipsis if only whitespace was lost |
|
#don't use tcl::string::range on ANSI data |
|
#set lostdata [tcl::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 {[tcl::string::trim $lostdata] eq ""} { |
|
set show_ellipsis 0 |
|
} |
|
} |
|
if {$show_ellipsis} { |
|
set rendered [overtype::block -blockalign right $rendered $opt_ellipsistext] |
|
} |
|
} |
|
|
|
#if {$opt_ellipsis} { |
|
# set show_ellipsis 1 |
|
# if {!$opt_ellipsiswhitespace} { |
|
# #we don't want ellipsis if only whitespace was lost |
|
# set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] |
|
# if {[tcl::string::trim $lostdata] eq ""} { |
|
# set show_ellipsis 0 |
|
# } |
|
# } |
|
# if {$show_ellipsis} { |
|
# set ellipsis [tcl::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 expand_right 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 -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] |
|
#puts stderr "--> [ansistring VIEW -lf 1 -nul 1 $rinfo] <--" |
|
set overflow_right [tcl::dict::get $rinfo overflow_right] |
|
set unapplied [tcl::dict::get $rinfo unapplied] |
|
lappend outputlines [tcl::dict::get $rinfo result] |
|
} |
|
set replay_codes [tcl::dict::get $rinfo replay_codes] |
|
set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] |
|
set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] |
|
} |
|
|
|
return [join $outputlines \n] |
|
} |
|
|
|
#variable optimise_ptruns 10 ;# can be set to zero to disable the ptruns branches |
|
variable optimise_ptruns 5 |
|
|
|
|
|
|
|
namespace eval argdoc { |
|
variable PUNKARGS |
|
lappend PUNKARGS [list { |
|
@id -id ::overtype::renderline |
|
@cmd -name overtype::renderline\ |
|
-summary\ |
|
{Render a line of text/ANSI input over a line of text.}\ |
|
-help\ |
|
{renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode |
|
commandline repl for the Tcl Punk Shell. |
|
It is also a central part of an ansi (micro) virtual terminal-emulator of sorts. |
|
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. |
|
Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't |
|
affect another. |
|
Calling on the punk::ansi library - it can coalesce codes to keep the size down. |
|
|
|
It is a giant mess of doing exactly what common wisdom says not to do... lots at once. |
|
Renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a |
|
static underlay. |
|
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. |
|
The overlay couuld be similar - in which case it may often be used to overwrite a column or section of |
|
the underlay. |
|
The overlay could however be a sequence of ANSI-laden text that jumps all over the place. |
|
|
|
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. |
|
By suppyling the ${$B}-info${$N} 1 option - it can return various fields indicating the state of the render. |
|
The main 3 are: result, overflow_right, and unapplied. |
|
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. |
|
} |
|
@opts |
|
-etabs -default 0 -type boolean |
|
-width -default \uFFEF -type integer |
|
-expand_right -default 0 -type boolean |
|
-transparent -default 0 -type {literal(0)|literal(1)|regexp} -help\ |
|
"0 to disable transparency processing |
|
1 to enable space characters in the |
|
overlay to be transparent, or a regex |
|
to match the character(s) required to be |
|
transparent in the overlay." |
|
-startcolumn -default 1 -type integer |
|
-cursor_column -default 1 -type integer -help\ |
|
{First column is 1. Cursor column can be zero or negative} |
|
-cursor_row -default "" -type integer |
|
-insert_mode -default 1 -type boolean |
|
-crm_mode -default 0 -type boolean |
|
-autowrap_mode -default 1 -type boolean |
|
-reverse_mode -default 0 -type boolean |
|
-info -default 0 -type boolean -help\ |
|
"When set to 1, return a dictionary of settings useful for |
|
processing ANSI input in a loop. When zero, the resulting |
|
string will contain the updated line, but not all the |
|
overtext may have been applied." |
|
-exposed1 -default \uFFFD -help\ |
|
{A character of single terminal column width to use |
|
as replacement when first-half of an underlying char |
|
is exposed due to overlay positioning/transparency |
|
which obscures the second-half of the char. May be ANSI |
|
coloured as this doesn't affect the display width. |
|
Default is \uFFFD - the unicode replacement char.} |
|
-exposed2 -default \uFFFD -help\ |
|
{A character of single terminal column width to use |
|
as replacement when second-half of an underlying char |
|
is exposed due to overlay positioning/transparency |
|
which obscures the first-half of the char. May be ANSI |
|
coloured as this doesn't affect the display width. |
|
Default is \uFFFD - the unicode replacement char.} |
|
-cursor_restore_attributes -default "" |
|
-cp437 -default 0 -type boolean |
|
-experimental -default {} |
|
-overtext_type -type string -choices {mixed plain ansisplit} -default mixed |
|
@values -min 2 -max 2 |
|
undertext -type string -help\ |
|
"A single line of text which may contain pre-rendered ANSI. |
|
'pre-rendered' in this context means that it may contain |
|
various static ANSI codes such as SGR colours and attributes |
|
but not motion-control or more complex ANSI sequences. |
|
It is an error to supply a newline (lf) character in the |
|
undertext." |
|
overtext -type string -help\ |
|
"ANSI (or plain text) to overlay onto the undertext. |
|
May contain ANSI movement codes even if they would move the |
|
cursor to another line. If -info is zero, the output will |
|
only display up to the point where the cursor moved off-line. |
|
If -info is 1, the line moved to may be reflected in the |
|
cursor_row element of the result. Overtext may contain an lf |
|
which is effectively a form of 'movement control' to increment |
|
the row. |
|
Other ANSI codes may perform operations such as changing the |
|
insert_mode or reverse_mode - and these are reflected in the |
|
result dictionary when '-info 1' is used, so that the state |
|
can then be applied to subsequent lines." |
|
}] |
|
} |
|
|
|
proc renderline {args} { |
|
#todo - fix 'unapplied' mechanism.This is particularly inefficient for long lines, or data such as binarytext which is not line-based. |
|
#All unapplied data is re-split/reprocessed repeatedly for each line! This is very wasteful and slow. |
|
|
|
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### |
|
# 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? |
|
# This would probably be impractical to support for different fonts) |
|
#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? |
|
|
|
|
|
|
|
#*** !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. |
|
|
|
#puts stderr "renderline '$args'" |
|
variable optimise_ptruns |
|
|
|
if {[llength $args] < 2} { |
|
error {usage: ?-info 0|1? ?-startcolumn <int>? ?-cursor_column <int>? ?-cursor_row <int>|""? ?-transparent [0|1|<regexp>]? ?-expand_right [1|0]? undertext overtext} |
|
} |
|
set under [lindex $args end-1] |
|
set over [lindex $args end] |
|
#lassign [lrange $args end-1 end] under over |
|
if {[string last \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" |
|
#} |
|
|
|
#generally faster to create a new dict in the proc than to use a namespace variable to create dict once and link to variable (2024 8.6/8.7) |
|
set opts [tcl::dict::create\ |
|
-etabs 0\ |
|
-width \uFFEF\ |
|
-expand_right 0\ |
|
-transparent 0\ |
|
-startcolumn 1\ |
|
-cursor_column 1\ |
|
-cursor_row ""\ |
|
-insert_mode 1\ |
|
-crm_mode 0\ |
|
-autowrap_mode 1\ |
|
-reverse_mode 0\ |
|
-info 0\ |
|
-exposed1 \uFFFD\ |
|
-exposed2 \uFFFD\ |
|
-cursor_restore_attributes ""\ |
|
-cp437 0\ |
|
-experimental {}\ |
|
-overtext_type mixed\ |
|
] |
|
#-overtext_type plain|mixed|ansisplit |
|
|
|
#-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, currsor movements to 2nd charcol, or overflow/expand_right |
|
#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] |
|
tcl::dict::for {k v} $argsflags { |
|
switch -- $k { |
|
-experimental - -cp437 - -width - -expand_right - -transparent - -startcolumn - -cursor_column - -cursor_row |
|
- -crm_mode - -insert_mode - -autowrap_mode - -reverse_mode |
|
- -info - -exposed1 - -exposed2 - -cursor_restore_attributes - -overtext_type { |
|
tcl::dict::set opts $k $v |
|
} |
|
default { |
|
error "overtype::renderline unknown option '$k'. Known options: [tcl::dict::keys $opts]" |
|
} |
|
} |
|
} |
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
set opt_width [tcl::dict::get $opts -width] |
|
set opt_etabs [tcl::dict::get $opts -etabs] |
|
set opt_expand_right [tcl::dict::get $opts -expand_right] |
|
set opt_colstart [tcl::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 [tcl::dict::get $opts -cursor_column];#start cursor column relative to overlay |
|
set opt_row_context [tcl::dict::get $opts -cursor_row] |
|
set opt_overtext_type [tcl::dict::get $opts -overtext_type] |
|
if {[string length $opt_row_context]} { |
|
if {![tcl::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 [tcl::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 [tcl::dict::get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line |
|
set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] ;#DECSNM |
|
set opt_crm_mode [tcl::dict::get $opts -crm_mode];# CRM - show control character mode |
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
set temp_cursor_saved [tcl::dict::get $opts -cursor_restore_attributes] |
|
|
|
set cp437_glyphs [tcl::dict::get $opts -cp437] |
|
set cp437_map [tcl::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? |
|
tcl::dict::unset cp437_map \n |
|
} |
|
|
|
set opt_transparent [tcl::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 [tcl::dict::get $opts -info] |
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
set opt_exposed1 [tcl::dict::get $opts -exposed1] |
|
set opt_exposed2 [tcl::dict::get $opts -exposed2] |
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
|
|
if {$opt_row_context eq ""} { |
|
set cursor_row 1 |
|
} else { |
|
set cursor_row $opt_row_context |
|
} |
|
|
|
set insert_mode $opt_insert_mode ;#default 1 |
|
set autowrap_mode $opt_autowrap_mode ;#default 1 |
|
set crm_mode $opt_crm_mode ;#default 0 (Show Control Character mode) |
|
set reverse_mode $opt_reverse_mode |
|
|
|
#----- |
|
# |
|
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 |
|
#todo - we also need to handle the new threaded repl where console config is in a different thread. |
|
# - also - concept of sub-regions being mini-consoles with their own settings - 'id' for console, or use in/out channels as id? |
|
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] |
|
} |
|
|
|
#review - is untabifying sensible at this point?? |
|
if {$opt_overtext_type eq "ansisplit"} { |
|
#todo - something for each pt part? |
|
} else { |
|
#plain|mixed |
|
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 ""} { |
|
if {[punk::ansi::ta::detect $under]} { |
|
set undermap [punk::ansi::ta::split_codes_single $under] |
|
} else { |
|
#single plaintext part |
|
set undermap [list $under] |
|
} |
|
} else { |
|
set undermap [list] |
|
} |
|
set understacks [list] |
|
set understacks_gx [list] |
|
set pm_list [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 expand_right 0 truncation |
|
set remainder [list] ;#for returnextra |
|
foreach {pt code} $undermap { |
|
#pt = plain text |
|
#append pt_underchars $pt |
|
if {$pt ne ""} { |
|
if {$cp437_glyphs} { |
|
set pt [tcl::string::map $cp437_map $pt] |
|
} |
|
set is_ptrun 0 |
|
if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} { |
|
set p1 [tcl::string::index $pt 0] |
|
set hex [format %x [scan $p1 %c]] ;#punk::char::char_hex |
|
set re [tcl::string::cat {^[} \\U$hex {]+$}] |
|
set is_ptrun [regexp $re $pt] |
|
} |
|
if {$is_ptrun} { |
|
set width [grapheme_width_cached $p1] ;# when zero??? |
|
set ptlen [string length $pt] |
|
#puts -nonewline stderr !$ptlen! |
|
if {$width <= 1} { |
|
#review - 0 and 1? |
|
incr i_u $ptlen |
|
lappend understacks {*}[lrepeat $ptlen $u_codestack] |
|
#we need to store the gx0 state per column - as characters with or without gx0 can be overlayed anywhere |
|
lappend understacks_gx {*}[lrepeat $ptlen $u_gx_stack] |
|
lappend undercols {*}[lrepeat $ptlen $p1] |
|
} else { |
|
incr i_u $ptlen ;#2nd col empty str - so same as above |
|
set 2ptlen [expr {$ptlen * 2}] |
|
lappend understacks {*}[lrepeat $2ptlen $u_codestack] |
|
lappend understacks_gx {*}[lrepeat $2ptlen $u_gx_stack] |
|
set l [concat {*}[lrepeat $ptlen [list $p1 ""]]] |
|
lappend undercols {*}$l |
|
unset l |
|
} |
|
|
|
} else { |
|
foreach grapheme [punk::char::grapheme_split $pt] { |
|
#an ugly but easy 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. |
|
#todo - test decimal value instead, compare performance |
|
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 { |
|
#zero width still acts as 1 below??? review what should happen |
|
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] ;#can only use with graphemes that have a single replacement char.. |
|
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 |
|
#keep any remaining PMs in place |
|
if {$code ne ""} { |
|
set c1c2 [tcl::string::range $code 0 1] |
|
|
|
set leadernorm [tcl::string::range [tcl::string::map [list\ |
|
\x1b\[ 7CSI\ |
|
\x9b 8CSI\ |
|
\x1b\( 7GFX\ |
|
\x1b^ 7PMX\ |
|
\x1bX 7SOS\ |
|
] $c1c2] 0 3];# leadernorm is 1st 2 chars mapped to 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 {[tcl::string::index $c1c2 0] eq "\x1b"} { |
|
set maybemouse [tcl::string::index $code 2] |
|
} |
|
|
|
if {$maybemouse ne "<" && [tcl::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 -- [tcl::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] |
|
} |
|
} |
|
} |
|
7PMX - 7SOS { |
|
#we can have PMs or SOS (start of string) in the underlay - though mostly the PMs should have been processed.. |
|
#attach the PM/SOS (entire ANSI sequence) to the previous grapheme! |
|
#It should not affect the size - but terminal display may get thrown out if terminal doesn't support them. |
|
|
|
#note that there may in theory already be ANSI stored - we don't assume it was a pure grapheme string |
|
set graphemeplus [lindex $undercols end] |
|
if {$graphemeplus ne "\0"} { |
|
append graphemeplus $code |
|
} else { |
|
set graphemeplus $code |
|
} |
|
lset undercols end $graphemeplus |
|
#The grapheme_width_cached function will be called on this later - and doesn't account for ansi. |
|
#we need to manually cache the item with it's proper width |
|
variable grapheme_widths |
|
#stripped and plus version keys pointing to same length |
|
dict set grapheme_widths $graphemeplus [grapheme_width_cached [::punk::ansi::ansistrip $graphemeplus]] |
|
|
|
} |
|
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..? |
|
} |
|
|
|
#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} { |
|
#set undercols [list {*}$undercols {*}[lrepeat $diff "\u0000"]] ;#2024 - much slower |
|
lappend undercols {*}[lrepeat $diff "\u0000"] |
|
lappend understacks {*}[lrepeat $diff $cs] |
|
lappend understacks_gx {*}[lrepeat $diff $gs] |
|
} |
|
} |
|
} |
|
|
|
if {$opt_width ne "\uFFEF"} { |
|
set renderwidth $opt_width |
|
} else { |
|
set renderwidth [llength $undercols] |
|
} |
|
|
|
|
|
#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 containing the right number of columns. |
|
#this will be processed as transparent - and handle doublewidth underlay characters appropriately |
|
set startpadding [string repeat " " [expr {$opt_colstart -1}]] |
|
#overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency |
|
if {$startpadding ne "" || $overdata ne ""} { |
|
if {$opt_overtext_type eq "ansisplit"} { |
|
set overmap $overdata |
|
lset overmap 0 "$startpadding[lindex $overmap 0]" |
|
} else { |
|
if {[punk::ansi::ta::detect $overdata]} { |
|
#TODO!! rework this. |
|
#e.g 200K+ input file with no newlines - we are wastefully calling split_codes_single repeatedly on mostly the same data. |
|
#set overmap [punk::ansi::ta::split_codes_single $startpadding$overdata] |
|
set overmap [punk::ansi::ta::split_codes_single $overdata] |
|
lset overmap 0 "$startpadding[lindex $overmap 0]" |
|
|
|
} else { |
|
#single plaintext part |
|
set overmap [list $startpadding$overdata] |
|
} |
|
} |
|
} else { |
|
set overmap [list] |
|
} |
|
#### |
|
|
|
|
|
#todo - detect plain ascii no-ansi input line (aside from leading ansi reset) |
|
#will that allow some optimisations? |
|
|
|
#todo - detect repeated transparent char in overlay |
|
#regexp {^(.)\1+$} ;#backtracking regexp - relatively slow. |
|
# - try set c [string index $data 0]; regexp [string map [list %c% $c] {^[%c%]+$}] $data |
|
#we should be able to optimize to pass through the underlay?? |
|
|
|
#??? |
|
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 |
|
set overlay_grapheme_control_stacks [list] |
|
#REVIEW - even if we pass in a pre-split overtext (-overtext_type ansisplit) |
|
#we are re-generating the overlay_grapheme_control_stacks list each time |
|
#this is a big issue when overtext is not broken into lines, but is just a big long ansi and/or plain text string. |
|
#todo - return also the unapplied portion of the overlay_grapheme_control_stacks list?? |
|
foreach {pt code} $overmap { |
|
if {$pt ne ""} { |
|
#todo - wrap in test for empty pt (we used split_codes_single - and it may be common for sgr sequences to be unmerged and so have empty pts between) |
|
if {$cp437_glyphs} { |
|
set pt [tcl::string::map $cp437_map $pt] |
|
} |
|
append pt_overchars $pt |
|
#will get empty pt between adjacent codes |
|
if {!$crm_mode} { |
|
|
|
set is_ptrun 0 |
|
if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} { |
|
set p1 [tcl::string::index $pt 0] |
|
set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+$}] |
|
set is_ptrun [regexp $re $pt] |
|
|
|
#leading only? we would have to check for graphemes at the trailing boundary? |
|
#set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+}] |
|
#set is_ptrun [regexp -indices $re $pt runrange] |
|
#if {$is_ptrun && 1} { |
|
#} |
|
} |
|
if {$is_ptrun} { |
|
#review - in theory a run over a certain length won't be part of some grapheme combo (graphemes can be long e.g 44?, but not as runs(?)) |
|
#could be edge cases for runs at line end? (should be ok as we include trailing \n in our data) |
|
set len [string length $pt] |
|
set g_element [list g $p1] |
|
#puts -nonewline stderr "!$len!" |
|
|
|
#lappend overstacks {*}[lrepeat $len $o_codestack] |
|
#lappend overstacks_gx {*}[lrepeat $len $o_gxstack] |
|
#incr i_o $len |
|
#lappend overlay_grapheme_control_list {*}[lrepeat $len [list g $p1]] |
|
#lappend overlay_grapheme_control_stacks {*}[lrepeat $len $o_codestack] |
|
|
|
set pi 0 |
|
incr i_o $len |
|
while {$pi < $len} { |
|
lappend overstacks $o_codestack |
|
lappend overstacks_gx $o_gxstack |
|
lappend overlay_grapheme_control_list $g_element |
|
lappend overlay_grapheme_control_stacks $o_codestack |
|
incr pi |
|
} |
|
} else { |
|
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 |
|
} |
|
} |
|
} else { |
|
set tsbegin [clock micros] |
|
foreach grapheme_original [punk::char::grapheme_split $pt] { |
|
set pt_crm [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $grapheme_original] |
|
#puts stderr "ptlen [string length $pt] graphemelen[string length $grapheme_original] pt_crmlen[string length $pt_crm] $pt_crm" |
|
foreach grapheme [punk::char::grapheme_split $pt_crm] { |
|
if {$grapheme eq "\n"} { |
|
lappend overlay_grapheme_control_stacks $o_codestack |
|
lappend overlay_grapheme_control_list [list crmcontrol "\x1b\[00001E"] |
|
} else { |
|
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 |
|
} |
|
} |
|
} |
|
set elapsed [expr {[clock micros] - $tsbegin}] |
|
puts stderr "ptlen [string length $pt] elapsedus:$elapsed" |
|
} |
|
} |
|
|
|
#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 ""} { |
|
#we need to immediately set crm_mode here if \x1b\[3h received |
|
if {$code eq "\x1b\[3h"} { |
|
set crm_mode 1 |
|
} elseif {$code eq "\x1b\[3l"} { |
|
set crm_mode 0 |
|
} |
|
#else crm_mode could be set either way from options |
|
if {$crm_mode && $code ne "\x1b\[00001E"} { |
|
#treat the code as type 'g' like above - only allow through codes to reset mode REVIEW for now just \x1b\[3l ? |
|
#we need to somehow convert further \n in the graphical rep to an instruction for newline that will bypass further crm_mode processing or we would loop. |
|
set code_as_pt [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $code] |
|
#split using standard split for first foreach loop - grapheme based split when processing 2nd foreach loop |
|
set chars [split $code_as_pt ""] |
|
set codeparts [list] ;#list of 2-el lists each element {crmcontrol <ansi>} or {g <text>} |
|
foreach c $chars { |
|
if {$c eq "\n"} { |
|
#use CNL (cursor next line) \x1b\[00001E ;#leading zeroes ok for this processor - used as debugging aid to distinguish |
|
lappend codeparts [list crmcontrol "\x1b\[00001E"] |
|
} else { |
|
if {[llength $codeparts] > 0 && [lindex $codeparts end 0] eq "g"} { |
|
set existing [lindex $codeparts end 1] |
|
lset codeparts end [list g [string cat $existing $c]] |
|
} else { |
|
lappend codeparts [list g $c] |
|
} |
|
} |
|
} |
|
|
|
set partidx 0 |
|
foreach record $codeparts { |
|
lassign $record rtype rval |
|
switch -exact -- $rtype { |
|
g { |
|
append pt_overchars $rval |
|
foreach grapheme [punk::char::grapheme_split $rval] { |
|
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 |
|
} |
|
} |
|
crmcontrol { |
|
#leave o_codestack |
|
lappend overlay_grapheme_control_stacks $o_codestack |
|
lappend overlay_grapheme_control_list [list crmcontrol $rval] |
|
} |
|
} |
|
} |
|
} else { |
|
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 {\x1b\[[0-1];[0-9][0-9]?[0-9]?;[0-9][0-9]?[0-9]?;[0-9][0-9]?[0-9]?t} $code]} { |
|
#pablodraw 24bit color - convert to standard sgr RGB code |
|
#we could do a more precise 000-255 regexp for each r g b, something like: ((?:[0-1]?[0-9]?[0-9])|(?:2[0-4][0-9])|(?:25[0-5])) |
|
#but that seems more expensive for little likely use (?) review |
|
lassign [regexp -all -inline {\x1b\[([0-1]);([0-9][0-9]?[0-9]?);([0-9][0-9]?[0-9]?);([0-9][0-9]?[0-9]?)t} $code] _ isfg pablo_r pablo_g pablo_b |
|
#todo - if any r g b value > 255 - add as [list other $code] |
|
if {$isfg} { |
|
set rgbcode "\x1b\[38\;2\;$pablo_r\;$pablo_g\;${pablo_b}m" |
|
} else { |
|
set rgbcode "\x1b\[48\;2\;$pablo_r\;$pablo_g\;${pablo_b}m" |
|
} |
|
set dup_posns [lsearch -all -exact $o_codestack $rgbcode] ;#must be -exact because of square-bracket glob chars |
|
set o_codestack [lremove $o_codestack {*}$dup_posns] |
|
lappend o_codestack $rgbcode |
|
lappend overlay_grapheme_control_list [list sgr $rgbcode] |
|
} 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 { |
|
#review - gx0 should just be a flag like autowrap_mode and insert_mode? |
|
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 {[tcl::dict::exists $overstacks $max_overlay_grapheme_index]} { |
|
# set replay_codes_overlay [join [tcl::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_expand_right} { |
|
#expand_right 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. |
|
#we currently only support horizontal expansion to the right (review regarding RTL text!) |
|
set overflow_idx -1 |
|
} else { |
|
#expand_right 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 unapplied_list [list] |
|
set unapplied_ansisplit [list ""] ;#pt code ... pt |
|
|
|
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 -expand_right 1 "" data |
|
|
|
#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 |
|
|
|
#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 |
|
#crm_mode affects both graphic and control |
|
if {0 && $crm_mode} { |
|
set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $ch] |
|
set chars [string map [list \n "\x1b\[00001E"] $chars] |
|
if {[llength [split $chars ""]] > 1} { |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
#prefix the unapplied controls with the string version of this control |
|
#set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]] |
|
#JMN - backwards compat ledit from punk::lib for tcl <9 |
|
ledit unapplied_list -1 -1 {*}[split $chars ""] |
|
set unapplied [join $unapplied_list ""] |
|
lset unapplied_ansisplit 0 $chars ;#no existing ? |
|
|
|
#incr idx_over |
|
break |
|
} else { |
|
set ch $chars |
|
} |
|
} |
|
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 <= $renderwidth-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 [tcl::string::map [list \n <lf> \x85 <lf> \b <bs> \r <cr> \v <vt> \x7f <del>] $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 { |
|
"<lf>" { |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
if {$idx == 0} { |
|
#puts "---a <lf> 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_to_unapplied $overlay_grapheme_control_list $gci |
|
break |
|
} elseif {$overflow_idx != -1 && $idx == $overflow_idx} { |
|
#linefeed after final column |
|
#puts "---c <lf> at overflow_idx=$overflow_idx" |
|
incr cursor_row |
|
set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1 |
|
set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently |
|
priv::render_to_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" |
|
if {$insert_mode == 0} { |
|
incr cursor_row |
|
if {$idx == -1 || $overflow_idx > $idx} { |
|
#don't set overflow_idx higher if it's already set lower and we're adding graphemes to overflow |
|
set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1 |
|
} |
|
set instruction lf_mid |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
break |
|
} else { |
|
incr cursor_row |
|
#don't adjust the overflow_idx |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction lf_mid |
|
break ;# could have overdata following the \n - don't keep processing |
|
} |
|
} |
|
|
|
} |
|
"<cr>" { |
|
#will we want/need to use raw <cr> for keypresses in terminal? (terminal with LNM in standard reset mode means enter=<cr> this is the usual config for terminals) |
|
#So far we are assuming the caller has translated to <lf> 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 ;#? |
|
} |
|
"<bs>" { |
|
#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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction backspace_at_start |
|
break |
|
} |
|
} |
|
} |
|
"<del>" { |
|
#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 |
|
} |
|
"<vt>" { |
|
#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_to_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 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_to_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} { |
|
#REVIEW |
|
set next_gc [lindex $overlay_grapheme_control_list $gci+1] ;#next grapheme or control |
|
lassign $next_gc next_type next_item |
|
if {$autowrap_mode} { |
|
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_to_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 |
|
} elseif {0 && $next_type ne "g"} { |
|
incr idx_over -1 |
|
priv::render_this_unapplied $overlay_grapheme_control_list $gci ;# |
|
set instruction overflow |
|
break |
|
} else { |
|
#no point throwing back to caller for each grapheme that is overflowing |
|
#without this branch - renderline would be called with overtext reducing only by one grapheme per call |
|
#processing a potentially long overtext each time (ie - very slow) |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
#JMN4 |
|
|
|
} |
|
} |
|
} else { |
|
#review. |
|
#overflow_idx = -1 |
|
#This corresponds to expand_right 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 " " |
|
#tcl::dict::set understacks $idx [list] ;#review - use idx-1 codestack? |
|
#lset understacks $idx [list] ;#will get index $i out of range error |
|
lappend understacks [list] ;#REVIEW |
|
incr idx |
|
incr cursor_column |
|
} else { |
|
#todo - punk::char::char_width |
|
set g [lindex $outcols $idx] |
|
#JMN |
|
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 [tcl::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 [tcl::dict::get $overstacks $idx_over] [tcl::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} { |
|
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 |
|
} |
|
} |
|
} |
|
} ;# end switch |
|
|
|
|
|
} |
|
other - crmcontrol { |
|
if {$crm_mode && $type ne "crmcontrol" && $item ne "\x1b\[00001E"} { |
|
if {$item eq "\x1b\[3l"} { |
|
set crm_mode 0 |
|
} else { |
|
#When our initial overlay split was done - we weren't in crm_mode - so there are codes that weren't mapped to unicode control character representations |
|
#set within_undercols [expr {$idx <= $renderwidth-1}] |
|
#set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $item] |
|
set chars [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $item] |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
#prefix the unapplied controls with the string version of this control |
|
#set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]] |
|
#JMN |
|
ledit unapplied_list -1 -1 {*}[split $chars ""] |
|
set unapplied [join $unapplied_list ""] |
|
#ledit unapplied_ansisplit -1 -1 $chars |
|
lset unapplied_ansisplit 0 $chars ;#?? |
|
|
|
break |
|
} |
|
} |
|
|
|
#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 possibly therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore? |
|
set code [tcl::string::map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $item] |
|
#since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' |
|
|
|
|
|
#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? |
|
|
|
set c1 [tcl::string::index $code 0] |
|
set c1c2c3 [tcl::string::range $code 0 2] |
|
#set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} |
|
#tcl 8.7 - faster to use inline list than to store it in a local var outside of loop. |
|
#(somewhat surprising) |
|
set leadernorm [tcl::string::range [tcl::string::map [list\ |
|
\x1b\[< 1006\ |
|
\x1b\[ 7CSI\ |
|
\x1bY 7MAP\ |
|
\x1bP 7DCS\ |
|
\x90 8DCS\ |
|
\x9b 8CSI\ |
|
\x1b\] 7OSC\ |
|
\x9d 8OSC\ |
|
\x1b 7ESC\ |
|
] $c1c2c3] 0 3] ;#leadernorm is 1st 1,2 or 3 chars mapped to 4char normalised indicator - or is original first chars (1,2 or 3 len) |
|
|
|
#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 [tcl::string::cat $leadernorm [tcl::string::range $code 3 end]] |
|
} |
|
7CSI - 7OSC { |
|
#set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]] |
|
set codenorm $leadernorm[tcl::string::range $code 2 end] |
|
} |
|
7DCS { |
|
#ESC P |
|
#e.g sixel |
|
#Device Control String https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Controls-beginning-with-ESC:ESC-F.C74 |
|
set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]] |
|
} |
|
8DCS { |
|
#e.g sixel |
|
#8-bit Device Control String |
|
set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] |
|
} |
|
7MAP { |
|
#map to another type of code to share implementation branch |
|
set codenorm $leadernorm[tcl::string::range $code 1 end] |
|
} |
|
7ESC { |
|
#set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] |
|
set codenorm $leadernorm[tcl::string::range $code 1 end] |
|
} |
|
8CSI - 8OSC { |
|
set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] |
|
} |
|
default { |
|
#JMN |
|
puts stderr "Sequence detected as ANSI, but not handled in leadernorm switch. leadernorm: [ansistring VIEW -lf 1 $leadernorm] code: [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
puts stderr "ARGS:" |
|
foreach a $args { |
|
puts stderr " $a" |
|
} |
|
puts stderr ----- |
|
foreach {xpt ycode} $overmap { |
|
puts stderr "t:'$xpt'" |
|
puts stderr "c:[ansistring VIEW $ycode]" |
|
} |
|
#we haven't made a mapping for this |
|
#could in theory be 1,2 or 3 in len |
|
#although we shouldn't be getting here if the regexp for ansi codes is kept in sync with our switch branches |
|
set codenorm $code |
|
} |
|
} |
|
|
|
switch -- $leadernorm { |
|
7MAP { |
|
switch -- [lindex $codenorm 4] { |
|
Y { |
|
#vt52 movement. we expect 2 chars representing position (limited range) |
|
set params [tcl::string::range $codenorm 5 end] |
|
if {[tcl::string::length $params] != 2} { |
|
#shouldn't really get here or need this branch if ansi splitting was done correctly |
|
puts stderr "overtype::renderline ESC Y recognised as vt52 move, but incorrect parameters length ([string length $params] vs expected 2) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]" |
|
} |
|
set line [tcl::string::index $params 5] |
|
set column [tcl::string::index $params 1] |
|
set r [expr {[scan $line %c] -31}] |
|
set c [expr {[scan $column %c] -31}] |
|
|
|
#MAP to: |
|
#CSI n;m H - CUP - Cursor Position |
|
set leadernorm 7CSI |
|
set codenorm "$leadernorm${r}\;${c}H" |
|
} |
|
} |
|
} |
|
} |
|
|
|
#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 -- [tcl::string::index $codenorm end] { |
|
M { |
|
puts stderr "mousedown $codenorm" |
|
} |
|
m { |
|
puts stderr "mouseup $codenorm" |
|
} |
|
} |
|
|
|
} |
|
{7CSI} - {8CSI} { |
|
set param [tcl::string::range $codenorm 4 end-1] |
|
#puts stdout "--> CSI [tcl::string::index $leadernorm 0] bit param:$param" |
|
set code_end [tcl::string::index $codenorm end] ;#used for e.g h|l set/unset mode |
|
|
|
switch -exact -- $code_end { |
|
A { |
|
#Row move - up |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
#todo |
|
lassign [split $param {;}] num modifierkey |
|
if {$modifierkey ne ""} { |
|
puts stderr "modifierkey:$modifierkey" |
|
} |
|
|
|
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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction up |
|
#retain cursor_column |
|
break |
|
} |
|
B { |
|
#CUD - Cursor Down |
|
#Row move - down |
|
lassign [split $param {;}] num modifierkey |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
#move down |
|
if {$modifierkey ne ""} { |
|
puts stderr "modifierkey:$modifierkey" |
|
} |
|
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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction down |
|
#retain cursor_column |
|
break |
|
} |
|
C { |
|
#CUF - Cursor Forward |
|
#Col move |
|
#puts stdout "->forward" |
|
#todo - consider right-to-left cursor mode (e.g Hebrew).. some day. |
|
#cursor forward |
|
#right-arrow/move forward |
|
lassign [split $param {;}] num modifierkey |
|
if {$modifierkey ne ""} { |
|
puts stderr "modifierkey:$modifierkey" |
|
} |
|
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 {$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_right and 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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction wrapmoveforward |
|
break |
|
} else { |
|
set cursor_column $max |
|
set idx [expr {$cursor_column -1}] |
|
} |
|
} |
|
} else { |
|
#review - dead branch |
|
if {!$expand_right || ($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 {[tcl::dict::exists $understacks $idx]} { |
|
# set stackinfo [tcl::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 [tcl::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}] |
|
} |
|
} |
|
} |
|
} |
|
} |
|
D { |
|
#Col move |
|
#puts stdout "<-back" |
|
#cursor back |
|
#left-arrow/move-back when ltr mode |
|
lassign [split $param {;}] num modifierkey |
|
if {$modifierkey ne ""} { |
|
puts stderr "modifierkey:$modifierkey" |
|
} |
|
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_to_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 |
|
} |
|
} |
|
} |
|
E { |
|
#CNL - Cursor Next Line |
|
if {$param eq ""} { |
|
set downmove 1 |
|
} else { |
|
set downmove [expr {$param}] |
|
} |
|
puts stderr "renderline CNL down-by-$downmove" |
|
set cursor_column 1 |
|
set cursor_row [expr {$cursor_row + $downmove}] |
|
set idx [expr {$cursor_column -1}] |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
incr idx_over |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction move |
|
break |
|
|
|
} |
|
F { |
|
#CPL - Cursor Previous Line |
|
if {$param eq ""} { |
|
set upmove 1 |
|
} else { |
|
set upmove [expr {$param}] |
|
} |
|
puts stderr "renderline CPL up-by-$upmove" |
|
set cursor_column 1 |
|
set cursor_row [expr {$cursor_row -$upmove}] |
|
if {$cursor_row < 1} { |
|
set cursor_row 1 |
|
} |
|
set idx [expr {$cursor_column - 1}] |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
incr idx_over |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction move |
|
break |
|
|
|
} |
|
G { |
|
#CHA - Cursor Horizontal Absolute (move to absolute column no) |
|
if {$param eq ""} { |
|
set targetcol 1 |
|
} else { |
|
set targetcol $param |
|
if {![string is integer -strict $targetcol]} { |
|
puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Unrecognised parameter '$param'" |
|
} |
|
set targetcol [expr {$param}] |
|
set max [llength $outcols] |
|
if {$overflow_idx == -1} { |
|
incr max |
|
} |
|
if {$targetcol > $max} { |
|
puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Param '$param' > max: $max" |
|
set targetcol $max |
|
} |
|
} |
|
#adjust to colstart - as column 1 is within overlay |
|
#??? REVIEW |
|
set idx [expr {($targetcol -1) + $opt_colstart -1}] |
|
|
|
set cursor_column $targetcol |
|
#puts stderr "renderline absolute col move ESC G (TEST)" |
|
} |
|
H - f { |
|
#CSI n;m H - CUP - Cursor Position |
|
|
|
#CSI n;m f - HVP - Horizontal Vertical Position REVIEW - same as CUP with differences (what?) in some terminal modes |
|
# - 'counts as effector format function (like CR or LF) rather than an editor function (like CUD or CNL)' |
|
# - REVIEW |
|
#see Annex A at: https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf |
|
|
|
#test e.g ansicat face_2.ans |
|
#$re_both_move |
|
lassign [split $param {;}] paramrow paramcol |
|
#missing defaults to 1 |
|
#CSI ;5H = CSI 1;5H -> row 1 col 5 |
|
#CSI 17;H = CSI 17H = CSI 17;1H -> row 17 col 1 |
|
|
|
if {$paramcol eq ""} {set paramcol 1} |
|
if {$paramrow eq ""} {set paramrow 1} |
|
if {![string is integer -strict $paramcol] || ![string is integer -strict $paramrow]} { |
|
puts stderr "renderline CUP (CSI <param> H) unrecognised param $param" |
|
#ignore? |
|
} else { |
|
set max [llength $outcols] |
|
if {$overflow_idx == -1} { |
|
incr max |
|
} |
|
if {$paramcol > $max} { |
|
set target_column $max |
|
} else { |
|
set target_column [expr {$paramcol}] |
|
} |
|
|
|
|
|
if {$paramrow < 1} { |
|
puts stderr "renderline CUP (CSI <param> H) bad row target 0. Assuming 1" |
|
set target_row 1 |
|
} else { |
|
set target_row [expr {$paramrow}] |
|
} |
|
if {$target_row == $cursor_row} { |
|
#col move only - no need for break and move |
|
#puts stderr "renderline CUP col move only to col $target_column param:$param" |
|
set cursor_column $target_column |
|
set idx [expr {$cursor_column -1}] |
|
} else { |
|
set cursor_row $target_row |
|
set cursor_column $target_column |
|
set idx [expr {$cursor_column -1}] |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
incr idx_over |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction move |
|
break |
|
} |
|
} |
|
} |
|
J { |
|
set modegroup [tcl::string::index $codenorm 4] ;#e.g ? |
|
switch -exact -- $modegroup { |
|
? { |
|
#CSI ? Pn J - selective erase |
|
puts stderr "overtype::renderline ED - SELECTIVE ERASE IN DISPLAY (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
default { |
|
puts stderr "overtype::renderline ED - ERASE IN DISPLAY (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
if {$param eq ""} {set param 0} |
|
switch -exact -- $param { |
|
0 { |
|
#clear from cursor to end of screen |
|
} |
|
1 { |
|
#clear from cursor to beginning of screen |
|
} |
|
2 { |
|
#clear entire screen |
|
#ansi.sys - move cursor to upper left REVIEW |
|
set cursor_row 1 |
|
set cursor_column 1 |
|
set idx [expr {$cursor_column -1}] |
|
set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] |
|
incr idx_over |
|
if {[llength $outcols]} { |
|
priv::render_erasechar 0 [llength $outcols] |
|
} |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction clear_and_move |
|
break |
|
} |
|
3 { |
|
#clear entire screen. presumably cursor doesn't move - otherwise it would be same as 2J ? |
|
|
|
} |
|
default { |
|
} |
|
} |
|
|
|
} |
|
} |
|
} |
|
K { |
|
#see DECECM regarding background colour |
|
set modegroup [tcl::string::index $codenorm 4] ;#e.g ? |
|
switch -exact -- $modegroup { |
|
? { |
|
puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
set param [string range $param 1 end] ;#chop qmark |
|
if {$param eq ""} {set param 0} |
|
switch -exact -- $param { |
|
0 { |
|
#clear from cursor to end of line - depending on DECSCA |
|
} |
|
1 { |
|
#clear from cursor to beginning of line - depending on DECSCA |
|
|
|
} |
|
2 { |
|
#clear entire line - depending on DECSCA |
|
} |
|
default { |
|
puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
|
|
} |
|
default { |
|
puts stderr "overtype::renderline EL - ERASE IN LINE (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
if {$param eq ""} {set param 0} |
|
switch -exact -- $param { |
|
0 { |
|
#clear from cursor to end of line |
|
} |
|
1 { |
|
#clear from cursor to beginning of line |
|
|
|
} |
|
2 { |
|
#clear entire line |
|
} |
|
default { |
|
puts stderr "overtype::renderline EL - ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
} |
|
} |
|
} |
|
L { |
|
puts stderr "overtype::renderline IL - Insert Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
M { |
|
#CSI Pn M - DL - Delete Line |
|
puts stderr "overtype::renderline DL - Delete Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
|
|
} |
|
T { |
|
#CSI Pn T - SD Pan Up (empty lines introduced at top) |
|
#CSI Pn+T - kitty extension (lines at top come from scrollback buffer) |
|
#Pn new lines appear at top of the display, Pn old lines disappear at the bottom of the display |
|
if {$param eq "" || $param eq "0"} {set param 1} |
|
if {[string index $param end] eq "+"} { |
|
puts stderr "overtype::renderline CSI Pn + T - kitty Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} else { |
|
puts stderr "overtype::renderline CSI Pn T - SD Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
X { |
|
puts stderr "overtype::renderline X ECH ERASE CHARACTER - $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. |
|
} |
|
q { |
|
set code_secondlast [tcl::string::index $codenorm end-1] |
|
switch -exact -- $code_secondlast { |
|
{"} { |
|
#DECSCA - Select Character Protection Attribute |
|
#(for use with selective erase: DECSED and DECSEL) |
|
set param [tcl::string::range $codenorm 4 end-2] |
|
if {$param eq ""} {set param 0} |
|
#TODO - store like SGR in stacks - replays? |
|
switch -exact -- $param { |
|
0 - 2 { |
|
#canerase |
|
puts stderr "overtype::renderline - DECSCA canerase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
1 { |
|
#cannoterase |
|
puts stderr "overtype::renderline - DECSCA cannoterase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
default { |
|
puts stderr "overtype::renderline DECSCA param '$param' not understood [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
|
|
} |
|
default { |
|
puts stderr "overtype::renderline - CSI ... q not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
|
|
} |
|
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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction move ;#own instruction? decstbm? |
|
break |
|
} |
|
s { |
|
#code conflict between ansi emulation and DECSLRM - REVIEW |
|
#ANSISYSSC (when no parameters) - like other terminals - essentially treat same as DECSC |
|
# todo - when parameters - support DECSLRM instead |
|
|
|
if {$param ne ""} { |
|
#DECSLRM - should only be recognised if DECLRMM is set (vertical split screen mode) |
|
lassign [split $param {;}] margin_left margin_right |
|
puts stderr "overtype DECSLRM not yet supported - got [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
if {$margin_left eq ""} { |
|
set margin_left 1 |
|
} |
|
set columns_per_page 80 ;#todo - set to 'page width (DECSCPP set columns per page)' - could be 132 or?? |
|
if {$margin_right eq ""} { |
|
set margin_right $columns_per_page |
|
} |
|
puts stderr "DECSLRM margin left: $margin_left margin right: $margin_right" |
|
if {![string is integer -strict $margin_left] || $margin_left < 0} { |
|
puts stderr "DECSLRM invalid margin_left" |
|
} |
|
if {![string is integer -strict $margin_right] || $margin_right < 0} { |
|
puts stderr "DECSLRM invalid margin_right" |
|
} |
|
set scrolling_region_size [expr {$margin_right - $margin_left}] |
|
if {$scrolling_region_size < 2 || $scrolling_region_size > $columns_per_page} { |
|
puts stderr "DECSLRM region size '$scrolling_regsion_size' must be between 1 and $columns_per_page" |
|
} |
|
#todo |
|
|
|
|
|
} else { |
|
#DECSC |
|
#//notes on expected behaviour: |
|
#DECSC - saves following items in terminal's memory |
|
#cursor position |
|
#character attributes set by the SGR command |
|
#character sets (G0,G1,G2 or G3) currently in GL and GR |
|
#Wrap flag (autowrap or no autowrap) |
|
#State of origin mode (DECOM) |
|
#selective erase attribute |
|
#any single shift 2 (SS2) or single shift 3(SSD) functions sent |
|
|
|
#$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 { |
|
#ANSISYSRC save cursor (when no parameters) (DECSC) |
|
|
|
#$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_to_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code |
|
incr idx_over |
|
|
|
set unapplied "" |
|
set unapplied_list [list] |
|
set unapplied_ansisplit [list ""] ;#remove below if nothing added |
|
foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { |
|
lassign $gc type item |
|
switch -- $type { |
|
g { |
|
lappend unapplied_list $item |
|
ledit unapplied_ansisplit end end [string cat [lindex $unapplied_ansisplit end] $item] |
|
} |
|
gx0 { |
|
if {$item eq "gx0_on"} { |
|
lappend unapplied_list "\x1b(0" |
|
lappend unapplied_ansisplit "\x1b(0" "" |
|
} elseif {$item eq "gx0_off"} { |
|
lappend unapplied_list "\x1b(B" |
|
lappend unapplied_ansisplit "\x1b(B" "" |
|
} |
|
} |
|
default { |
|
lappend unapplied_list $item |
|
lappend unapplied_ansisplit $item "" |
|
} |
|
} |
|
#incr idx_over |
|
} |
|
set unapplied [join $unapplied_list ""] |
|
if {[llength $unapplied_ansisplit] == 1 && [lindex $unapplied_ansisplit 0] eq ""} { |
|
set unapplied_ansisplit [list] |
|
} |
|
#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 |
|
} |
|
"{" { |
|
|
|
puts stderr "renderline warning - CSI..<leftcurly> - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" |
|
} |
|
"}" { |
|
set code_secondlast [tcl::string::index $codenorm end-1] |
|
switch -exact -- $code_secondlast { |
|
' { |
|
puts stderr "renderline warning - DECIC - Insert Column - CSI...'<rightcurly> - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" |
|
} |
|
default { |
|
puts stderr "renderline warning - CSI..<rightcurly> - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" |
|
} |
|
} |
|
} |
|
~ { |
|
set code_secondlast [tcl::string::index $codenorm end-1] ;#used for e.g CSI x '~ |
|
switch -exact -- $code_secondlast { |
|
' { |
|
#DECDC - editing sequence - Delete Column |
|
puts stderr "renderline warning - DECDC - unimplemented" |
|
} |
|
default { |
|
#$re_vt_sequence |
|
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 { |
|
#set mode unset mode |
|
#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=<mode>h|l - not seen on windows terminals.. review |
|
#e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html |
|
set modegroup [tcl::string::index $codenorm 4] ;#e.g ? = |
|
switch -exact -- $modegroup { |
|
? { |
|
set smparams [tcl::string::range $codenorm 5 end-1] ;#params between ? and h|l |
|
#one or more modes can be set |
|
set smparam_list [split $smparams {;}] |
|
foreach num $smparam_list { |
|
switch -- $num { |
|
"" { |
|
#ignore empties e.g extra/trailing semicolon in params |
|
} |
|
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 |
|
|
|
if {$code_end eq "h"} { |
|
#set (enable) |
|
set reverse_mode 1 |
|
} else { |
|
#reset (disable) |
|
set reverse_mode 0 |
|
} |
|
|
|
} |
|
7 { |
|
#DECAWM autowrap |
|
if {$code_end 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 usually - but sanity check with warning for now. |
|
if {$idx >= $overflow_idx} { |
|
puts stderr "renderline warning - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected" |
|
} |
|
} else { |
|
#reset (disable) |
|
set autowrap_mode 0 |
|
#REVIEW! |
|
set overflow_idx -1 |
|
} |
|
} |
|
25 { |
|
if {$code_end eq "h"} { |
|
#visible cursor |
|
|
|
} else { |
|
#invisible cursor |
|
|
|
} |
|
} |
|
117 { |
|
#DECECM - Erase Color Mode |
|
#https://invisible-island.net/ncurses/ncurses.faq.html |
|
#The Erase color selection controls the background color used when text is erased or new |
|
#text is scrolled on to the screen. Screen background causes newly erased areas or |
|
#scrolled text to be written using color index zero, the screen background. This is VT |
|
#and DECterm compatible. Text background causes erased areas or scrolled text to be |
|
#written using the current text background color. This is PC console compatible and is |
|
#the factory default. |
|
|
|
#see also: https://unix.stackexchange.com/questions/251726/clear-to-end-of-line-uses-the-wrong-background-color-in-screen |
|
} |
|
} |
|
} |
|
} |
|
= { |
|
set num [tcl::string::range $codenorm 5 end-1] ;#param between = and h|l |
|
puts stderr "overtype::renderline CSI=...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" |
|
} |
|
default { |
|
#e.g CSI 4 h |
|
set num [tcl::string::range $codenorm 4 end-1] ;#param before h|l |
|
switch -exact -- $num { |
|
3 { |
|
puts stderr "CRM MODE $code_end" |
|
#CRM - Show control character mode |
|
# 'No control functions are executed except LF,FF and VT which are represented in the CRM FONT before a CRLF(new line) is executed' |
|
# |
|
#use ansistring VIEW -nul 1 -lf 2 -ff 2 -vt 2 |
|
#https://vt100.net/docs/vt510-rm/CRM.html |
|
#NOTE - vt100 CRM always does auto-wrap at right margin. |
|
#disabling auto-wrap in set-up or by sequence is disabled. |
|
#We should default to turning off auto-wrap when crm_mode enabled.. but |
|
#displaying truncated (on rhs) crm can still be very useful - and we have optimisation in overflow to avoid excess renderline calls (per grapheme) |
|
#we therefore could reasonably put in an exception to allow auto_wrap to be disabled after crm_mode is engaged, |
|
#although this would be potentially an annoying difference to some.. REVIEW |
|
if {$code_end eq "h"} { |
|
set crm_mode 1 |
|
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 |
|
} |
|
} else { |
|
set crm_mode 0 |
|
} |
|
} |
|
4 { |
|
#IRM - Insert/Replace Mode |
|
if {$code_end eq "h"} { |
|
#CSI 4 h |
|
set insert_mode 1 |
|
} else { |
|
#CSI 4 l |
|
#replace mode |
|
set insert_mode 0 |
|
} |
|
} |
|
default { |
|
puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" |
|
} |
|
} |
|
} |
|
} |
|
} |
|
| { |
|
switch -- [tcl::string::index $codenorm end-1] { |
|
{$} { |
|
#CSI ... $ | DECSCPP set columns per page- (recommended in vt510 docs as preferable to DECCOLM) |
|
#real terminals generally only supported 80/132 |
|
#some other virtuals support any where from 2 to 65,536? |
|
#we will allow arbitrary widths >= 2 .. to some as yet undetermined limit. |
|
#CSI <param> $ | |
|
#empty or 0 param is 80 for compatibility - other numbers > 2 accepted |
|
set page_width -1 ;#flag as unset |
|
if {$param eq ""} { |
|
set page_width 80 |
|
} elseif {[string is integer -strict $param] && $param >=2} { |
|
set page_width [expr {$param}] ;#we should allow leading zeros in the number - but lets normalize using expr |
|
} else { |
|
puts stderr "overtype::renderline unacceptable DECSPP value '$param'" |
|
} |
|
|
|
if {$page_width > 2} { |
|
puts stderr "overtype::renderline DECSCPP - not implemented - but selected width '$page_width' looks ok" |
|
#if cursor already beyond new page_width - will move to right colum - otherwise no cursor movement |
|
|
|
} |
|
|
|
} |
|
default { |
|
puts stderr "overtype::renderline unrecognised CSI code ending in pipe (|) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" |
|
} |
|
} |
|
} |
|
t { |
|
set params [split $param {;}] |
|
if {[llength $params] == 4} { |
|
#pablodraw 24bit color |
|
#see also: https://github.com/ansilove/libansilove/blob/master/src/loaders/ansi.c |
|
lassign $params isfg pablo_r pablo_g pablo_b |
|
#e.g esc\[0\;171\;87\;0t |
|
set stack [lindex $overlay_grapheme_control_stacks $gci] |
|
puts stderr "pablodraw debug [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
#foreach s $stack { |
|
# puts stderr " - [ansistring VIEW -lf 1 -vt 1 -nul 1 $s]" |
|
#} |
|
#we expect first param to be 0 for background, 1 for foreground |
|
if {$isfg} { |
|
set rgbcode "\x1b\[38\;2\;$pablo_r\;$pablo_g\;${pablo_b}m" |
|
} else { |
|
set rgbcode "\x1b\[48\;2\;$pablo_r\;$pablo_g\;${pablo_b}m" |
|
} |
|
#too late here !! |
|
#lappend stack $rgbcode |
|
#lset overlay_grapheme_control_stacks $gci $stack |
|
|
|
} else { |
|
puts stderr "overtype::renderline unrecognised custom CSI 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 {\x1b(D|M|E)$} |
|
#also vt52 Y.. |
|
#also PM \x1b^...(ST) |
|
switch -- [tcl::string::index $codenorm 4] { |
|
c { |
|
#RIS - reset terminal to initial state - where 'terminal' in this case is the renderspace - not the underlying terminal! |
|
puts stderr "renderline reset" |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction reset |
|
break |
|
} |
|
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 "renderline ESC D not fully implemented" |
|
incr cursor_row |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction down |
|
#retain cursor_column |
|
break |
|
} |
|
E { |
|
#\x85 |
|
#review - is behaviour different to lf? |
|
#todo - possibly(?) same logic as <lf> handling above. i.e return instruction depends on where column_cursor is at the time we get NEL |
|
#leave implementation until logic for <lf> 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 "overtype::renderline ESC E unimplemented" |
|
|
|
} |
|
H { |
|
#\x88 |
|
#Tab Set |
|
puts stderr "overtype::renderline ESC H tab set unimplemented" |
|
} |
|
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 "overtype::renderline 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_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction up ;#need instruction for scroll-down? |
|
#retain cursor_column |
|
break |
|
} |
|
N { |
|
#\x8e - affects next character only |
|
puts stderr "overtype::renderline single shift select G2 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
O { |
|
#\x8f - affects next character only |
|
puts stderr "overtype::renderline single shift select G3 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
P { |
|
#\x90 |
|
#DCS - shouldn't get here - handled in 7DCS branch |
|
#similarly \] OSC (\x9d) and \\ (\x9c) ST |
|
} |
|
V { |
|
#\x96 |
|
|
|
} |
|
W { |
|
#\x97 |
|
} |
|
X { |
|
#\x98 |
|
#SOS |
|
if {[string index $code end] eq "\007"} { |
|
set sos_content [string range $code 2 end-1] ;#ST is \007 |
|
} else { |
|
set sos_content [string range $code 2 end-2] ;#ST is \x1b\\ |
|
} |
|
#return in some useful form to the caller |
|
#TODO! |
|
lappend sos_list [list string $sos_content row $cursor_row column $cursor_column] |
|
puts stderr "overtype::renderline ESCX SOS UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
^ { |
|
#puts stderr "renderline PM" |
|
#Privacy Message. |
|
if {[string index $code end] eq "\007"} { |
|
set pm_content [string range $code 2 end-1] ;#ST is \007 |
|
} else { |
|
set pm_content [string range $code 2 end-2] ;#ST is \x1b\\ |
|
} |
|
#We don't want to render it - but we need to make it available to the application |
|
#see the textblock library in punk, for the exception we make here for single backspace. |
|
#It is unlikely to be encountered as a useful PM - so we hack to pass it through as a fix |
|
#for spacing issues on old terminals which miscalculate the single-width 'Symbols for Legacy Computing' |
|
if {$pm_content eq "\b"} { |
|
#puts stderr "renderline PM sole backspace special handling for \U1FB00 - \U1FBFF" |
|
#esc^\b\007 or esc^\besc\\ |
|
#HACKY pass-through - targeting terminals that both mis-space legacy symbols *and* don't support PMs |
|
#The result is repair of the extra space. If the terminal is a modern one and does support PM - the \b should be hidden anyway. |
|
#If the terminal has the space problem AND does support PMs - then this just won't fix it. |
|
#The fix relies on the symbol-supplier to cooperate by appending esc^\b\esc\\ to the problematic symbols. |
|
|
|
#priv::render_addchar $idx $code [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode |
|
#idx has been incremented after last grapheme added |
|
priv::render_append_to_char [expr {$idx -1}] $code |
|
} |
|
#lappend to a dict element in the result for application-specific processing |
|
lappend pm_list $pm_content |
|
} |
|
_ { |
|
#APC Application Program Command |
|
#just warn for now.. |
|
puts stderr "overtype::renderline ESC_ APC command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
default { |
|
puts stderr "overtype::renderline ESC<x> code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]" |
|
} |
|
} |
|
|
|
} |
|
7DCS - 8DCS { |
|
#match 'DCS P1 ; P2 ; P3' (without spaces) |
|
# where Ps1,P2,P3 are all optional and P1,P2 are single digit and P3 can *technically* be any positive integer but is usually ignored (commonly set to zero) |
|
# Our regexp isn't precise as we will validate number of params and values after matching - but we will assume P3 should be small (review for micrometres - could be 4 digits? more?) |
|
# (limit to 10 chars to avoid insane values?) |
|
#https://github.com/hackerb9/vt340test/blob/main/physicalsixels.md |
|
# P1P2P3q - "Protocol Selector" |
|
# P1 - Pixel Aspect Ratio (Vertical:Horizontal) |
|
# P2 - background control |
|
# P3 - horizontal grid size (default units decipoints 1/720 inch - but theoretically controlled by ANSI SSU sequence) |
|
# P1P2P3 commonly omitted - with subsequent <DQ>P4;P5;P6;P7 "Raster Attributes (DECGRA)" being used for: |
|
# Aspect Ratio (P4,P5) |
|
set sixelstart [tcl::string::range $codenorm 4 13] |
|
set sixelmatch [regexp -all -inline {^((?:[0-9]*;){0,2}(?:[0-9]*))q} $sixelstart] |
|
if {[llength $sixelmatch] == 2} { |
|
#sixel |
|
#note sixel data can have newlines before ST |
|
set sixelparams [lindex $sixelmatch 1] |
|
set params [split $sixelparams {;}] |
|
set badsixelparams 0 |
|
if {[llength $params] > 3} { |
|
set badsixelparams 1 |
|
} |
|
lassign $params P1 P2 P3 |
|
if {[string length $P1] > 1 || [string length $P2] > 1 || [string length $P3] > 3} { |
|
set badsixelparams 1 |
|
} |
|
if {$badsixelparams} { |
|
puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING looks like sixel, but bad params. sixelstart [ansistring VIEW -lf 1 -vt 1 -nul 1 $sixelstart]" |
|
} else { |
|
#todo - move to punk::sixel library |
|
|
|
#P1 - Pixel Aspect Ratio |
|
# round(10/P1):1 if 2<= P1 <= 9) 2:1 otherwise |
|
# omitted 2:1 (default) |
|
# 0,1 2:1 |
|
# 2 5:1 |
|
# 3,4 3:1 |
|
# 5,6 2:1 |
|
# 7,8,9 1:1 |
|
switch -- $P1 { |
|
"" - 0 - 1 { |
|
#omitted (default) |
|
set sixel_pixel_aspect "2:1" |
|
} |
|
2 { |
|
set sixel_pixel_aspect "5:1" |
|
} |
|
3 - 4 { |
|
set sixel_pixel_aspect "3:1" |
|
} |
|
5 - 6 { |
|
set sixel_pixel_aspect "2:1" |
|
} |
|
7 - 8 - 9 { |
|
set sixel_pixel_aspect "1:1" |
|
} |
|
default { |
|
set sixel_pixel_aspect "invalid" |
|
puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING looks like sixel, but unrecognised P1 (pixel aspect ratio). sixelstart [ansistring VIEW -lf 1 -vt 1 -nul 1 $sixelstart]" |
|
} |
|
} |
|
|
|
#P2 - background colour |
|
# 0,2 (default) pixel positions specified as 0 are set to current bg colour |
|
# 1 pixel positions specified as 0 remain at current colour |
|
switch -- $P2 { |
|
"" - 0 - 2 { |
|
set sixel_background "current_background" |
|
} |
|
1 { |
|
set sixel_background "transparent" |
|
} |
|
default { |
|
set sixel_background "invalid" |
|
puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING looks like sixel, but unrecognised P2 (background control). sixelstart [ansistring VIEW -lf 1 -vt 1 -nul 1 $sixelstart]" |
|
} |
|
} |
|
|
|
|
|
#P3 horizontal grid size - ignored on VT300 - commonly set to zero |
|
|
|
# ECMA-48 SSU (ESC Ps <sp> I) |
|
# 0 - CHARACTER |
|
# 1 - MILLIMETRE |
|
# 2 - COMPUTER DECIPOINT 0.03528mm 1/720 of 25.4mm) |
|
# 3 - DECIDIDOT 0.03759mm (10/266mm) |
|
# 4 - MIL 0.0254mm (1/1000 of 25.4mm) |
|
# 5 - BASIC MEASURING UNIT (BMU) 0.02117mm (1/1200 of 25.4mm) |
|
# 6 - MICROMETRE 0.001mm |
|
# 7 - PIXEL - the smallest increment that can be specified in a device |
|
# 8 - DECIPOINT - 0.03514mm (35/996mm) |
|
set sixel_horizontal_grid $P3 |
|
set sixel_ssu "decipoint" ;#todo? |
|
|
|
#todo - look for and parse DECGRA introduced by double quote |
|
puts stderr "overtype::renderline SIXEL aspect: $sixel_pixel_aspect bg: $sixel_background hgrid: $sixel_horizontal_grid. sixelstart [ansistring VIEW -lf 1 -vt 1 -nul 1 $sixelstart]" |
|
|
|
|
|
#todo |
|
} |
|
} else { |
|
puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
#ST (string terminator) \x9c or \x1b\\ |
|
if {[tcl::string::index $codenorm end] eq "\x9c"} { |
|
set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is 8-bit 0x9c |
|
} else { |
|
set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\ |
|
} |
|
} |
|
} |
|
7OSC - 8OSC { |
|
# OSCs are terminated with ST of either \007 or \x1b\\ - we allow either whether code was 7 or 8 bit |
|
if {[tcl::string::index $codenorm end] eq "\007"} { |
|
set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is \007 |
|
} else { |
|
set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\ |
|
} |
|
set first_colon [tcl::string::first {;} $code_content] |
|
if {$first_colon == -1} { |
|
#there probably should always be a colon - but we'll try to make sense of it without |
|
set osc_code $code_content ;#e.g \x1b\]104\007 vs \x1b\]104\;\007 |
|
} else { |
|
set osc_code [tcl::string::range $code_content 0 $first_colon-1] |
|
} |
|
switch -exact -- $osc_code { |
|
2 { |
|
set newtitle [tcl::string::range $code_content 2 end] |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction [list set_window_title $newtitle] |
|
break |
|
} |
|
4 { |
|
#OSC 4 - set colour palette |
|
#can take multiple params |
|
#e.g \x1b\]4\;1\;red\;2\;green\x1b\\ |
|
set params [tcl::string::range $code_content 2 end] ;#strip 4 and first semicolon |
|
set cmap [dict create] |
|
foreach {cnum spec} [split $params {;}] { |
|
if {$cnum >= 0 && $cnum <= 255} { |
|
#todo - parse spec from names like 'red' to RGB |
|
#todo - accept rgb:ab/cd/ef as well as rgb:/a/b/c (as alias for aa/bb/cc) |
|
#also - what about rgb:abcd/defg/hijk and 12-bit abc/def/ghi ? |
|
dict set cmap $cnum $spec |
|
} else { |
|
#todo - log |
|
puts stderr "overtype::renderline OSC 4 set colour palette - bad color number: $cnum must be from 0 to 255. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
|
|
puts stderr "overtype::renderline OSC 4 set colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
|
|
|
|
} |
|
10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 { |
|
#OSC 10 through 17 - so called 'dynamic colours' |
|
#can take multiple params - each successive parameter changes the next colour in the list |
|
#- e.g if code started at 11 - next param is for 12. 17 takes only one param because there are no more |
|
#10 change text foreground colour |
|
#11 change text background colour |
|
#12 change text cursor colour |
|
#13 change mouse foreground colour |
|
#14 change mouse background colour |
|
#15 change tektronix foreground colour |
|
#16 change tektronix background colour |
|
#17 change highlight colour |
|
set params [tcl::string::range $code_content 2 end] |
|
|
|
puts stderr "overtype::renderline OSC $osc_code set dynamic colours unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
|
|
|
|
} |
|
18 { |
|
#why is this not considered one of the dynamic colours above? |
|
#https://www.xfree86.org/current/ctlseqs.html |
|
#tektronix cursor color |
|
puts stderr "overtype::renderline OSC 18 - set tektronix cursor color unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
99 { |
|
#kitty desktop notifications |
|
#https://sw.kovidgoyal.net/kitty/desktop-notifications/ |
|
#<OSC> 99 ; metadata ; payload <terminator> |
|
puts stderr "overtype::renderline OSC 99 kitty desktop notification unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
104 { |
|
#reset colour palette |
|
#we want to do it for the current rendering context only (vvt) - not just pass through to underlying vt |
|
puts stderr "overtype::renderline OSC 104 reset colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
priv::render_to_unapplied $overlay_grapheme_control_list $gci |
|
set instruction [list reset_colour_palette] |
|
break |
|
} |
|
1337 { |
|
#iterm2 graphics and file transfer |
|
puts stderr "overtype::renderline OSC 1337 iterm2 graphics/file_transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]" |
|
} |
|
5113 { |
|
puts stderr "overtype::renderline OSC 5113 kitty file transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]" |
|
} |
|
default { |
|
puts stderr "overtype::renderline OSC - UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" |
|
} |
|
} |
|
|
|
} |
|
default { |
|
} |
|
} |
|
|
|
|
|
} |
|
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_expand_right == 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 |
|
} |
|
set trailing_nulls 0 |
|
foreach ch [lreverse $outcols] { |
|
if {$ch eq "\u0000"} { |
|
incr trailing_nulls |
|
} else { |
|
break |
|
} |
|
} |
|
if {$trailing_nulls} { |
|
set first_tail_null_posn [expr {[llength $outcols] - $trailing_nulls}] |
|
} else { |
|
set first_tail_null_posn -1 |
|
} |
|
|
|
#puts stderr "first_tail_null_posn: $first_tail_null_posn" |
|
#puts stderr "colview: [ansistring VIEW $outcols]" |
|
|
|
foreach ch $outcols { |
|
#puts "---- [ansistring VIEW $ch]" |
|
|
|
set gxleader "" |
|
if {$i < [llength $understacks_gx]} { |
|
#set g0 [tcl::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 [tcl::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 {$ch eq "\u0000"} { |
|
if {$cp437_glyphs} { |
|
#map all nulls including at tail to space |
|
append outstring " " |
|
} else { |
|
if {$trailing_nulls && $i < $first_tail_null_posn} { |
|
append outstring " " ;#map inner nulls to space |
|
} else { |
|
append outstring \u0000 |
|
} |
|
} |
|
} else { |
|
append outstring $ch |
|
} |
|
} |
|
incr i |
|
} |
|
#flower.ans good test for null handling - reverse line building |
|
#review - presence of overflow_right doesn't indicate line's trailing nulls should remain. |
|
#The cells could have been erased? |
|
#if {!$cp437_glyphs} { |
|
# #if {![ansistring length $overflow_right]} { |
|
# # set outstring [tcl::string::trimright $outstring "\u0000"] |
|
# #} |
|
# set outstring [tcl::string::trimright $outstring "\u0000"] |
|
# set outstring [tcl::string::map {\u0000 " "} $outstring] |
|
#} |
|
|
|
|
|
#REVIEW |
|
#set overflow_right [tcl::string::trimright $overflow_right "\u0000"] |
|
#set overflow_right [tcl::string::map {\u0000 " "} $overflow_right] |
|
|
|
set replay_codes "" |
|
if {[llength $understacks] > 0} { |
|
if {$overflow_idx == -1} { |
|
#set tail_idx [tcl::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] ;#without this - we would get for example, trailing backgrounds after rightmost column |
|
|
|
#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}] |
|
} |
|
if {[llength $unapplied_ansisplit] == 1 && [lindex $unapplied_ansisplit 0] eq ""} { |
|
set unapplied_ansisplit [list] |
|
} |
|
|
|
set result [tcl::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\ |
|
unapplied_list $unapplied_list\ |
|
unapplied_ansisplit $unapplied_ansisplit\ |
|
insert_mode $insert_mode\ |
|
autowrap_mode $autowrap_mode\ |
|
crm_mode $crm_mode\ |
|
reverse_mode $reverse_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\ |
|
expand_right $opt_expand_right\ |
|
replay_codes $replay_codes\ |
|
replay_codes_underlay $replay_codes_underlay\ |
|
replay_codes_overlay $replay_codes_overlay\ |
|
pm_list $pm_list\ |
|
] |
|
if {$opt_returnextra == 1} { |
|
#puts stderr "renderline: $result" |
|
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. |
|
} |
|
} |
|
tcl::dict::set result result [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result result]] |
|
tcl::dict::set result overflow_right [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result overflow_right]] |
|
tcl::dict::set result unapplied [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied]] |
|
tcl::dict::set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied_list]] |
|
tcl::dict::set result unapplied_ansisplit [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied_ansisplit]] |
|
tcl::dict::set result replay_codes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes]] |
|
tcl::dict::set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_underlay]] |
|
tcl::dict::set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_overlay]] |
|
tcl::dict::set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result cursor_saved_attributes]] |
|
return $result |
|
} |
|
} else { |
|
#puts stderr "renderline returning: result $outstring instruction $instruction unapplied $unapplied overflow_right $overflow_right" |
|
return $outstring |
|
} |
|
#return [join $out ""] |
|
} |
|
|
|
#*** !doctools |
|
#[list_end] [comment {--- end definitions namespace overtype ---}] |
|
} |
|
|
|
tcl::namespace::eval overtype::piper { |
|
proc overcentre {args} { |
|
if {[llength $args] < 2} { |
|
error {usage: ?-bias left|right? ?-transparent [0|1|<regexp>]? ?-exposed1 <char>? ?-exposed2 <char>? ?-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 <column>? ?-transparent [0|1|<regexp>]? ?-exposed1 <char>? ?-exposed2 <char>? ?-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 |
|
} |
|
} |
|
|
|
|
|
# -- --- --- --- --- --- --- --- --- --- --- |
|
proc overtype::renderline_transparent {args} { |
|
foreach {under over} [lrange $args end-1 end] break |
|
set argsflags [lrange $args 0 end-2] |
|
set defaults [tcl::dict::create\ |
|
-transparent 1\ |
|
-exposed 1 " "\ |
|
-exposed 2 " "\ |
|
] |
|
set newargs [tcl::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. |
|
# |
|
tcl::namespace::eval overtype::piper { |
|
proc renderline {args} { |
|
if {[llength $args] < 2} { |
|
error {usage: ?-start <int>? ?-transparent [0|1|<regexp>]? ?-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 primarily for single grapheme - but will work for multiple |
|
#WARNING: query CAN contain ansi or newlines - but if cache was not already set manually,the answer will be incorrect! |
|
#We deliberately allow this for PM/SOS attached within a column |
|
#(a cache of ansifreestring_width calls - as these are quite regex heavy) |
|
proc overtype::grapheme_width_cached {ch} { |
|
variable grapheme_widths |
|
if {[tcl::dict::exists $grapheme_widths $ch]} { |
|
return [tcl::dict::get $grapheme_widths $ch] |
|
} |
|
set width [punk::char::ansifreestring_width $ch] |
|
tcl::dict::set grapheme_widths $ch $width |
|
return $width |
|
} |
|
|
|
|
|
|
|
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 [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings |
|
} |
|
if {[tcl::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] |
|
} |
|
#ansistrip 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::ansistrip $textblock] |
|
} |
|
if {[tcl::string::last \n $textblock] >= 0} { |
|
set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map {\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 [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize <data>]] width height |
|
} |
|
|
|
tcl::namespace::eval overtype::priv { |
|
variable cache_is_sgr [tcl::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 {[tcl::dict::exists $cache_is_sgr $code]} { |
|
return [tcl::dict::get $cache_is_sgr $code] |
|
} |
|
set answer [punk::ansi::codetype::is_sgr $code] |
|
tcl::dict::set cache_is_sgr $code $answer |
|
return $answer |
|
} |
|
proc render_to_unapplied {overlay_grapheme_control_list gci} { |
|
upvar idx_over idx_over |
|
|
|
#----------------------------------------- |
|
#review - this is a lot of copies of the same thing. |
|
# ultimately we want to reduce expensive things like redundant grapheme-splits |
|
# perhaps unapplied_tagged of some sort e.g - {g <grapheme> g <grapheme> code <ansi> pt <text>} ?? |
|
upvar unapplied unapplied |
|
upvar unapplied_list unapplied_list ;#maintaining as a list allows caller to utilize it without having to re-split |
|
upvar unapplied_ansisplit unapplied_ansisplit ;# pt ?code pt...? |
|
#----------------------------------------- |
|
|
|
|
|
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 "" |
|
set unapplied_list [list] |
|
set unapplied_ansisplit [list ""] |
|
#append unapplied [join [lindex $overstacks $idx_over] ""] |
|
#append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]] |
|
set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] |
|
if {$sgr_merged ne ""} { |
|
lappend unapplied_list $sgr_merged |
|
lappend unapplied_ansisplit $sgr_merged "" |
|
} |
|
switch -- [lindex $overstacks_gx $idx_over] { |
|
"gx0_on" { |
|
lappend unapplied_list "\x1b(0" |
|
lappend unapplied_ansisplit "\x1b(0" "" |
|
} |
|
"gx0_off" { |
|
lappend unapplied_list "\x1b(B" |
|
lappend unapplied_ansisplit "\x1b(B" "" |
|
} |
|
} |
|
|
|
foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { |
|
lassign $gc type item |
|
#types g other sgr gx0 |
|
switch -- $type { |
|
g { |
|
lappend unapplied_list $item |
|
lset unapplied_ansisplit end [string cat [lindex $unapplied_ansisplit end] $item] |
|
} |
|
gx0 { |
|
if {$item eq "gx0_on"} { |
|
lappend unapplied_list "\x1b(0" |
|
lappend unapplied_ansisplit "\x1b(0" "" |
|
} elseif {$item eq "gx0_off"} { |
|
lappend unapplied_list "\x1b(B" |
|
lappend unapplied_ansisplit "\x1b(B" "" |
|
} |
|
} |
|
default { |
|
lappend unapplied_list $item |
|
lappend unapplied_ansisplit $item "" |
|
} |
|
} |
|
} |
|
if {[llength $unapplied_ansisplit] == 1 && [lindex $unapplied_ansisplit 0] eq ""} { |
|
set unapplied_ansisplit [list] |
|
} |
|
set unapplied [join $unapplied_list ""] |
|
} |
|
|
|
#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 unapplied_list unapplied_list |
|
upvar unapplied_ansisplit unapplied_ansisplit |
|
#-------------- |
|
|
|
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 "" |
|
set unapplied_list [list] |
|
set unapplied_ansisplit [list ""] ;#remove empty entry at end if nothing added |
|
|
|
set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] |
|
if {$sgr_merged ne ""} { |
|
lappend unapplied_list $sgr_merged |
|
lappend unapplied_ansisplit $sgr_merged "" |
|
} |
|
switch -- [lindex $overstacks_gx $idx_over] { |
|
"gx0_on" { |
|
lappend unapplied_list "\x1b(0" |
|
lappend unapplied_ansisplit "\x1b(0" "" |
|
} |
|
"gx0_off" { |
|
lappend unapplied_list "\x1b(B" |
|
lappend unapplied_ansisplit "\x1b(B" "" |
|
} |
|
} |
|
|
|
foreach gc [lrange $overlay_grapheme_control_list $gci end] { |
|
lassign $gc type item |
|
#types g other sgr gx0 |
|
switch -- $type { |
|
g { |
|
lappend unapplied_list $item |
|
lset unapplied_ansisplit end [string cat [lindex $unapplied_ansisplit end] $item] |
|
} |
|
gx0 { |
|
if {$item eq "gx0_on"} { |
|
lappend unapplied_list "\x1b(0" |
|
lappend unapplied_ansisplit "\x1b(0" "" |
|
} elseif {$item eq "gx0_off"} { |
|
lappend unapplied_list "\x1b(B" |
|
lappend unapplied_ansisplit "\x1b(B" "" |
|
} |
|
} |
|
default { |
|
lappend unapplied_list $item |
|
lappend unapplied_ansisplit $item "" |
|
} |
|
} |
|
} |
|
if {[llength $unapplied_ansisplit] == 1 && [lindex $unapplied_ansisplit 0] eq ""} { |
|
set unapplied_ansisplit [list] |
|
} |
|
set unapplied [join $unapplied_list ""] |
|
} |
|
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] |
|
} elseif {$i == 0 || $i == $nxt} { |
|
#nothing to do |
|
} else { |
|
puts stderr "render_delchar - attempt to delchar at index $i >= number of outcols $nxt - shouldn't happen" |
|
} |
|
} |
|
proc render_erasechar {i count} { |
|
upvar outcols o |
|
upvar understacks ustacks |
|
upvar understacks_gx gxstacks |
|
upvar replay_codes_overlay replay |
|
#ECH clears character attributes from erased character positions |
|
#ECH accepts 0 or empty parameter, which is equivalent to 1. Caller of render_erasechar should do that mapping and only supply 1 or greater. |
|
if {![tcl::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? |
|
#DECECM ??? |
|
set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list $replay]]] |
|
set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] ;# ??? review |
|
return |
|
} |
|
proc render_setchar {i c } { |
|
upvar outcols o |
|
lset o $i $c |
|
} |
|
|
|
#Initial usecase is for old-terminal hack to add PM-wrapped \b |
|
#review - can be used for other multibyte sequences that occupy one column? |
|
#combiners? diacritics? |
|
proc render_append_to_char {i c} { |
|
upvar outcols o |
|
if {$i > [llength $o]-1} { |
|
error "render_append_to_char cannot append [ansistring VIEW -lf 1 -nul 1 $c] to existing char at index $i while $i >= llength outcols [llength $o]" |
|
} |
|
set existing [lindex $o $i] |
|
if {$existing eq "\0"} { |
|
lset o $i $c |
|
} else { |
|
lset o $i $existing$c |
|
} |
|
} |
|
#is actually addgrapheme? |
|
proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { |
|
upvar outcols o |
|
upvar understacks ustacks |
|
upvar understacks_gx gxstacks |
|
|
|
# -- --- --- |
|
#this is somewhat of a hack.. probably not really the equivalent of proper reverse video? review |
|
#we should ideally be able to reverse the video of a sequence that already includes SGR reverse/noreverse attributes |
|
upvar reverse_mode do_reverse |
|
#if {$do_reverse} { |
|
# lappend sgrstack [a+ reverse] |
|
#} else { |
|
# lappend sgrstack [a+ noreverse] |
|
#} |
|
|
|
#JMN3 |
|
if {$do_reverse} { |
|
#note we can't just look for \x1b\[7m or \x1b\[27m |
|
# it may be a more complex sequence like \x1b\[0\;\;7\;31m etc |
|
|
|
set existing_reverse_state 0 |
|
set codeinfo [punk::ansi::codetype::sgr_merge $sgrstack -info 1] |
|
set codestate_reverse [dict get $codeinfo codestate reverse] |
|
switch -- $codestate_reverse { |
|
7 { |
|
set existing_reverse_state 1 |
|
} |
|
27 { |
|
set existing_reverse_state 0 |
|
} |
|
"" { |
|
} |
|
} |
|
if {$existing_reverse_state == 0} { |
|
set rflip [a+ reverse] |
|
} else { |
|
#reverse of reverse |
|
set rflip [a+ noreverse] |
|
} |
|
#note that mergeresult can have multiple esc (due to unmergeables or non sgr codes) |
|
set sgrstack [list [dict get $codeinfo mergeresult] $rflip] |
|
#set sgrstack [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]] |
|
} |
|
|
|
# -- --- --- |
|
|
|
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] |
|
#JMN insert via ledit |
|
ledit o $i $i-1 $c |
|
} else { |
|
lappend o $c |
|
} |
|
if {$i < [llength $ustacks]} { |
|
#set ustacks [linsert $ustacks $i $sgrstack] |
|
#set gxstacks [linsert $gxstacks $i $gx0stack] |
|
#insert via ledit |
|
ledit ustacks $i $i-1 $sgrstack |
|
ledit gxstacks $i $i-1 $gx0stack |
|
} else { |
|
lappend ustacks $sgrstack |
|
lappend gxstacks $gx0stack |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
# -- --- --- --- --- --- --- --- --- --- --- |
|
tcl::namespace::eval overtype { |
|
interp alias {} ::overtype::center {} ::overtype::centre |
|
} |
|
|
|
|
|
namespace eval ::punk::args::register { |
|
#use fully qualified so 8.6 doesn't find existing var in global namespace |
|
lappend ::punk::args::register::NAMESPACES ::overtype ::overtype::argdoc |
|
} |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide overtype [tcl::namespace::eval overtype { |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
return |
|
|
|
#*** !doctools |
|
#[manpage_end]
|
|
|