proc un_ansi {data} { # -------------------------------------------------------------------------- # remove ansi escape sequences # -------------------------------------------------------------------------- set txt {} while {[string length ${data}]} { set match { } switch -regexp -- ${data} { {^\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]} { regexp -- {^\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]} ${data} match append txt "\n" } {^(.+?)\x1b} { regexp -- {^(.+?)\x1b} ${data} UNUSED match # handle special escape sequences regsub -all -- {\\([\\\[\]])} ${match} {\1} raw_match append txt ${raw_match} } {^\x1b} { # do nothing } default { set match ${data} append txt ${match} } } set data [string range ${data} [string length ${match}] end] } # remove white spaces not needed anymore regsub -all -- "\t+" ${txt} { } txt regsub -all -- " +" ${txt} { } txt regsub -all -nocase -- "\n+" [string trim ${txt}] "\n" txt return ${txt} }
There are devices building the screen not line by line. I even found devices that try to be intelligent and speed up the terminal output by just replacing the changing parts of the screen in any order. Therefore something like a virtual terminal is useful:
proc un_ansi_vt {data {mode raw} {bounding {0 25 0 80 0 0 0 1}}} { # -------------------------------------------------------------------------- # simulation of a virtual terminal to create line by line output # -------------------------------------------------------------------------- global terminal foreach {row_min row_max col_min col_max linewrap pagewrap status listmode} ${bounding} { break } set col_last 0 set row_last 0 set linewrap_last ${linewrap} if {${status} && [info exists terminal(status)]} { foreach {row_last col_last linewrap_last} $terminal(status) { break } } switch -glob -- ${mode} { delta - diff* - raw { catch {unset terminal} } reset* { catch {unset terminal} # FRINK: set row, col by for for {set row ${row_min}} {${row} < ${row_max}} {incr row} { for {set col ${col_min}} {${col} < ${col_max}} {incr col} { set terminal(${row},${col}) { } } } set col_last 0 set row_last 0 set linewrap_last ${linewrap} } default { # an append/overwrite mode if {![array size terminal]} { for {set row ${row_min}} {${row} < ${row_max}} {incr row} { for {set col ${col_min}} {${col} < ${col_max}} {incr col} { set terminal(${row},${col}) { } } } } } } set col ${row_last} set row ${col_last} set linewrap ${linewrap_last} while {[string length ${data}]} { set match { } switch -regexp -- ${data} { {^\x1b\[([0-9]*);([0-9]*)[Hf]} { # position cursor regexp -- {^\x1b\[0*([0-9]*);0*([0-9]*)[Hf]} ${data} match row col if {![llength ${row}]} { set row 0 } if {![llength ${col}]} { set col 0 } } {^\x1b\[[0-9]*[A-D]} { # move cursor regexp -- {^\x1b\[0*([0-9]*)([A-D])} ${data} match cnt chr if {![llength ${cnt}]} { set cnt 1 } switch -exact -- ${chr} { A { # moves the cursor up by 'cnt' rows incr row -${cnt} } B { # moves the cursor down by 'cnt' rows incr row ${cnt} } C { # moves the cursor forward by 'cnt' columns incr col ${cnt} } D { # moves the cursor backward by 'cnt' columns incr col -${cnt} } } if {(${col} < ${col_min}) ||(${col_max} < ${col})} { if {${linewrap}} { set col ${col_min} incr row } else { puts stderr "line position\t'${match}' at (${row},${col})" } } if {(${row} < ${row_min}) ||(${row_max} < ${row})} { if {${pagewrap}} { set row ${row_min} } else { puts stderr "page bounding\t'${match}' at (${row},${col})" } } } {^\x1b\[7[hl]} { regexp -- {^\x1b\[7([hl])} ${data} match chr # h/l: enable/disable line wrap set linewrap [string match {h} ${chr}] } {^\x1b\[[;0-9]*m} { # reset attributes regexp -- {^\x1b\[[;0-9]*m} ${data} match set terminal(${row},${col}) { } } {^\x1b\[[12]J} { # erase line regexp -- {^\x1b\[([12])J} ${data} match chr switch -exact -- ${chr} { 2 { # erases the screen with the background colour and moves the\ cursor to home. catch {unset terminal} if {[string compare {raw} ${mode}]} { for {set row ${row_min}} {${row} < ${row_max}} {incr row} { for {set col ${col_min}} {${col} < ${col_max}} {incr col} { set terminal(${row},${col}) { } } } } set row 0 set col 0 } 1 { # erases the screen from the current line up to the top of the\ screen. foreach pos [array names *,*] { if {[lindex [split ${pos} {,}] 0] <= ${row}} { unset terminal(${pos}) } } } default { # erases the screen from the current line down to the bottom of\ the screen. foreach pos [array names *,*] { if {${row} <= [lindex [split ${pos} {,}] 0]} { unset terminal(${pos}) } } } } } {^\x1b\[[12]K} { # erase column regexp -- {^\x1b\[([12])K} ${data} match chr switch -exact -- ${chr} { 2 { # erases the entire current line. foreach pos [array names "[set row],*"] { set terminal(${pos}) { } } } 1 { # erases from the current cursor position to the start of the\ current line. foreach pos [array names "[set row],*"] { if {[lindex [split ${pos} {,}] end] <= ${col}} { set terminal(${pos}) { } } } } default { # erases from the current cursor position to the end of the\ current line. foreach pos [array names "[set row],*"] { if {${col} <= [lindex [split ${pos} {,}] end]} { set terminal(${pos}) { } } } } } } {^\x1b\[\?*[0-9]*[A-Za-z]} { regexp -- {^\x1b\[\?*[0-9;]*[A-Za-z]} ${data} match puts stderr "ignore sequence\t'${match}'" } {^\x1b[\(\)][0-9A-Za-z]?} { # set fonts regexp -- {^\x1b[\(\)][0-9A-Za-z]?} ${data} match } {^\x1b} { # raw escape regexp -- {^\x1b} ${data} match } {^(.+?)\x1b} { regexp -- {^(.+?)\x1b} ${data} UNUSED match # handle special escape sequences regsub -all -- {\\([\\\[\]])} ${match} {\1} raw_match foreach ch [split ${raw_match} {}] { if {${col_max} < ${col}} { if {${linewrap}} { set col ${col_min} incr row } else { puts stderr "line overrun\t'[string range ${data} 0 20]'" break } } if {${row_max} < ${row}} { if {${pagewrap}} { set row ${row_min} } else { puts stderr "page overrun\t'[string range ${data} 0 20]'" break } } set terminal(${row},${col}) ${ch} incr col } } default { set match ${data} } } set data [string range ${data} [string length ${match}] end] } set terminal(status) [list ${row} ${col} ${linewrap}] set res {} set row_last [lindex [split [lsort -dictionary [array names terminal *,*]] {,}] 0] foreach pos [lsort -dictionary [array names terminal *,*]] { set row [lindex [split ${pos} {,}] 0] if {${listmode} &&(${row} != ${row_last})} { append res "\n" set row_last ${row} } append res $terminal(${pos}) } if {${listmode}} { set res [split ${res} "\n"] } return ${res} }By setting the bounding you are able to change to different sizes of terminals. Using mode will allow to switch between current changes (mode: raw, diff, delta) and new screen (mode: reset) or keeping previous screen (mode: default) making changes there. Of cause erasing screen in last mode will have the same effect as reset mode.
Category Example Category Expect