#punk linerepl #todo - make repls configurable/pluggable packages #list/string-rep bug global run_commandstr "" set stdin_info [chan configure stdin] if {[dict exists $stdin_info -inputmode]} { #this is the only way I currently know to detect console on windows.. doesn't work on Alma linux. # tcl_interactive used by repl to determine if stderr output prompt to be printed. # (that way, piping commands into stdin should not produce prompts for each command) set tcl_interactive 1 } #however, the -mode option only seems to appear on linux when a terminal exists.. if {[dict exists $stdin_info -mode]} { set tcl_interactive 1 } #give up for now set tcl_interactive 1 proc todo {} { puts "tcl History" puts "repltelemetry package" puts "deaddrop package for a consistent way for modules to leave small notes to others that may come later." } package require Thread package require shellfilter package require shellrun package require punk package require punk::ns package require punk::ansi package require punk::console package require textblock if {![info exists ::env(SHELL)]} { set ::env(SHELL) punk86 } if {![info exists ::env(TERM)]} { # tset -r seems to rely on env(TERM) - so this doesn't seem to work #if {![catch {exec tset -r} result]} { # #e.g Terminal type is xterm-256color. # set t [string trimright [lindex $result end] .] # set ::env(TERM) $t #} else { #fake it ? #set ::env(TERM) vt100 set ::env(TERM) xterm-256color #} } #todo - move to less generic namespace namespace eval repl { variable screen_last_chars "" ;#a small sliding append buffer for last char of any screen output to detect \n vs string variable screen_last_char_list [list] variable last_unknown "" variable prompt_reset_flag 0 ;#trigger repl to re-retrieve prompt settings variable output "" #important not to initialize - as it can be preset by cooperating package before app-punk has been package required #(this is an example of a deaddrop) variable post_script variable signal_control_c 0 } namespace eval punk::repl { variable debug_repl 0 proc has_script_var_bug {} { set script {set j [list spud] ; list} append script \n uplevel #0 $script set rep1 [tcl::unsupported::representation $::j] set script "" set rep2 [tcl::unsupported::representation $::j] set nostring1 [string match "*no string" $rep1] set nostring2 [string match "*no string" $rep2] #we assume it should have no string rep in either case #Review: check Tcl versions for behaviour/consistency if {!$nostring2} { return true } else { return false } } } namespace eval repl { #since we are targeting Tcl 8.6+ - we should be using 'interp bgerror .' #todo - make optional/configurable? proc bgerror2 {args} { puts stderr "====================" puts stderr "repl::bgerror" puts stderr "====================" puts stderr "[lindex $args 0]" puts stderr "-------------------" puts stderr "[lrange $args 1 end]" puts stderr "====================" puts stderr "^^^^^^^^^^^^^^^^^^^" } proc bgerror {message} { puts stderr "*> repl background error: $message" puts stderr "*> [set ::errorInfo]" set stdinreader [fileevent stdin readable] if {![string length $stdinreader]} { puts stderr "*> stdin reader inactive" } else { puts stderr "*> stdin reader active" } } if {![llength [info commands ::bgerror]]} { interp alias {} bgerror {} ::repl::bgerror } } namespace eval ::repl::term { } package require term::ansi::code::ctrl if {$::tcl_platform(platform) eq "windows"} { #jmn disable twapi #package require zzzload #zzzload::pkg_require twapi after idle [list after 1000 { #puts stdout "===============repl loading twapi===========" #zzzload::pkg_wait twapi if {![catch {package require twapi}]} { proc ::repl::term::handler_console_control {args} { #puts -nonewline stdout . #flush stdout incr ::repl::signal_control_c #rputs stderr "* console_control: $args" if {$::punk::console::is_raw} { #how to let rawmode loop handle it? It doesn't seem to get through if we return 0 puts stderr "signal ctrl-c while in raw mode" after 200 {exit 42} ;#temp flush stderr return 42 } #note - returning 0 means pass event to other handlers including OS default handler if {$::repl::signal_control_c <= 2} { set remaining [expr {3 - $::repl::signal_control_c}] puts stderr "signal ctrl-c (perform $remaining more to quit, enter to return to repl)" flush stderr return 1 } elseif {$::repl::signal_control_c == 3} { puts stderr "signal ctrl-c x3 received - quitting" flush stderr after 25 quit return 1 } elseif {$::repl::signal_control_c == 4} { puts stderr "signal ctrl-c x4 received - one more to hard exit" flush stderr return 1 } elseif {$::repl::signal_control_c >= 5} { #a script that allows events to be processed could still be running puts stderr "signal ctrl-c x5 received - hard exit" flush stderr after 25 exit 499 ;# HTTP 'client closed request' - just for the hell of it. } else { puts stderr "signal ctrl-c $::repl::signal_control_c received" flush stderr #return 0 to fall through to default handler return 0 } } twapi::set_console_control_handler ::repl::term::handler_console_control proc ::repl::term::set_console_title {text} { #go via console - in case ansi disabled/unavailable punk::console::titleset $text } proc ::repl::term::set_console_icon {name} { #todo } #we can't yet emit from an event with proper prompt handling - #repl::rputs stdout "twapi loaded" } else { #repl::rputs stderr " Failed to load twapi" } }] } else { #TODO proc ::repl::term::set_console_title {text} { #todo - terminfo/termcap? #puts -nonewline "\033\]2;$text\007" ;#works for xterm and most derivatives puts -nonewline [term::ansi::code::ctrl::title $text] } proc ::repl::term::set_console_icon {name} { #old xterm feature for label given to xterm window when miniaturized? TODO research #puts -nonewline "\033\]1;$name\007" } } #expermental terminal alt screens #alternatives are \x1b\[?47h ans \x1b[?\47l proc ::repl::term::screen_push_alt {} { #tput smcup puts -nonewline stderr "\033\[?1049h" } proc ::repl::term::screen_pop_alt {} { #tput rmcup puts -nonewline stderr "\033\[?1049l" } interp alias {} smcup {} ::repl::term::screen_push_alt interp alias {} rmcup {} ::repl::term::screen_pop_alt # moved to punk package.. #set outdevice [shellfilter::stack::new punkout -settings [list -tag "punkout" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stdout] -file [dict get $::punk::config::running logfile_stdout]]] #set out [dict get $outdevice localchan] #set errdevice [shellfilter::stack::new punkerr -settings [list -tag "punkerr" -buffering none -raw 1 -syslog [dict get $::punk::config::running syslog_stderr] -file [dict get $::punk::config::running logfile_stderr]]] #set err [dict get $errdevice localchan] # #set indevice [shellfilter::stack::new commandin -settings {-tag "commandin" -readbuffering line -writebuffering none -raw 1 -direction in}] #set program_read_stdin_pipe [dict get $indevice localchan] # unknown -- # This procedure is called when a Tcl command is invoked that doesn't # exist in the interpreter. It takes the following steps to make the # command available: # # 1. See if the autoload facility can locate the command in a # Tcl script file. If so, load it and execute it. # 2. If the command was invoked interactively at top-level: # (a) see if the command exists as an executable UNIX program. # If so, "exec" the command. # (b) see if the command requests csh-like history substitution # in one of the common forms !!, !, or ^old^new. If # so, emulate csh's history substitution. # (c) see if the command is a unique abbreviation for another # command. If so, invoke the command. # # Arguments: # args - A list whose elements are the words of the original # command, including the command name. #review - we shouldn't really be doing this #We need to work out if we can live with the real default unknown and just inject some special cases at the beginning before falling-back to the normal one proc ::unknown args { #puts stderr "unk>$args" variable ::tcl::UnknownPending global auto_noexec auto_noload env tcl_interactive errorInfo errorCode if {[info exists errorInfo]} { set savedErrorInfo $errorInfo } if {[info exists errorCode]} { set savedErrorCode $errorCode } set name [lindex $args 0] if {![info exists auto_noload]} { # # Make sure we're not trying to load the same proc twice. # if {[info exists UnknownPending($name)]} { return -code error "self-referential recursion\ in \"unknown\" for command \"$name\"" } set UnknownPending($name) pending set ret [catch { auto_load $name [uplevel 1 {::namespace current}] } msg opts] unset UnknownPending($name) if {$ret != 0} { dict append opts -errorinfo "\n (autoloading \"$name\")" return -options $opts $msg } if {![array size UnknownPending]} { unset UnknownPending } if {$msg} { if {[info exists savedErrorCode]} { set ::errorCode $savedErrorCode } else { unset -nocomplain ::errorCode } if {[info exists savedErrorInfo]} { set errorInfo $savedErrorInfo } else { unset -nocomplain errorInfo } set code [catch {uplevel 1 $args} msg opts] if {$code == 1} { # # Compute stack trace contribution from the [uplevel]. # Note the dependence on how Tcl_AddErrorInfo, etc. # construct the stack trace. # set errInfo [dict get $opts -errorinfo] set errCode [dict get $opts -errorcode] set cinfo $args if {[string length [encoding convertto utf-8 $cinfo]] > 150} { set cinfo [string range $cinfo 0 150] while {[string length [encoding convertto utf-8 $cinfo]] > 150} { set cinfo [string range $cinfo 0 end-1] } append cinfo ... } set tail "\n (\"uplevel\" body line 1)\n invoked\ from within\n\"uplevel 1 \$args\"" set expect "$msg\n while executing\n\"$cinfo\"$tail" if {$errInfo eq $expect} { # # The stack has only the eval from the expanded command # Do not generate any stack trace here. # dict unset opts -errorinfo dict incr opts -level return -options $opts $msg } # # Stack trace is nested, trim off just the contribution # from the extra "eval" of $args due to the "catch" above. # set last [string last $tail $errInfo] if {$last + [string length $tail] != [string length $errInfo]} { # Very likely cannot happen return -options $opts $msg } set errInfo [string range $errInfo 0 $last-1] set tail "\"$cinfo\"" set last [string last $tail $errInfo] if {$last < 0 || $last + [string length $tail] != [string length $errInfo]} { return -code error -errorcode $errCode \ -errorinfo $errInfo $msg } set errInfo [string range $errInfo 0 $last-1] set tail "\n invoked from within\n" set last [string last $tail $errInfo] if {$last + [string length $tail] == [string length $errInfo]} { return -code error -errorcode $errCode \ -errorinfo [string range $errInfo 0 $last-1] $msg } set tail "\n while executing\n" set last [string last $tail $errInfo] if {$last + [string length $tail] == [string length $errInfo]} { return -code error -errorcode $errCode \ -errorinfo [string range $errInfo 0 $last-1] $msg } return -options $opts $msg } else { dict incr opts -level return -options $opts $msg } } } #set isrepl [expr {[file tail [file rootname [info script]]] eq "repl"}] set isrepl $::repl::running ;#may not be reading though if {$isrepl} { #set ::tcl_interactive 1 } if {$isrepl || (([info level] == 1) && (([info script] eq "" ) ) && ([info exists tcl_interactive] && $tcl_interactive))} { if {![info exists auto_noexec]} { set new [auto_execok $name] if {$new ne ""} { set redir "" if {[namespace which -command console] eq ""} { set redir ">&@stdout <@stdin" } #experiment todo - use twapi and named pipes #twapi::namedpipe_server {\\.\pipe\something} #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc # if {[string first " " $new] > 0} { set c1 $name } else { set c1 $new } # 'script' command to fake a tty # note that we lose the exit code from the underlying command by using 'script' if we call shellfilter::run without -e option to script set scr [auto_execok script] set scr "" ;#set src to empty to disable - script is just a problematic experiment if {$scr ne ""} { #set scriptrun "( $c1 [lrange $args 1 end] )" if 0 { set scriptrun "( $c1 " foreach a [lrange $args 1 end] { if {[string first " " $a] > 0} { #append scriptrun "\"$a\"" append scriptrun $a } else { append scriptrun $a } append scriptrun " " } append scriptrun " )" } #------------------------------------- if 0 { package require string::token::shell set shellparts [string token shell -indices $args] set scriptrun "( $c1 " foreach info [lrange $shellparts 1 end] { set type [lindex $info 0] if {$type eq "D:QUOTED"} { append scriptrun "\"" append scriptrun [lindex $info 3] append scriptrun "\"" } elseif {$type eq "S:QUOTED"} { append scriptrun "'" append scriptrun [lindex $info 3] append scriptrun "'" } elseif {$type eq "PLAIN"} { append scriptrun [lindex $info 3] } else { error "Can't interpret '$args' with sh-like syntax" } append scriptrun " " } append scriptrun " )" } #------------------------------------- #uplevel 1 [list ::catch \ [list ::shellfilter::run [list $scr -q -e -c $scriptrun /dev/null] -teehandle punk -inbuffering line -outbuffering none ] \ ::tcl::UnknownResult ::tcl::UnknownOptions] if {[string tolower [file rootname [file tail $new]]] ne "script"} { if {$::env(SHELL) eq "punk86"} { set shellcmdflag "punk86 cmdb" } elseif {$::env(SHELL) eq "cmd"} { set shellcmdflag "cmd /c" } elseif {$::env(SHELL) eq "pwsh"} { set shellcmdflag "pwsh -c" } else { # sh etc #set shellcmdflag "$::env(SHELL) -c" set shellcmdflag "-c" } #set commandlist [shellfilter::get_scriptrun_from_cmdlist_dquote_if_not [concat [list $new ] [lrange $args 1 end]]] set commandlist [shellfilter::get_scriptrun_from_cmdlist_dquote_if_not $args $shellcmdflag] puts stderr ">>> [lindex $commandlist 4]" } else { set commandlist [list $new {*}[lrange $args 1 end]] } puts stderr ">>>scriptrun_commandlist: $commandlist" #ansiwrap for testing #set id_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] uplevel #0 [list ::catch [list ::shellfilter::run $commandlist -teehandle punk -inbuffering line -outbuffering none ] ::tcl::UnknownResult ::tcl::UnknownOptions] #shellfilter::stack::remove stderr $id_stderr puts stdout "script result $::tcl::UnknownOptions $::tcl::UnknownResult" if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" } else { #no point returning "exitcode 0" if that's the only non-error return. #It is misleading. Better to return empty string. set ::tcl::UnknownResult "" } } else { set idlist_stdout [list] set idlist_stderr [list] set shellrun::runout "" #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] if {![dict get $::punk::config::running exec_unknown]} { uplevel 1 [list ::catch \ [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle punk -inbuffering line -outbuffering none ] \ ::tcl::UnknownResult ::tcl::UnknownOptions] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { dict set ::tcl::UnknownOptions -code error set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" } else { #no point returning "exitcode 0" if that's the only non-error return. #It is misleading. Better to return empty string. set ::tcl::UnknownResult "" } } else { set ::punk::last_run_display [list] set redir ">&@stdout <@stdin" uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] #we can't detect stdout/stderr output from the exec #for now emit an extra \n on stderr #todo - use console apis (twapi on windows) to detect cursor posn? # # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit if {[dict get $::tcl::UnknownOptions -code] == 0} { set c green set m "ok" } else { set c yellow set m "errorCode $::errorCode" } set chunklist [list] lappend chunklist [list "info" "[a $c]$m[a] " ] set ::punk::last_run_display $chunklist } foreach id $idlist_stdout { shellfilter::stack::remove stdout $id } foreach id $idlist_stderr { shellfilter::stack::remove stderr $id } } #uplevel 1 [list ::catch \ # [concat exec $redir $new [lrange $args 1 end]] \ # ::tcl::UnknownResult ::tcl::UnknownOptions] #puts "===exec with redir:$redir $::tcl::UnknownResult ==" dict incr ::tcl::UnknownOptions -level return -options $::tcl::UnknownOptions $::tcl::UnknownResult } } if {$name eq "!!"} { set newcmd [history event] } elseif {[regexp {^!(.+)$} $name -> event]} { set newcmd [history event $event] } elseif {[regexp {^\^([^^]*)\^([^^]*)\^?$} $name -> old new]} { set newcmd [history event -1] catch {regsub -all -- $old $newcmd $new newcmd} } if {[info exists newcmd]} { tclLog $newcmd history change $newcmd 0 uplevel 1 [list ::catch $newcmd \ ::tcl::UnknownResult ::tcl::UnknownOptions] dict incr ::tcl::UnknownOptions -level return -options $::tcl::UnknownOptions $::tcl::UnknownResult } set ret [catch {set candidates [info commands $name*]} msg] if {$name eq "::"} { set name "" } if {$ret != 0} { dict append opts -errorinfo \ "\n (expanding command prefix \"$name\" in unknown)" return -options $opts $msg } # Filter out bogus matches when $name contained # a glob-special char [Bug 946952] if {$name eq ""} { # Handle empty $name separately due to strangeness # in [string first] (See RFE 1243354) set cmds $candidates } else { set cmds [list] foreach x $candidates { if {[string first $name $x] == 0} { lappend cmds $x } } } #punk - disable prefix match search set default_cmd_search 0 if {$default_cmd_search} { if {[llength $cmds] == 1} { uplevel 1 [list ::catch [lreplace $args 0 0 [lindex $cmds 0]] \ ::tcl::UnknownResult ::tcl::UnknownOptions] dict incr ::tcl::UnknownOptions -level return -options $::tcl::UnknownOptions $::tcl::UnknownResult } if {[llength $cmds]} { return -code error "ambiguous command name \"$name\": [lsort $cmds]" } } else { #punk hacked version - report matches but don't run if {[llength $cmds]} { return -code error "unknown command name \"$name\": possible match(es) [lsort $cmds]" } } } return -code error -errorcode [list TCL LOOKUP COMMAND $name] \ "invalid command name \"$name\"" } punk::configure_unknown ;#must be called because we hacked the tcl 'unknown' proc proc repl::reset_prompt {} { variable prompt_reset_flag set prompt_reset_flag 1 } #todo - review proc repl::term::reset {} { set prompt_reset_flag 1 #clear ;#call to external executable which may not be available puts stdout [::term::ansi::code::ctrl::rd] } proc repl::get_prompt_config {} { if {$::tcl_interactive} { set RST [a] set resultprompt "[a green bold]-$RST " set nlprompt "[a green bold].$RST " set infoprompt "[a green bold]*$RST " set debugprompt "[a purple bold]~$RST " } else { set resultprompt "" set nlprompt "" set infoprompt "" set debugprompt "" } return [list resultprompt $resultprompt nlprompt $nlprompt infoprompt $infoprompt debugprompt $debugprompt] } proc repl::start {inchan args} { variable commandstr variable readingchunk # --- variable editbuf variable editbuf_list ;#command history variable editbuf_linenum_submitted variable editbuf_active_index # --- variable running variable reading variable done set done 0 variable startinstance variable loopinstance if {[namespace exists ::punkapp]} { if {[dict exists $args -defaultresult]} { set ::punkapp::default_result [dict get $args -defaultresult] } } incr startinstance set loopinstance 0 set running 1 set commandstr "" set readingchunk "" # --- set editbuf [punk::repl::class::class_editbuf new {}] lappend editbuf_list $editbuf ;#current editbuf is always in the history set editbuf_linenum_submitted 0 set editbuf_active_index 0 # --- if {$::punk::console::ansi_wanted == 2} { if {[::punk::console::test_can_ansi]} { set ::punk::console::ansi_wanted 1 } else { set ::punk::console::ansi_wanted -1 } } set prompt_config [get_prompt_config] doprompt "P% " fileevent $inchan readable [list [namespace current]::repl_handler $inchan $prompt_config] set reading 1 catch { #set punk::console::tabwidth [punk::console::get_tabstop_apparent_width] } vwait [namespace current]::done #todo - override exit? #after 0 ::repl::post_operations after idle ::repl::post_operations vwait repl::post_operations_done if {[namespace exists ::punkapp]} { #todo check and get punkapp::result array - but what key? if {[info exists ::punkapp::result(shell)]} { set temp $::punkapp::result(shell) unset ::punkapp::result(shell) return $temp } elseif {[info exists ::punkapp::default_result]} { set temp $::punkapp::default_result unset ::punkapp::default_result return $temp } } return 0 } proc repl::post_operations {} { if {[info exists ::repl::post_script] && [string length $::repl::post_script]} { #put aside post_script so the script has the option to add another post_script and restart the repl set ::repl::running_script $::repl::post_script set ::repl::post_script "" uplevel #0 {eval $::repl::running_script} } #todo - tidyup so repl could be restarted set repl::post_operations_done 0 } proc repl::reopen_stdin {} { #variable reopen_stdin_attempts if {$::tcl_platform(platform) eq "windows"} { puts stderr "|repl> Attempting reconnection of console to stdin by opening 'CON'" } else { puts stderr "|repl> Attempting reconnection of console to stdin by opening '/dev/tty'" } #puts stderr "channels:[chan names]" #flush stderr #catch {chan close stdin} chan close stdin if {$::tcl_platform(platform) eq "windows"} { #set s [open "CON" r] set s [open {CONIN$} r] if {[package provide twapi] ne ""} { set h [twapi::get_tcl_channel_handle $s in] twapi::SetStdHandle -10 $h } puts stderr "restarting repl on inputchannel:$s" return [repl::start $s] } else { #/dev/tty - reference to the controlling terminal for a process #review/test set s [open "/dev/tty" r] } repl::start stdin } proc quit {} { set ::repl::done "quit" return "" ;#make sure to return nothing so "quit" doesn't land on stdout } #just a failed experiment.. tried various things proc repl::reopen_stdinX {} { #windows - todo unix package require twapi if 0 { if {[catch {package require Memchan} errM]} { #package require tcl::chan::fifo2 #lassign [tcl::chan::fifo2] a b package require tcl::chan::fifo set x [tcl::chan::fifo] } else { #lassign [fifo2] a b set x [fifo] } #first channel opened after stdin closed becomes stdin #use a fifo or fifo2 because [chan pipe] assigns the wrong end first! #a will be stdin } #these can't replace proper stdin (filehandle 0) because they're not 'file backed' or 'os level' #try opening a named pipe server to become stdin set pipename {\\.\pipe\stdin_%id%} set pipename [string map [list %id% [pid]] $pipename] package require tcl::chan::fifo chan close stdin lassign [tcl::chan::fifo] a puts stderr "newchan: $a" puts stderr "|test> $a [chan conf $a]" #set server [twapi::namedpipe_server $pipename] #set client [twapi::namedpipe_client $pipename] ;#open a client and connect to the server we just made puts stderr "chan names: [chan names]" #by now $server not valid? #set server stdin #chan configure $server -buffering line -encoding unicode #chan configure $client -buffering line -encoding unicode #puts stderr "|test>ns-server $server [chan conf $server]" #puts stderr "|test>ns-client $client [chan conf $client]" set conin [twapi::get_console_handle stdin] twapi::set_standard_handle stdin $conin set h_in [twapi::get_standard_handle stdin] puts stderr "|test> $a [chan conf $a]" #chan configure $client -blocking 0 after 2 repl::start $a } #add to sliding buffer of last x chars emmitted to screen by repl #(we could maintain only one char - more kept merely for debug assistance) #will not detect emissions from exec with stdout redirected and presumably some extensions etc proc repl::screen_last_char_add {c what {why ""}} { variable screen_last_chars variable screen_last_char_list if {![string length $c]} { return [string index $screen_last_chars end] } if {[string length $screen_last_chars] > 10} { set screen_last_chars [string range $screen_last_chars 1 end] ;#evict first char set screen_last_char_list [lrange $screen_last_char_list 1 end] } append screen_last_chars $c lappend screen_last_char_list [list $c $what $why] #return [string index $screen_last_chars end] #return [lindex $screen_last_char_list 0 0] return [lindex $screen_last_char_list end 0] } proc repl::screen_last_char_get {} { variable screen_last_char_list return [lindex $screen_last_char_list end 0] } proc repl::screen_last_char_getinfo {} { variable screen_last_char_list return [lindex $screen_last_char_list end] } #-------------------------------------- #another experiment proc repl::newout {} { namespace eval ::replout { namespace ensemble create -map { initialize init finalize close watch watch write write } } proc ::replout::init {id mode} { return {initialize finalize watch write} } proc ::replout::close {id} { } proc ::replout::watch {id spec} { } proc ::replout::write {id data} { puts -nonewline stderr $data return [string length $data] } close stdout set fd [chan create write ::replout] chan configure $fd -buffering none return $fd } interp alias {} newout {} repl::newout proc repl::newout2 {} { close stdout set s [open "CON" w] chan configure $s -buffering none } #-------------------------------------- proc repl::doprompt {prompt {col {green bold}}} { #prompt to stderr. #We can pipe commands into repl's stdin without the prompt interfering with the output. #Although all command output for each line goes to stdout - not just what is emmited with puts if {$::tcl_interactive} { set last_char_info [screen_last_char_getinfo] if {![llength $last_char_info]} { set needs_clearance 1 } else { lassign $last_char_info c what why if {$why eq "prompt"} { set needs_clearance 0 } else { set needs_clearance [screen_needs_clearance] #puts -nonewline "-->$needs_clearance $last_char_info" } } if {$needs_clearance == 1} { set c \n } else { set c "" } set pre "" if {[string first \n $prompt] >=0} { set plines [split $prompt \n] set pre [join [lrange $plines 0 end-1] \n]\n set prompt [lindex $plines end] } #this sort of works - but steals some of our stdin data ? review # #lassign [punk::console::get_cursor_pos_list] column row #if {$row != 1} { # set c "\n" #} set o [a {*}$col] set r [a] puts -nonewline stderr $c$pre$o$prompt$r screen_last_char_add " " "prompt-stderr" prompt flush stderr } } #use rputs in repl_handler instead of puts # - to help ensure we don't emit extra blank lines in info or debug output #rputs expects the standard tcl 'puts' command to be in place. # all bets are off if this has been redefined with some other api # rputs deliberately doesn't check screen_last_chars before emitting data (unless reporting an error in rputs itself) proc repl::rputs {args} { variable screen_last_chars variable last_out_was_newline variable last_repl_char set pseudo_map [dict create\ debug stderr\ debugreport stderr\ ] if {[::tcl::mathop::<= 1 [llength $args] 3]} { set out [lindex $args end] append out ""; #copy on write if {([llength $args] > 1) && [lindex $args 0] ne "-nonewline"} { set this_tail \n set rputschan [lindex $args 0] #map pseudo-channels to real if {$rputschan in [dict keys $pseudo_map]} { lset args 0 [dict get $pseudo_map $rputschan] } } elseif {[llength $args] == 1} { set this_tail \n set rputschan "stdout" } else { #>1 arg with -nonewline set this_tail [string index $out end] set rputschan [lindex $args 1] #map pseudo-channels to real if {$rputschan in [dict keys $pseudo_map]} { lset args 0 [dict get $pseudo_map $rputschan] } } set last_char_info_width 60 #review - string shouldn't be truncated prior to stripcodes - could chop ansi codes! #set summary "[::shellfilter::ansi::stripcodes [string range $out 0 $last_char_info_width]]" set out_plain_text [punk::ansi::stripansi $out] set summary [string range $out_plain_text 0 $last_char_info_width] if {[string length $summary] > $last_char_info_width} { append summary " ..." } #make sure we use supplied rputschan in the screen_las_char_add 'what' - which may not be the underlying chan if it was a pseudo screen_last_char_add $this_tail repl-$rputschan $summary #tailcall? puts {*}$args } else { #looks like an invalid puts call - use the normal error produced by the puts command #This should only occur if the repl itself is being rewritten/debugged, #so we will use red "!" and not worry about the extra newlines before and after if {[catch { puts {*}$args } err]} { set c [a yellow bold] set n [a] #possibly non punk-compliant output because we're assuming the repl was the most recent emitter #could be wrong, in which case we may emit an extra newline #- shouldn't matter in this case #set last_char [string range $screen_last_chars end] set last_char [screen_last_char_get] if {$last_char eq "\n"} { set clear "" } else { set clear "\n" } puts -nonewline stderr "$clear[a red bold]! REPL ERROR IN rputs $c$err$n\n" screen_last_char_add "\n" replerror "rputs err: '$err'" return } else { #?? shouldn't happen with standard puts command #do our best and assume final arg is still the data being emitted #worst that will happen is we won't detect a trailing newline and will later emit an extra blank line. set out [lindex $args end] set this_tail [string index $out end] screen_last_char_add $this_tail replunknown "rputs $args" return } } } #whether we need a newline as clearance from previous output #review - race with copy pasted data, hold-down of enter key # and data from external process or background script that doesn't go through our stdout filter #we probably can't use get_cursor_pos - as that needs to emit to stdout and read-loop on stdin which will possibly? make things worse proc repl::screen_needs_clearance {} { variable screen_last_chars #set last_char [string index $screen_last_chars end] set last_char_info [screen_last_char_getinfo] if {![llength $last_char_info]} { #assumption return 1 } lassign $last_char_info c what why switch -- $what { stdout - stderr - stdout/stderr { return 1 } } return [expr {$c ne "\n"}] } namespace eval repl { variable startinstance 0 variable loopinstance 0 variable loopcomplete 0 variable in_repl_handler [list] } namespace eval punk::repl::class { oo::class create class_bufman { } #multiline editing buffer oo::class create class_editbuf { variable o_context variable o_config variable o_rendered_lines variable o_remaining ;#? #o_chunk_list & o_chunk_info should make timed viewing of replays possible variable o_chunk_list variable o_chunk_info ;#arrival timing etc variable o_cursor_row variable o_cursor_col variable o_insert_mode constructor {configdict {contextdict {}}} { my clear set o_config $configdict if {[dict exists $configdict rendered_initialchunk]} { #pre-rendered initial chunk #-- set o_chunk_list "" ;#replace empty chunk from 'clear' call set o_chunk_info [dict create] #-- set ch [dict get $configdict rendered_initialchunk] my add_rendered_chunk $ch } set o_context $contextdict #error "[self class].constructor Unable to interpret config '$o_config'" } method cursor_row {} { return $o_cursor_row } method cursor_column {} { return $o_cursor_col } method insert_mode {} { return $o_insert_mode } method clear {} { set o_rendered_lines [list ""] set o_chunk_list [list] set o_chunk_info [dict create] set o_cursor_row 1 set o_cursor_col 1 set o_insert_mode 1 ;#default to insert mode lappend o_chunk_list "" dict set o_chunk_info 0 [dict create micros [clock microseconds] type rendered] } method add_chunk {chunk} { #we still split on lf - but each physical line may contain horizontal or vertical movements so we need to feed each line in and possibly get an overflow_right and unapplied and cursor-movent return info lappend o_chunk_list $chunk ;#may contain newlines,horizontal/vertical movements etc - all ok dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type raw] if {$chunk eq ""} { return } set firstnl [string first \n $chunk] set newparts [split $chunk \n] #attempt to render new 'lines' into the editbuffer - taking account of active cursor row & col & insertmode #merge based on current cursor row and col #set lastrline [lindex $o_rendered_lines end] #set n0 [lindex $newparts 0] #set merged0 [string cat $lastrline $n0] #we should merge first row of newparts differently in case our chunks split a grapheme-combination? # if {$o_cursor_row < 1} { puts stderr "add_chunk warning cursor_row < 1 - changing to minimum value 1" set o_cursor_row 1 } set cursor_row_idx [expr {$o_cursor_row -1}] set activeline [lindex $o_rendered_lines $cursor_row_idx] set new0 [lindex $newparts 0] #set combined [string cat $activeline $new0] #use -cursor_row to tell renderline it's row context. if {$firstnl >=0} { #append combined \n append new0 \n } set underlay [punk::ansi::stripansi $activeline] set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] if {$o_cursor_col > $line_nextchar_col} { set o_cursor_col $line_nextchar_col } set mergedinfo [overtype::renderline -info 1 -overflow 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $new0] set result [dict get $mergedinfo result] set o_insert_mode [dict get $mergedinfo insert_mode] set result_col [dict get $mergedinfo cursor_column] set result_row [dict get $mergedinfo cursor_row] set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v set unapplied [dict get $mergedinfo unapplied] set instruction [dict get $mergedinfo instruction] set insert_lines_below [dict get $mergedinfo insert_lines_below] set insert_lines_above [dict get $mergedinfo insert_lines_above] # -- --- --- --- --- --- set debug_first_row 2 #puts "merged: $mergedinfo" set debug "add_chunk0" append debug \n $mergedinfo append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $new0] before row:$o_cursor_row after row: $result_row before col:$o_cursor_col after col:$result_col" package require textblock set debug [textblock::frame -buildcache 0 $debug] catch {punk::console::move_emitblock_return $debug_first_row 1 $debug} # -- --- --- --- --- --- set o_cursor_col $result_col set cursor_row_idx [expr {$o_cursor_row-1}] lset o_rendered_lines $cursor_row_idx $result set nextrow $result_row switch -- $instruction { lf_start { #for normal commandline - we just add a line below lappend o_rendered_lines "" incr nextrow set o_cursor_col 1 } } if {$insert_lines_below == 1} { if {[string length $overflow_right]} { lappend o_rendered_lines $overflow_right set o_cursor_col [expr {[punk::ansi::printing_length $overflow_right] +1}] } else { lappend o_rendered_lines "" set o_cursor_col 1 } } elseif {$insert_lines_above == 1} { #for {set i 0} {$i < $insert_lines_above} {incr i} { # set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] # incr nextrow -1 #} set o_rendered_lines [linsert $o_rendered_lines $cursor_row_idx ""] set o_cursor_col 1 } set o_cursor_row $nextrow set cursor_row_idx [expr {$o_cursor_row-1}] if {$cursor_row_idx < [llength $o_rendered_lines]} { set activeline [lindex $o_rendered_lines $cursor_row_idx] } else { lappend o_rendered_lines "" set activeline "" } set i 1 foreach p [lrange $newparts 1 end] { if {$i < [llength $newparts]-1} { append p \n } else { if {$p eq ""} { break } } #puts stderr "overtype::renderline -info 1 -overflow 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $activeline '$p'" set underlay $activeline set line_nextchar_col [expr {[punk::char::string_width $underlay] + 1}] if {$o_cursor_col > $line_nextchar_col} { set o_cursor_col $line_nextchar_col } set mergedinfo [overtype::renderline -info 1 -overflow 1 -insert_mode $o_insert_mode -cursor_column $o_cursor_col -cursor_row $o_cursor_row $underlay $p] set debug "add_chunk$i" append debug \n $mergedinfo append debug \n "input:[ansistring VIEW -lf 1 -vt 1 $p]" package require textblock set debug [textblock::frame -buildcache 0 $debug] #catch {punk::console::move_emitblock_return [expr {$debug_first_row + ($i * 6)}] 1 $debug} set result [dict get $mergedinfo result] set o_insert_mode [dict get $mergedinfo insert_mode] set o_cursor_col [dict get $mergedinfo cursor_column] set cmove [dict get $mergedinfo cursor_row] set overflow_right [dict get $mergedinfo overflow_right] ;#should be empty if no \v set unapplied [dict get $mergedinfo unapplied] set insert_lines_below [dict get $mergedinfo insert_lines_below] if {[string is integer -strict $cmove]} { if {$cmove == 0} { set nextrow [expr {$o_cursor_row + 1}] set o_cursor_col 1 } elseif {$cmove == 1} { #check for overflow_right and unapplied #leave cursor_column } elseif {$cmove >= 1} { } } else { # = - absolute set nextrow [string range $cmove 1 end] } if {$nextrow eq $o_cursor_row} { incr nextrow } set o_cursor_row $nextrow if {$insert_lines_below} { } set cursor_row_idx [expr {$o_cursor_row-1}] if {$cursor_row_idx < [llength $o_rendered_lines]} { set activeline [lindex $o_rendered_lines $cursor_row_idx] } else { lappend o_rendered_lines "" set activeline "" } lset o_rendered_lines $cursor_row_idx $result incr i } } method add_rendered_chunk {rchunk} { #split only on lf newlines - movement codes and \b \v \r not expected #check only for \v \r as chars we don't expect/want in rendered lines #chunk as been pre-rendered (or is known to be plain ascii without ANSI or \b \v \r) #but we don't yet have grapheme split info for it if {[regexp {[\v\b\r]} $rchunk]} { error "[self class].add_rendered_chunk chunk contains \\v or \\b or \\r. Rendered chunk shouldn't contain these characters or ANSI movement codes" } lappend o_chunk_list $rchunk ;#rchunk may contain newlines - that's ok dict set o_chunk_info [expr {[llength $o_chunk_list] -1}] [dict create micros [clock microseconds] type rendered] set newparts [split $rchunk \n] #lappend o_chunk_list $rchunk set lastrline [lindex $o_rendered_lines end] #in renderedlines list merge last line of old with first line of new #we can't just cat the newpart on to existing rendered line - the chunk could have split a grapheme (e.g char+combiner(s)) #we #todo - redo grapheme split on merged line set merged [string cat $lastrline [lindex $newparts 0]] lset o_rendered_lines end $merged #todo #each newpart needs its grapheme split info to be stored set o_rendered_lines [concat $o_rendered_lines [lrange $newparts 1 end]] } method linecount {} { return [llength $o_rendered_lines] } method line {idx} { if {[string is integer -strict $idx]} { incr idx -1 } return [lindex $o_rendered_lines $idx] } method lines {args} { switch -- [llength $args] { 0 {return $o_rendered_lines} 1 { set idx [lindex $args 0] if {[string is integer -strict $idx]} { incr idx -1 } return [list [lindex $o_rendered_lines $idx]] } 2 { lassign $args idx1 idx2 if {[string is integer -strict $idx1]} { incr idx1 -1 } if {[string is integer -strict $idx2]} { incr idx2 -1 } return [lrange $o_rendered_lines $idx1 $idx2] } default {error "lines expected 0,1 or 2 indices"} } } #todo - index base??? method lines_numbered {args} { #build a paired list so we don't have to do various calcs on end+ end- etc checking llength #punk::range will use lseq if available - else use it's own slower code set max [llength $o_rendered_lines] ;#assume >=1 set nums [punk::range 1 $max] set numline_list [list] foreach n $nums ln $o_rendered_lines { lappend numline_list [list $n $ln] } switch -- [llength $args] { 0 {return $numline_list} 1 {return [lindex $numline_list [lindex $args 0]]} 2 {return [lrange $numline_list {*}$args]} default {error "lines expected 0,1 or 2 indices"} } } #1-based method delete_line {linenum} { error "unimplemented" if {$linenum eq "end"} { set linenum [llength $o_rendered_lines] } if {![string is integer -strict $linenum]} { error "delete_line accepts only end or an integer from 1 to linecount" } if {$linenum == 0} { error "minimum line is 1" } set o_rendered_lines [lreplace $o_rendered_lines $index $index] } #clear data from last line only method clear_tail {} { set o_cursor_row [llength $o_rendered_lines] set o_cursor_col 1 lset o_rendered_lines end "" } #1-based method view_lines {args} { set llist [my lines {*}$args] return [join $llist \n] } method view_lines_numbered {args} { set ANSI_linenum [a+ green] set RST [a] set llist [my lines_numbered {*}$args] set nums [lsearch -all -inline -index 0 -subindices $llist *] lset nums $o_cursor_row-1 "[a+ bold underline]$o_cursor_row${RST}$ANSI_linenum" set lines [lsearch -all -inline -index 1 -subindices $llist *] set cursorline [lindex $lines $o_cursor_row-1] set charindex_at_cursor [ansistring COLUMNINDEX $cursorline $o_cursor_col] if {$charindex_at_cursor ne ""} { lassign [ansistring INDEXCOLUMNS $cursorline $charindex_at_cursor] col0 col1 #we now have the column extents of the possibly double-wide character at the cursor #we can apply ansi just to those columns using a transparent overtype set prefix [string repeat " " [expr {$col0 -1}]] set linecols [punk::ansi::printing_length $cursorline] set suffix [string repeat " " [expr {$linecols -$col1}]] #capitalised INDEX - for grapheme/control-char index e.g a with diacritic a\u0300 has a single index set char_at_cursor [ansistring INDEX $cursorline $charindex_at_cursor] ;#this is the char with appropriate ansireset codes set rawchar [punk::ansi::stripansi $char_at_cursor] if {$rawchar eq " "} { set charhighlight "[punk::ansi::a+ White]_[a]" } else { set charhighlight [punk::ansi::a+ reverse]$char_at_cursor[a] } set cursorline [overtype::renderline -transparent 1 -insert_mode 0 -overflow 0 $cursorline $prefix$charhighlight$suffix] lset lines $o_cursor_row-1 $cursorline } set numcol "$ANSI_linenum[join $nums \n][a]" set linecol [join $lines \n] return [textblock::join $numcol " " $linecol] } method debugview_lines {} { set result "" foreach ln $o_rendered_lines { append result [ansistring VIEW -lf 1 -vt 1 $ln] \n ;#should be no lf or vt - but if there is.. we'd better show it } append result \n "cursor row: $o_cursor_row col: $o_cursor_col" return $result } method last_char {} { return [string index [lindex $o_chunk_list end] end] } #more strictly - last non-ansi? method last_grapheme {} { set lastchunk [lindex $o_chunk_list end] set plaintext_parts [punk::ansi::ta::split_at_codes $lastchunk] set pt [lindex $plaintext_parts end] if {$pt eq ""} { set pt [lindex $plaintext_parts end-1] } set graphemes [punk::char::grapheme_split $pt] return [lindex $graphemes end] } method last_ansi {} { set lastchunk [lindex $o_chunk_list end] set parts [punk::ansi::ta::split_codes_single $lastchunk] set lastcode [lindex $parts end-1] return $lastcode #return [ansistring VIEW -lf 1 $lastcode] } method chunks {args} { switch -- [llength $args] { 0 {return $o_chunk_list} 1 {return [lindex $o_chunk_list [lindex $args 0]]} 2 {return [lrange $o_chunk_list {*}$args]} default {error "chunks expected 0,1 or 2 arguments (index or range)"} } } method view_chunks {} { set result "" set dashes [string repeat - 20] foreach arrival_chunk $o_chunk_list chunk_id [dict keys $o_chunk_info] { set chunk_info [dict get $o_chunk_info $chunk_id] append result $dashes \n set micros [dict get $chunk_info micros] append result "$chunk_id arrival: [clock format [expr {$micros / 1000000}] -format "%Y-%m-%d %H:%M:%S"] ($micros)" \n append result $dashes \n append result $arrival_chunk \n } return $result } method debugview_chunks {} { set result "" foreach ln $o_chunk_list { append result [ansistring VIEW -lf 1 -vt 1 $ln] \n } append result \n "cursor row: $o_cursor_row col: $o_cursor_col" return $result } method view_raw {} { return [join $o_chunk_list ""] } method debugview_raw {} { set sublf [ansistring VIEW -lf 1 \n] #set subvt [ansistring VIEW -lvt 1 \v] ;#vt replacement with $subvt\v will not align accurately.. todo ? return [string map [list $sublf $sublf\n] [ansistring VIEW -lf 1 -vt 0 [join $o_chunk_list ""]]] } } } proc repl::repl_handler_checkchannel {inputchan} { if {$::repl::signal_control_c > 0 || [chan eof $inputchan]} { if {[lindex $::errorCode 0] eq "CHILDKILLED"} { #rputs stderr "\n|repl> ctrl-c errorCode: $::errorCode" #avoid spurious triggers after interrupting a command.. #review - dodgy.. we just want to interrupt child processes but then still be able to interrupt repl set ::repl::signal_control_c 0 set preverr [string map [list "child killed" "child_killed"] $::errorInfo] catch {error $preverr} ;#for errorInfo display } else { set ::repl::signal_control_c 0 fileevent $inputchan readable {} set reading 0 set running 0 if {$::tcl_interactive} { rputs stderr "\n|repl> EOF on $inputchan." } set [namespace current]::done 1 after 1 [list repl::reopen_stdin] #tailcall repl::reopen_stdin } } } proc repl::repl_handler_restorechannel {inputchan previous_input_state} { if {[chan conf $inputchan] ne $previous_input_state} { set restore_input_conf [dict remove $previous_input_state -inputmode] ;#Attempting to set input mode often gives permission denied on windows - why? if {[catch { chan conf $inputchan {*}$restore_input_conf } errM]} { rputs stderr "|repl>original: [ansistring VIEW $previous_input_state]" rputs stderr "|repl>current : [ansistring VIEW [chan conf $inputchan]]" rputs stderr "\n|repl> Failed to return $inputchan to original state" rputs stderr "|repl>ERR: $errM" } } return [chan conf $inputchan] } proc repl::repl_handler {inputchan prompt_config} { variable in_repl_handler set in_repl_handler [list $inputchan $prompt_config] fileevent $inputchan readable {} upvar ::punk::console::input_chunks_waiting input_chunks_waiting #note -inputmode not available in Tcl 8.6 for chan configure! set rawmode 0 set original_input_conf [chan configure $inputchan] ;#whether repl is in line or raw mode - we restore the stdin state if {[dict exists $original_input_conf -inputmode]} { if {[dict get $original_input_conf -inputmode] eq "raw"} { #user or script has apparently put stdin into raw mode - update punk::console::is_raw to match set rawmode 1 set ::punk::console::is_raw 1 } else { set ::punk::console::is_raw 0 } #what about enable/disable virtualTerminal ? #using stdin -inputmode to switch modes won't set virtualterminal input state appropriately #we expect the state of -inputmode to be 'normal' even though we flip it during the read part of our repl loop #if it's been set to raw - assume it is deliberately done this way as the user could have alternatively called punk::mode raw or punk::console::enableVirtualTerminal #by not doing this automatically - we assume the caller has a reason. } else { set rawmode [set ::punk::console::is_raw] } if {!$rawmode} { #stdin with line-mode readable events (at least on windows for Tcl 8.7a6 to 9.0a) can get stuck with bytes pending when input longer than 100chars - even though there is a linefeed further on than that. #This potentially affects a reasonable number of Tcl8.7 kit/tclsh binaries out in the wild. #see bug https://core.tcl-lang.org/tcl/tktview/bda99f2393 (gets stdin problem when non-blocking - Windows) #when in non-blocking mode we will have to read that in to get further - but we don't know if that was the end of line or if there is more - and we may not get a newline even though one was present originally on stdin. #presence of 8.7 buffering bug will result in unrecoverable situation - even switching to raw and using read will not be able to retrieve tail data. #the readable event only gives us 200 bytes (same problem may be at 4k/8k in other versions) #This occurs whether we use gets or read - set stdinlines [list] set linemax 5 ;#not an absolute.. set lc 0 if {[dict get $original_input_conf -blocking] ne "0"} { chan configure $inputchan -blocking 0 } set waitingchunk "" #review - input_chunks_waiting in line mode - if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { #puts stderr "repl_handler input_chunks_waiting($inputchan) while in line mode. Had data:[ansistring VIEW -lf 1 $input_chunks_waiting($inputchan)]" set allwaiting [join $input_chunks_waiting($inputchan) ""] set input_chunks_waiting($inputchan) [list] set yellow [punk::ansi::a+ yellow bold] set waitinglines [split $allwaiting \n] foreach ln [lrange $waitinglines 0 end-1] { lappend stdinlines $ln incr lc } set waitingchunk [lindex $waitinglines end] # -- #set chunksize [gets $inputchan chunk] set chunk [read $inputchan] set chunksize [string length $chunk] # -- if {$chunksize > 0} { if {[string index $chunk end] eq "\n"} { lappend stdinlines $waitingchunk[string range $chunk 0 end-1] #punk::console::cursorsave_move_emitblock_return 30 30 "repl_handler num_stdinlines [llength $stdinlines] chunk:$yellow[ansistring VIEW -lf 1 $chunk][a] fblocked:[fblocked $inputchan] pending:[chan pending input stdin]" if {![chan eof $inputchan]} { repl_handler_restorechannel $inputchan $original_input_conf } uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] } else { set input_chunks_waiting($inputchan) [list $allwaiting] lappend input_chunks_waiting($inputchan) $chunk } } else { if {[fblocked $inputchan]} { #set screeninfo [punk::console::get_size] #lassign $screeninfo _c cols _r rows set rows 0 set cols 3 if {[string is integer -strict $rows]} { set RED [punk::ansi::a+ red bold]; set RST [punk::ansi::a] set msg "${RED}line-length Tcl windows channel bug? Hit enter to continue$RST" set msglen [ansistring length $msg] punk::console::cursorsave_move_emitblock_return $rows [expr {$cols - $msglen -1}] $msg } after 100 } set input_chunks_waiting($inputchan) [list $allwaiting] } } else { repl_handler_checkchannel $inputchan # -- --- --- #set chunksize [gets $inputchan chunk] # -- --- --- set chunk [read $inputchan] set chunksize [string length $chunk] # -- --- --- if {$chunksize > 0} { #punk::console::cursorsave_move_emitblock_return 35 120 "chunk: [ansistring VIEW -lf 1 "...[string range $chunk end-10 end]"]" set ln $chunk ;#temp #punk::console::cursorsave_move_emitblock_return 25 30 [textblock::frame -title line "[a+ green]$waitingchunk[a][a+ red][ansistring VIEW -lf 1 $ln][a+ green]pending:[chan pending input stdin][a]"] if {[string index $ln end] eq "\n"} { lappend stdinlines [string range $ln 0 end-1] incr lc if {![chan eof $inputchan]} { repl_handler_restorechannel $inputchan $original_input_conf } uplevel #0 [list repl::repl_process_data $inputchan line "" $stdinlines $prompt_config] } else { lappend input_chunks_waiting($inputchan) $ln } } } } else { if {[info exists input_chunks_waiting($inputchan)] && [llength $input_chunks_waiting($inputchan)]} { #we could concat and process as if one chunk - but for now at least - we want to preserve the 'chunkiness' set wchunks $input_chunks_waiting($inputchan) set ch [lindex $wchunks 0] set input_chunks_waiting($inputchan) [lrange $wchunks 1 end] uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $ch [list] $prompt_config] } else { repl_handler_checkchannel $inputchan if {[dict get $original_input_conf -blocking] ne "0" || [dict get $original_input_conf -translation] ne "lf"} { chan configure $inputchan -blocking 0 chan configure $inputchan -translation lf } set chunk [read $inputchan] if {![chan eof $inputchan]} { repl_handler_restorechannel $inputchan $original_input_conf } uplevel #0 [list repl::repl_process_data $inputchan raw-read $chunk [list] $prompt_config] while {[join $input_chunks_waiting($inputchan)] ne ""} { #puts "...[llength $input_chunks_waiting($inputchan)]" set wchunks $input_chunks_waiting($inputchan) set ch [lindex $wchunks 0] set input_chunks_waiting($inputchan) [lrange $wchunks 1 end] uplevel #0 [list repl::repl_process_data $inputchan raw-waiting $ch [list] $prompt_config] } } } if {![chan eof $inputchan]} { ################################################################################## #Re-enable channel read handler only if no waiting chunks - must process in order ################################################################################## if {![llength $input_chunks_waiting($inputchan)]} { fileevent $inputchan readable [list ::repl::repl_handler $inputchan $prompt_config] } else { after idle [list ::repl::repl_handler $inputchan $prompt_config] } #################################################### } else { #rputs stderr "repl_handler EOF inputchannel:[chan conf $inputchan]" repl_handler_checkchannel $inputchan } set in_repl_handler [list] } proc repl::editbuf {index args} { variable editbuf_list set editbuf [lindex $editbuf_list $index] if {$editbuf ne ""} { $editbuf {*}$args } else { return "No such index in editbuf list" } } interp alias {} editbuf {} ::repl::editbuf proc repl::repl_process_data {inputchan type chunk stdinlines prompt_config} { variable loopinstance variable loopcomplete incr loopinstance set moredata 0 upvar ::punk::console::input_chunks_waiting input_chunks_waiting try { variable prompt_reset_flag #catch {puts stderr "xx--->[rep $::arglej]"} if {$prompt_reset_flag == 1} { set prompt_config [get_prompt_config] set prompt_reset_flag 0 } variable last_repl_char "" ;#last char emitted by this handler to stdout/stderr variable lastoutchar "" variable lasterrchar "" variable cursorcolumn "" variable commandstr # --- variable editbuf variable editbuf_list variable editbuf_linenum_submitted # --- variable readingchunk variable running variable reading variable post_script variable id_outstack upvar ::punk::last_run_display last_run_display upvar ::punk::config::running running_config set resultprompt [dict get $prompt_config resultprompt] set nlprompt [dict get $prompt_config nlprompt] set infoprompt [dict get $prompt_config infoprompt] set debugprompt [dict get $prompt_config debugprompt] #JMN #fileevent $inputchan readable {} #According to DKF - -buffering option doesn't affect input channels if {$cursorcolumn eq ""} { set cursorcolumn 1 } # -- --- --- #for raw mode set chunkreadsize 1024 set maxreads 4 set linemax 40 ;#max number of lines received for us to read another chunk in same loop - *not a limit on number of lines in a round* #Note - we could read for example 1024 lines if they fit in our chunk read size - and we'll have to process them all, but if 1024 > $linemax we won't read more available data in this round. # -- --- --- set rawmode [set ::punk::console::is_raw] if {!$rawmode} { #puts stderr "-->got [ansistring VIEW -lf 1 $stdinlines]<--" } else { #raw set numreads 0 set lc 0 set onetime 1 while {$onetime && [string length $chunk] >= 0 } { set onetime 0 set chunklen [string length $chunk] #punk::console::move_emitblock_return 20 120 $chunklen-->[chan conf stdin]<-- if {$chunklen > 0} { set info1 "read $chunklen bytes->[ansistring VIEW -lf 1 -vt 1 $chunk]" #consider also the terminal linefeed mode. #https://vt100.net/docs/vt510-rm/LNM.html # terminals (by default) generally use a lone cr to represent enter (LNM reset ie CSI 20l) #(as per above doc: "For compatibility with Digital's software you should keep LNM reset (line feed)") #You can insert an lf using ctrl-j - and of course stdin could have crlf or lf #pasting from notepad++ with mixed line endings seems to paste everything ok #we don't really know the source of input keyboard vs paste vs pipe - and whether a read has potentially chopped a crl in half.. #possibly no real way to determine that. We could wait a small time to see if there's more data coming.. and potentially impact performance. #Instead we'll try to make sense of it here. if {$chunklen == 1} { #presume it's a keypress from terminal set chunk [string map [list \r \n] $chunk] } else { #maybe a paste? (or stdin to active shell loop - possibly with no terminal ? ) #we'd better check for crlf and/or plain lf. If found - presume any lone CR is to be left as is. if {[string first \n $chunk] < 0} { set chunk [string map [list \r \n] $chunk] } #else - #has lf - but what if last char is cr? #It may require user to hit enter - probably ok. #could be a sequence of cr's from holding enter key } #review - we can receive chars such as escapes or arrow inline with other data even from keyboard if keys are pushed quickly (or automated?) # - so we probably shouldn't really rely on whether a char arrives alone in a chunk as a factor in its behaviour #On the other hand - timing of keystrokes could be legitimate indications of intention in a cli ? #esc or ctrl-lb if {$chunk eq "\x1b"} { #return #set readingchunk "" set stdinlines [list "\x1b"] set commandstr "" set chunk "" $editbuf clear_tail screen_last_char_add \x1b stdin escape break } #if ProcessedInput is disabled - we can get ctrl-c #e.g with punk::console::disableProcessedInput #if we get just ctrl-c in one chunk #ctrl-c if {$chunk eq "\x03"} { #::repl::term::handler_console_control "ctrl-c_via_rawloop" error "character 03 -> ctrl-c" } #for now - exit with small delay for tidyup #ctrl-z if {$chunk eq "\x1a"} { #::repl::term::handler_console_control "ctrl-z_via_rawloop" punk::mode line after 1000 exit return } if {$chunk eq "\x7f"} { set chunk "\b\x7f" } #ctrl-bslash if {$chunk eq "\x1c"} { #try to brutally terminate process #attempt to leave terminal in a reasonable state punk::mode line after 200 {exit 42} } if {$chunk eq "\x1b\[D"} { #move cursor record within our buffer #rputs stderr "${debugprompt}arrow-left D" #set commandstr "" #punk::console::move_back 1 ;#terminal does it anyway? } $editbuf add_chunk $chunk #-------------------------- if {[set ::punk::console::ansi_available]} { package require textblock #experimental - use punk::console::get_size to determine current visible width. #This should ideally be using sigwinch or some equivalent to set a value somewhere. #testing each time is very inefficient (1+ms) #unfortunately there isn't an easy way to get such an event on windows console based systems - REVIEW. set do_checkwidth 1 ;#make configurable if performance hit is too severe? TODO if {$do_checkwidth} { set consolewidth [dict get [punk::console::get_size] columns] } else { set consolewidth 132 ;#todo - something better! } set debug_width 0 if {$::punk::repl::debug_repl > 0} { set lastc [string index $chunk end] set lastc [ansistring VIEW -lf 1 -vt 1 $lastc] if {[string length $lastc]} { #set info [textblock::frame [textblock::block 10 10 $lastc]] } if {[catch { set info [$editbuf debugview_raw] if {$type eq "raw-waiting"} { set info [a+ bold yellow]$info[a] } else { set info [a+ green]$info[a] } set lines [lines_as_list -ansiresets 1 $info] if {[llength $lines] > 20} { set lines [lrange $lines end-19 end] set info [list_as_lines $lines] } } errM]} { set info [textblock::frame -buildcache 0 -title "[a red]error[a]" $errM] } else { set info [textblock::frame -buildcache 0 -ansiborder [a+ green bold] -title "[a cyan]debugview_raw[a]" $info] } set debug_width [textblock::widthtopline $info] set spacepatch [textblock::block $debug_width 2 " "] puts -nonewline [punk::ansi::cursor_off] #use non cursorsave versions - cursor save/restore will interfere with any concurrent ansi rendering that uses save/restore - because save/restore is a single item, not a stack. set debug_offset [[expr {$consolewidth - $debug_width - 2}]] punk::console::move_emitblock_return 8 $debug_offset $spacepatch punk::console::move_emitblock_return 10 $debug_offset $info puts -nonewline [punk::ansi::cursor_on] } if {[catch { #set info [$editbuf view_lines] set info [$editbuf view_lines_numbered] set lines [lines_as_list -ansiresets 1 $info] if {[llength $lines] > 20} { set lines [lrange $lines end-19 end] set info [list_as_lines $lines] } } editbuf_error]} { set info [textblock::frame -buildcache 0 -title "[a red]error[a]" "$editbuf_error\n$::errorInfo"] } else { set title "[a cyan]editbuf [expr {[llength $editbuf_list]-1}] lines [$editbuf linecount][a]" append title "[a+ yellow bold] col:[format %3s [$editbuf cursor_column]] row:[$editbuf cursor_row][a]" set row1 " lastchar:[ansistring VIEW -lf 1 [$editbuf last_char]] lastgrapheme:[ansistring VIEW -lf 1 [$editbuf last_grapheme]]" set row2 " lastansi:[ansistring VIEW -lf 1 [$editbuf last_ansi]]" set info [a+ green bold]$row1\n$row2[a]\n$info set info [textblock::frame -buildcache 0 -ansiborder [a+ green bold] -title $title $info] } set editbuf_width [textblock::widthtopline $info] set spacepatch [textblock::block $editbuf_width 2 " "] set editbuf_offset [expr {$consolewidth - $debug_width - $editbuf_width - 2}] punk::console::cursorsave_move_emitblock_return 8 $editbuf_offset $spacepatch punk::console::cursorsave_move_emitblock_return 10 $editbuf_offset $info } set lines_unsubmitted [expr {[$editbuf linecount] - $editbuf_linenum_submitted}] #there is always one 'line' unsubmitted - although it may be the current one being built, which may be empty string if {$lines_unsubmitted < 1} { puts stderr "repl editbuf_linenum_submitted out of sync with editbuf" } #set trailing_line_index [expr {[$editbuf linecount] -1}] set last_line_num [$editbuf linecount] #set nextsubmit_index [expr {$editbuf_lineindex_submitted + 1}] set nextsubmit_line_num [expr {$editbuf_linenum_submitted + 1}] set cursor_row [$editbuf cursor_row] set cursor_index [expr {$cursor_row -1}] set lastansi [$editbuf last_ansi] if {$lastansi eq "\x1b\[A"} { if {$cursor_row > 1} { puts -nonewline stdout "\x1b\[A" } } elseif {$lastansi eq "\x1b\[B"} { puts -nonewline stdout "\x1b\[B" } flush stdout set offset 3 puts -nonewline stdout [a+ cyan][punk::ansi::move_column [expr {$offset +1}]][punk::ansi::erase_eol][$editbuf line $cursor_row][a][punk::ansi::move_column [expr {$offset + [$editbuf cursor_column]}]] #puts -nonewline stdout $chunk flush stdout if {[$editbuf last_char] eq "\n"} { set linelen [punk::ansi::printing_length [$editbuf line $nextsubmit_line_num]] puts -nonewline stdout [a+ cyan bold][punk::ansi::move_column [expr {$offset +1}]][$editbuf line $nextsubmit_line_num][a][punk::ansi::move_column [expr {$offset + $linelen +1}]] #screen_last_char_add "\n" input inputline puts -nonewline stdout [punk::ansi::erase_eol]\n #puts -nonewline stdout \n screen_last_char_add "\n" input inputline set waiting [$editbuf line end] if {[string length $waiting] > 0} { set waiting [a+ yellow bold]$waiting[a] #puts stderr "waiting $waiting" $editbuf clear_tail lappend input_chunks_waiting($inputchan) $waiting } } if {$editbuf_linenum_submitted == 0} { #(there is no line 0 - lines start at 1) if {[$editbuf last_char] eq "\n"} { lappend stdinlines [$editbuf line 1] incr lc set editbuf_linenum_submitted 1 } } else { if {$nextsubmit_line_num < $last_line_num} { foreach ln [$editbuf lines $nextsubmit_line_num end-1] { lappend stdinlines $ln incr lc incr editbuf_linenum_submitted } } } set last_cursor_column [$editbuf cursor_column] } else { #rputs stderr "->0byte read stdin" if {[chan eof $inputchan]} { fileevent $inputchan readable {} set reading 0 set running 0 if {$::tcl_interactive} { rputs stderr "\n|repl> EOF on $inputchan." } set [namespace current]::done 1 #test #JMN #tailcall repl::reopen_stdin } #break } } } set maxlinenum [expr {[llength $stdinlines] -1}] set linenum 0 foreach line $stdinlines { #puts stderr "----->line: [ansistring VIEW -lf 1 $line] commandstr:[ansistring VIEW -lf 1 $commandstr]" set last_repl_char "" ;#last char emitted by this handler to stdout/stderr set lastoutchar "" set lasterrchar "" #consider \x1b as text on console vs \x1b the character #review - if we're getting these actual escape characters in line mode.. something is off - let's emit something instead of trying to interpret as a command and failing. #This tends to happen when some sort of readline not avaialbe ie on unix or mintty in windows #this only captures leading escape.. as an aid to diagnosis e.g won't be caught and the user will need to close the right bracket to complete the bogus command #we may need to think about legitimate raw escapes in commands e.g from pipes or script files, vs via console? #esc key or ctrl-lb followed by enter if {$line eq "\x1b"} { #abort current command if {$linenum == 0} { doprompt "E% " {yellow bold} set line "" #screen_last_char_add " " empty empty } else { doprompt "\nE% " {yellow bold} #screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required } incr linenum continue } else { if {$line eq "\x1b\[C"} { rputs stderr "${debugprompt}arrow-right C" #set commandstr "" } if {$line eq "\x1b\[D"} { #rputs stderr "${debugprompt}arrow-left D" #set commandstr "" #punk::console::move_back 1 } if {$line eq "\x1b\[A"} { rputs stderr "${debugprompt}arrow-up A" } if {$line eq "\x1b\[B"} { rputs stderr "arrow-down B" } if {[string match "\x1b*" $line]} { rputs stderr "${debugprompt}esc - '[punk::ansi::ansistring::VIEW $line]'" #set commandstr [punk::ansi::stripansi $commandstr] } } if {$commandstr ne ""} { append commandstr \n } set stdinconf [fconfigure $inputchan] if {$::tcl_platform(platform) eq "windows" && [dict get $stdinconf -encoding] ni [list unicode utf-16 utf-8]} { #some long console inputs are split weirdly when -encoding and -translation are left at defaults - requiring extra enter-key to get repl to process. #experiment to see if using binary and handling line endings manually gives insight. # - do: chan conf stdin -encoding binary -translation lf #first command after configuring stdin this way seems to be interpreted with wrong encoding - subsequent commands work - review #this branch only works on tcl8.7+ #It seems to fix the issue with holding down enter-key and getting extra blank lines, but # it breaks copy-paste (encoding issue?) #puts "--inputchan:$inputchan> [fconfigure $inputchan]" append commandstr $line puts "1=============>[string length $commandstr] bytes , [ansistring VIEW $commandstr] , info complete:[info complete $line] stdinconf:$stdinconf" set commandstr [string range $commandstr 0 end-3] set commandstr [encoding convertfrom utf-16be $commandstr] ;#This is weird - but it seems to be big endian? set commandstr [string trimright $commandstr] #puts "2=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" } else { #append commandstr $line #puts "0=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" append commandstr $line } #puts "=============>[string length $commandstr] bytes , [string map [list \r -r- \n -n-] $commandstr] , info complete:[info complete $line]" set ::repl::last_repl_char "\n" ;#this is actually the eol from stdin screen_last_char_add "\n" stdin $line #append commandstr \n if {[info complete $commandstr] && [string index $commandstr end] ne "\\"} { #set commandstr [overtype::renderline -overflow 1 "" $commandstr] set ::repl::output_stdout "" set ::repl::output_stderr "" set outstack [list] set errstack [list] #oneshot repl debug set wordparts [regexp -inline -all {\S+} $commandstr] lassign $wordparts cmd_firstword cmd_secondword if {$cmd_firstword eq "debugrepl"} { if {$cmd_secondword in [list 0 cancel]} { set ::punk::repl::debug_repl 0 } else { if {[string is integer -strict $cmd_secondword]} { incr ::punk::repl::debug_repl $cmd_secondword } else { incr ::punk::repl::debug_repl } } set commandstr "set ::punk::repl::debug_repl" } if {$::punk::repl::debug_repl > 100} { proc debug_repl_emit {msg} [string map [list %p% [list $debugprompt]] { set p %p% #don't auto-append \n even if missing. #we may want to use debug_repl_emit with multiple calls for one output line #if {[string index $msg end] ne "\n"} { # set msg "$msg\n" #} #set last_char [string index $::repl::screen_last_chars end] set last_char [screen_last_char_get] if {$last_char ne "\n"} { set clearance "\n" } else { set clearance "" } #use pseudo-channel debugreport rputs debugreport $clearance$p[string map [list \n \n$p] $msg] }] set info "" append info "repl loopinstance: $loopinstance debugrepl remaining: [expr {[set ::punk::repl::debug_repl]-1}]\n" append info "commandstr: [punk::ansi::ansistring::VIEW $commandstr]\n" append info "last_run_info\n" append info "length: [llength $::punk::last_run_display]\n" append info "namespace: $::punk::ns::ns_current" debug_repl_emit $info } else { proc debug_repl_emit {msg} {return} } #----------------------------------------- #review! #work around weird behaviour in tcl 8.6 & 8.7a5 (at least) part1 #https://wiki.tcl-lang.org/page/representation #/scriptlib/tests/listrep_bug.tcl #after the uplevel #0 $commandstr call # vars within the script that were set to a list, and have no string-rep, will generate a string-rep once the script (commandstr) is unset, or set to another value global run_command_string set run_command_string "$commandstr\n" ;#add anything that won't affect script. global run_command_cache #----------------------------------------- set ::punk::last_run_display [list] set ::repl::last_unknown "" #*********************************************************** #don't use puts,rputs or debug_repl_emit in this block #*********************************************************** if {[string length [dict get $running_config color_stdout]] && [punk::console::colour]} { lappend outstack [shellfilter::stack::add stdout ansiwrap -settings [list -colour [dict get $running_config color_stdout]]] } lappend outstack [shellfilter::stack::add stdout tee_to_var -settings {-varname ::repl::output_stdout}] if {[string length [dict get $running_config color_stderr]] && [punk::console::colour]} { lappend errstack [shellfilter::stack::add stderr ansiwrap -settings [list -colour [dict get $running_config color_stderr]]] } lappend errstack [shellfilter::stack::add stderr tee_to_var -settings {-varname ::repl::output_stderr}] #chan configure stdout -buffering none #JMN fileevent $inputchan readable {} set reading 0 #don't let unknown use 'args' to convert commandstr to list #=============================================================================== #Actual command call #puts "____>[rep $commandstr]" #=============================================================================== if {[string equal -length [string length "repl_runraw "] "repl_runraw " $commandstr]} { #pass unevaluated command to runraw set status [catch {uplevel #0 [list runraw $commandstr]} raw_result] } else { #puts stderr "repl uplevel 0 '$run_command_string'" set status [catch { #uplevel 1 $run_command_string #uplevel 1 {namespace eval $::punk::ns::ns_current $run_command_string} set weirdns 0 set parts [punk::ns::nsparts $::punk::ns::ns_current] foreach p $parts { if {[string match :* $p] || [string match *: $p]} { set weirdns 1 break } } if {$weirdns} { uplevel 1 {punk::ns::nseval $::punk::ns::ns_current $run_command_string} } else { #puts stderr "--> [ansistring VIEW -lf 1 -vt 1 $run_command_string] <--" uplevel 1 {namespace inscope $::punk::ns::ns_current $run_command_string} } } raw_result] } #puts stderr "repl raw_result: $raw_result" #set result $raw_result #append result ""; #copy on write #copy on write #append result $raw_result "" set result [string cat $raw_result ""] #puts stderr "-->>$result<--" #=============================================================================== flush stdout flush stderr foreach s [lreverse $outstack] { shellfilter::stack::remove stdout $s } foreach s [lreverse $errstack] { shellfilter::stack::remove stderr $s } #----------------------------------------- #list/string-rep bug workaround part 2 #todo - set flag based on punk::repl::has_script_var_bug lappend run_command_cache $run_command_string #puts stderr "run_command_string rep: [rep $run_command_string]" if {[llength $run_command_cache] > 2000} { set run_command_cache [lrange $run_command_cache 1750 end] } #----------------------------------------- set lastoutchar [string index [punk::ansi::stripansi $::repl::output_stdout] end] set lasterrchar [string index [punk::ansi::stripansi $::repl::output_stderr] end] #to determine whether cursor is back at col0 of newline screen_last_char_add [string index $lastoutchar$lasterrchar end] "stdout/stderr" set result_is_chunk_list 0 #------ #todo - fix. It doesn't make much sense to only detect if the unknown command occurred in first word. #e.g set x [something arg] not detected vs something arg #also - unknown commands aren't the only things that can write directly to the os handles stderr & stdout if { [string length $::repl::last_unknown] && \ [string equal -length [string length $::repl::last_unknown] $::repl::last_unknown $line] } { #can't currently detect stdout/stderr writes from unknown's call to exec #add a clearance newline for direct unknown calls for now #there is usually output anyway - but we will get an extra blank line now even for a call that only had an exit code # # set unknown_clearance "\n* repl newline" screen_last_char_add "\uFFFF" clearance "clearance after direct unknown call" if {[llength $last_run_display]} { if {$status == 0} { set result $last_run_display } else { } set result_is_chunk_list 1 } } #------ #ok to use repl::screen_needs_clearance from here down.. (code smell proc only valid use in narrow context) #*********************************************************** #rputs -nonewline stderr $unknown_clearance if {$::punk::repl::debug_repl > 0} { set lastcharinfo "\n" set whatcol [string repeat " " 12] foreach cinfo $::repl::screen_last_char_list { lassign $cinfo c whatinfo whyinfo set cdisplay [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $c] #assertion cdisplay has no raw newlines if {[punk::char::ansifreestring_width $cdisplay] == 1} { set cdisplay "$cdisplay " ;#make 2 wide } if {[string match repl-debugreport* $whatinfo]} { #exclude noise debug_repl_emit - but still show the last_char set whysummary "" } else { #set whysummary [string map [list \n "-n-"] $whyinfo] set whysummary [punk::ansi::ansistring::VIEW -lf 1 -vt 1 $whyinfo] } set whatinfo [string range $whatinfo$whatcol 0 [string length $whatcol]] append lastcharinfo "$cdisplay $whatinfo $whysummary\n" } debug_repl_emit "screen_last_chars: $lastcharinfo" } debug_repl_emit "lastoutchar:'$lastoutchar' lasterrchar: '$lasterrchar'" if {$status == 0} { debug_repl_emit "command call status: $status OK" } else { debug_repl_emit "command call status: $status ERR" } #puts stderr "'$::repl::output_stdout' lastoutchar:'$lastoutchar' result:'$result'" #$command is an unevaluated script at this point # so may not be a well formed list e.g 'set x [list a "b"]' #- lindex will fail #if {[lindex $command 0] eq "runx"} {} if { [string equal -length [string length "d/ "] "d/ " $commandstr] || \ [string equal "d/\n" $commandstr] || \ [string equal -length [string length "dd/ "] "dd/ " $commandstr] || \ [string equal "dd/\n" $commandstr] || \ [string equal -length [string length "./ "] "./ " $commandstr] || \ [string equal "./\n" $commandstr] || \ [string equal -length [string length "../ "] "../ " $commandstr] || \ [string equal "../\n" $commandstr] || \ [string equal -length [string length "runx "] "runx " $commandstr] || \ [string equal -length [string length "sh_runx "] "sh_runx " $commandstr] || \ [string equal -length [string length "runout "] "runout " $commandstr] || \ [string equal -length [string length "sh_runout "] "sh_runout " $commandstr] || \ [string equal -length [string length "runerr "] "runerr " $commandstr] || \ [string equal -length [string length "sh_runerr "] "sh_runerr " $commandstr] } { if {[llength $last_run_display]} { set result $last_run_display set result_is_chunk_list 1 } } # -- --- --- --- --- --- --- --- --- --- ##an attempt to preserve underlying rep ##this is not for performance - just to be less disruptive to underlying rep to aid in learning/debugging # -- --- --- --- --- --- --- --- --- --- # JN 2023 - The lrange operation is destructive to path intrep # The lrange operation is destructive to strings with leading/trailing newlines # -- --- --- --- --- --- --- --- --- --- #set saved_errorCode $::errorCode #set saved_errorInfo $::errorInfo #if {[catch {lrange $result 0 end} result_as_list]} { # set is_result_empty [expr {$result eq ""}] # set ::errorCode $saved_errorCode # set ::errorInfo $saved_errorInfo #} else { # set is_result_empty [expr {[llength $result_as_list] == 0}] #} # -- --- --- --- --- --- --- --- --- --- #set resultrep [::tcl::unsupported::representation $result] set is_result_empty [expr {$result eq ""}] #catch {puts stderr "yy--->[rep $::arglej]"} set reading 1 if {!$is_result_empty} { if {$status == 0} { if {[screen_needs_clearance]} { rputs -nonewline stderr \n } if {$result_is_chunk_list} { foreach c $result { lassign $c termchan text if {[string length $text]} { switch -- $termchan { result { #rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] set h [textblock::height $text] set promptcol [string repeat $resultprompt\n $h] set promptcol [string range $promptcol 0 end-1] rputs [textblock::join -- $promptcol $text] #puts -nonewline stdout $text } resulterr { rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] } info { rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] } default { #rputs -nonewline $termchan $text set chanprompt "_ " rputs $termchan ${chanprompt}[string map [list \n "\n${chanprompt}"] $text] } } } } } else { #----------------------------------------------------------- # avoid repl forcing string rep of simple results. This is just to aid analysis using tcl::unsupported::representation #set rparts [split $result {}] #if {[lsearch $rparts \n] < 0} { # #type of $result unaffected # rputs "$resultprompt $result" #} else { # #$result will be a string due to use of string map # rputs $resultprompt[string map [list \n "\n$resultprompt"] $result] #} #----------------------------------------------------------- #we have copied rawresult using append with empty string - so our string interaction with result var here shouldn't affect the returned value #empty-string result handled in other branch set flat [string map [list \r\n "" \n ""] $result] if {[string length $flat] == [string length $result]} { #no line-endings in data rputs "$resultprompt$result" } else { #if {[string index $result end] eq "\n"} { # set result [string range $result 0 end-1] #} if {[string length $flat] == 0} { if {[string range $result end-1 end] eq "\r\n"} { set result [string range $result 0 end-2] } else { set result [string range $result 0 end-1] } } #NOTE - textblock::height is the line height - not reflective of height of data with ansi-moves or things like sixels set h [textblock::height $result] set promptcol [string repeat $resultprompt\n $h] set promptcol [string range $promptcol 0 end-1] rputs [textblock::join -- $promptcol $result] #orig #rputs $resultprompt[string map [list \r\n "\n$resultprompt" \n "\n$resultprompt"] $result] } } doprompt "P% " } else { #tcl err if {$result_is_chunk_list} { foreach c $last_run_display { lassign $c termchan text if {[string length $text]} { switch -- $termchan { result { rputs stdout $resultprompt[string map [list \n "\n$resultprompt"] $text] #puts -nonewline stdout $text } resulterr { rputs stderr $resultprompt[string map [list \n "\n$resultprompt"] $text] } info { rputs stderr $infoprompt[string map [list \n "\n$infoprompt"] $text] } default { rputs -nonewline $termchan $text } } } } } set c [a yellow bold] set n [a] rputs stderr $c$result$n #tcl err hint prompt - lowercase doprompt "p% " } } else { #doprompt "P% " "green normal" if {$linenum == 0} { doprompt "P% " "green normal" screen_last_char_add " " empty empty } else { doprompt "\nP% " "green normal" screen_last_char_add "\n" empty empty ;#add \n to indicate noclearance required } } #catch {puts stderr "zz1--->[rep $::arglej]"} #puts stderr "??? $commandstr" if {$::punk::repl::debug_repl > 0} { incr ::punk::repl::debug_repl -1 } set commandstr "" #catch {puts stderr "zz2---->[rep $::arglej]"} set lines [$editbuf lines] set buf_has_data 0 foreach ln $lines { if {[string trim $ln] ne ""} { set buf_has_data 1 } } if {$buf_has_data} { set editbufnext [punk::repl::class::class_editbuf new {}] lappend editbuf_list $editbufnext set editbuf_linenum_submitted 0 set editbuf $editbufnext } #editbuf } else { #append commandstr \n if {$::repl::signal_control_c} { set ::repl::signal_control_c 0 fileevent $inputchan readable {} rputs stderr "* console_control: control-c" flush stderr set c [a yellow bold] set n [a] rputs stderr "${c}repl interrupted$n" #set commandstr [list error "repl interrupted"] set commandstr "" doprompt ">_ " flush stdout } else { #Incomplete command # parse and determine outermost unclosed quote/bracket and include in prompt if {$linenum == $maxlinenum} { if {$rawmode} { #review #we haven't put the data following last le into commandstr - but we want to display proper completion status prior to enter being hit or more data coming in. #this could give spurious results for large pastes where buffering chunks it in odd places.? #it does however give sensible output for the common case of a small paste where the last line ending wasn't included #set waiting [punk::lib::system::incomplete $commandstr$readingchunk] set waiting [punk::lib::system::incomplete $commandstr[$editbuf line end]] } else { set waiting [punk::lib::system::incomplete $commandstr] } if {[llength $waiting]} { set c [lindex $waiting end] } else { #set c " " set c \u240a } doprompt ">$c " } } } incr linenum } if {$maxlinenum == -1} { #when in raw mode - no linefeed yet received #rputs stderr "repl: no complete input line: $commandstr" #screen_last_char_add "\n" empty empty #screen_last_char_add [string index $readingchunk end] stdinchunk stdinchunk screen_last_char_add [string index [$editbuf line end] end] stdinchunk stdinchunk } #fileevent $inputchan readable [list repl::repl_handler $inputchan $prompt_config] #catch {puts stderr "zend--->[rep $::arglej]"} } trap {POSIX} {e eopts} { rputs stderr "trap POSIX '$e' eopts:'$eopts" flush stderr } on error {repl_error erropts} { rputs stderr "error in repl_handler: $repl_error" rputs stderr "-------------" rputs stderr "$::errorInfo" rputs stderr "-------------" set stdinreader [fileevent $inputchan readable] if {![string length $stdinreader]} { rputs stderr "*> $inputchan reader inactive" } else { rputs stderr "*> $inputchan reader active" } if {[chan eof $inputchan]} { rputs stderr "will attempt restart of repl on input channel: $inputchan in next loop" catch {set ::punk::ns::ns_current "::"} } else { rputs stderr "continuing.." } flush stderr #tailcall repl::start $inputchan } } proc repl::completion {context ebuf} { } package provide punk::repl [namespace eval punk::repl { variable version set version 0.1 }] package provide punk::repl [namespace eval punk::repl { variable version set version 0.1 }] #repl::start stdin #exit 0 #repl::start $program_read_stdin_pipe