'''2004-06-16''' [SRIV] This is a minimal console text editor for Linux written in pure TCL. Usage: con-editor.tcl filename To save the file, press Ctrl-Q, it will prompt you to Save Y/n. Ctrl-Y will delete the current line. If you use this from a minicom serial console in Linux, switch minicom's terminal type from VT102 to ANSI. I'm using this on a Gumstix[http://www.gumstix.com/index.html] with tclsh, since there's no more space left for any larger editor binary. This takes up a whole 8538 bytes. ---- [LV] Perhaps the backspace key would work if you added a \u0008 (^h) to the current \u0007f (^?) case? ([JH]: Yes, added in.) ---- Edited by [JH] 2005-05-04 to support any size terminal, clean up the code for improved readability, add real tab handling (tricky) and some other tidbits. Anyone wanting to interpret more escape codes should find it easier to work with. '''Mar 19,2006''' [SRIV] I made a new version that adds tab handling using a different technique, as well a novel long line editing feature that doesn't redraw the whole screen to save on bandwidth over slow links. Grab it here [http://server.linuxsys.net/files/e] . ---- Mar 24,2006 [LES] I write "Isso é um teste de acentuação". Press Enter and it becomes "Isso ��© um teste de acentua��§��£o". Eh. [schlenk] Looks like it is read correctly and then transformed to utf-8, which the term does not display correctly. Changing the fconfigures for stdout/stdin to use some -encoding value may help. [SRIV] I added -encoding iso8859-1 to the two fconfigures and it seems to work. I'm not familiar enough with encodings to know what the right answer is though. ---- Screenshot: [http://www.sriv.net/images/con-editor.jpg] ---- #!/bin/sh # The next line is executed by /bin/sh, but not tcl \ exec tclsh "$0" ${1+"$@"} # con-editor.tcl a linux console based editor in pure tcl # 2004-06-16 Steve Redler IV # 2005-05-04 mods by Hobbs to work in any terminal size, clean up code # and add more key functionality, tab handling # 2006-03-17 bugfix for cursor-left & for terms that report 0 cols & rows # place cursor at home after file is loaded # bugfix: allow inserting text to blank lines set filename [lindex $argv 0] proc edittext {fid} { global BUFFER IDX set viewRow 1 ; # row idx into view area, 1-based set viewCol 1 ; # col idx into view area, 1-based set bufRow 0 ; # row idx into full buffer, 0-based set bufCol 0 ; # col idx into full buffer, 0-based set IDX(ROWLAST) -1 ; # last row most recently displayed in view set IDX(COLLAST) -1 ; # last col most recently displayed in view set char "" ; # last char received set line [lindex $BUFFER $bufRow] ; # line data of current line display $bufRow $bufCol home; flush stdout while {$char != "\u0011"} { set char [read $fid 1] if {[eof $fid]} {return done} # Control chars start at a == \u0001 and count up. switch -exact -- $char { \u0011 { # ^q - quit return done } \u0001 { # ^a - beginning of line set bufCol 0 } \u0004 { # ^d - delete if {$bufCol > [string length $line]} { set bufCol [string length $line] } set line [string replace $line $bufCol $bufCol] set BUFFER [lreplace $BUFFER $bufRow $bufRow $line] set IDX(COLLAST) -1 ; # force redraw } \u0005 { # ^e - end of line set bufCol [string length $line] } \u000a { # ^j - insert last yank set currline [string range $line 0 [expr {$bufCol - 1}]] set BUFFER [lreplace $BUFFER $bufRow $bufRow $currline] incr bufRow incr viewRow set BUFFER [linsert $BUFFER $bufRow \ [string range $line $bufCol end]] set IDX(COLLAST) -1 ; # force redraw set line [lindex $BUFFER $bufRow] set bufCol 0 } \u0019 { # ^y - yank line if {$bufRow < [llength $BUFFER]} { set BUFFER [lreplace $BUFFER $bufRow $bufRow] set IDX(COLLAST) -1 ; # force redraw } } \u0008 - \u007f { # ^h && backspace ? if {$bufCol != 0} { if {$bufCol > [string length $line]} { set bufCol [string length $line] } incr bufCol -1 set line [string replace $line $bufCol $bufCol] set BUFFER [lreplace $BUFFER $bufRow $bufRow $line] set IDX(COLLAST) -1 ; # force redraw } } \u001b { # ESC - handle escape sequences set next [read $fid 1] if {$next == "\["} { ; # \[ set next [read $fid 1] switch -exact -- $next { A { # Cursor Up (cuu1,up) if {$bufRow > 0} { incr bufRow -1 incr viewRow -1 } } B { # Cursor Down if {$bufRow < [expr {[llength $BUFFER] - 1}]} { incr bufRow 1 incr viewRow 1 } } C { # Cursor Right (cuf1,nd) if {$bufCol < [string length $line]} { incr bufCol 1 } } D { # Cursor Left if {$bufCol > [string length $line]} { set bufCol [string length $line] } if {$bufCol > 0} { incr bufCol -1 } } H { # Cursor Home set bufCol 0 set bufRow 0 set viewRow 1 } 3 { # delete set next [read $fid 1] if {$bufCol > [string length $line]} { set bufCol [string length $line] } set line [string replace $line $bufCol $bufCol] set BUFFER [lreplace $BUFFER $bufRow $bufRow $line] set IDX(COLLAST) -1 ; # force redraw } 5 { # 5 Prev screen if {[read $fid 1] == "~"} { set size [expr {$IDX(ROWMAX) - 1}] if {$bufRow < $size} { set bufRow 0 set viewRow 1 } else { incr bufRow -$size incr viewRow -$size } } } 6 { # 6 Next screen if {[read $fid 1] == "~"} { set size [expr {$IDX(ROWMAX) - 1}] incr bufRow $size incr viewRow $size if {$bufRow >= [llength $BUFFER]} { set viewRow [llength $BUFFER] set bufRow [expr {$viewRow - 1}] } } } } # most of the above cause a BUFFER row change set line [lindex $BUFFER $bufRow] } } default { set line "[string range $line 0 [expr $bufCol - 1]]${char}[string range $line $bufCol end]" set BUFFER [lreplace $BUFFER $bufRow $bufRow $line] incr bufCol [string length $char] if {$bufCol > [string length $line]} { set bufCol [string length $line] } set IDX(COLLAST) -1 ; # force redraw } } # Constrain current view idx if {$viewRow <= 1} {set viewRow 1} if {$viewRow >= ($IDX(ROWMAX) - 1)} { set viewRow [expr {$IDX(ROWMAX) - 1}] } set viewCol [expr {$bufCol + 1}] if {$viewCol >= $IDX(COLMAX)} {set viewCol $IDX(COLMAX)} # start and end view area to display set startRow [expr {$bufRow + 1 - $viewRow}] set startCol [expr {$bufCol + 1 - $viewCol}] display $startRow $startCol # translate viewCol to proper index (account for tabs) if {[string match "*\t*" $line]} { # let's just brute force over the line set i 0 foreach c [split [string range $line \ $startCol [expr {$bufCol - 1}]] ""] { if {[string equal "\t" $c]} { set i [expr {$i + (8 - $i%8)}] ; # align to 8c boundary } else { incr i } } set viewCol [expr {$startCol + 1 + $i}] } idx [expr {$bufRow + 1}] $viewCol puts -nonewline "\u001b\[${viewRow};${viewCol}H" ; # place cursor cursor on flush stdout } } proc linerange {line start end} { # Get # *visual* chars - account for tabs (== 8c) in line range set line [string range $line $start $end] if {[string match "*\t*" $line]} { # let's just brute force over the line set i 0 set end [expr {$end-$start}] set res {} foreach c [split $line ""] { if {[string equal "\t" $c]} { set i [expr {$i + (8 - $i%8)}] ; # align to 8c boundary } else { incr i } append res $c if {$i > $end} { break } } return $res } return $line } proc display {startRow startCol} { global IDX BUFFER cursor off ; home if {($IDX(ROWLAST) != $startRow) || ($IDX(COLLAST) != $startCol)} { # Add display size to get end points set endRow [expr {$startRow + $IDX(ROWMAX) - 1}] set endCol [expr {$startCol + $IDX(COLMAX) - 1}] for {set i $startRow} {$i < $endRow} {incr i} { puts -nonewline "\u001b\[K" ; # erase current line puts [linerange [lindex $BUFFER $i] $startCol $endCol] } set IDX(ROWLAST) $startRow set IDX(COLLAST) $startCol } } proc status {msg} { global IDX set len [expr {$IDX(ROWCOL) - 1}] set str [format "%-${len}.${len}s" $msg] puts -nonewline "\u001b\[$IDX(ROWMAX);00H$str" } proc idx {row col} { global IDX set str [format " L:%-4d C:%-4d" $row $col] # the string must not exceed $IDX(ROWCOLLEN) length puts -nonewline "\u001b\[$IDX(ROWMAX);$IDX(ROWCOL)H[string range \ $str 0 $IDX(ROWCOLLEN)]" } proc home {} { puts -nonewline "\u001b\[1;1H" } proc clear {} { puts -nonewline "\u001b\[2J" } proc cursor {bool} { puts -nonewline "\u001b\[?[expr \ {$::IDX(ROWMAX)+1}][expr {$bool ? "h" : "j"}]" } #start of console editor program #puts -nonewline $CLEAR proc console_edit {fileName} { global BUFFER IDX #Script-Edit by Steve Redler IV steve@sr-tech.com 5-30-2001 set IDX(ROWMAX) 24 set IDX(COLMAX) 80 if {![catch {exec stty -a} err] && [regexp {rows (\d+); columns (\d+)} $err -> rows cols]} { if {$rows != "0" && $cols != 0} { set IDX(ROWMAX) $rows set IDX(COLMAX) $cols } } set IDX(ROWCOLLEN) 15 set IDX(ROWCOL) [expr {$IDX(COLMAX) - $IDX(ROWCOLLEN)}] set infile [open $fileName RDWR] set BUFFER [split [read $infile] "\n"] close $infile clear ; home status "\u0007$fileName loaded" idx [llength $BUFFER] 1 fconfigure stdin -buffering none -blocking 1 -encoding iso8859-1 fconfigure stdout -translation crlf -encoding iso8859-1 flush stdout exec stty raw -echo edittext stdin status "Save '$fileName'? Y/n" flush stdout #fconfigure stdin -buffering full -blocking 1 set line [read stdin 1] exec stty -raw echo if {$line != "n"} { set outfile [open $fileName w ] puts "len of buffer [llength $BUFFER]" for {set i 0} {$i<[llength $BUFFER]} {incr i} { puts $outfile [lindex $BUFFER $i] } close $outfile status " Saved" } else { status " Aborted" } after 100 clear ; home exit 0 } if {$filename == ""} { puts "\nPlease specify a filename" gets stdin filename if {$filename == ""} {exit} } console_edit $filename ---- [RLH] Is this limited to Linux only? Can another *nix use it as well? [SRIV] It should work on any *nix, but I've only tested it on Linux. Test reports welcome. ''[escargo] 29 Mar 2006'' - I might give it a try under [Cygwin] and [Cygwin]/X [http://x.cygwin.com/]. It seems like the key to its function is that the terminal needs to interpret ANSI escape sequences correctly. It might be possible to check the value of env(TERM) to see if it's an acceptable value. ''[escargo] 30 Mar 2006'' - The [Microsoft Windows] with [Cygwin] console window is of terminal type "cygwin". When I start [Cygwin]/X, and use the xterm there (of terminal type "xterm"), I get an error message about the "exec stty raw -echo". It's funny, because the commands for stty work in the xterm window. [RLH] I might try it on an HP/UX server. I wonder if we could shoot for a PICO/NANO clone? [DKF]: With experience, "stty" on some platforms needs to have ''stdin'' as a terminal. That's probably the cause of that error message. ---- [Category Application]