1#!/bin/sh 2# -*- tcl -*- 3# The next line is executed by /bin/sh, but not tcl \ 4exec tclsh "$0" ${1+"$@"} 5 6package require Expect 7package require Tk 8 9# Name: tkterm - terminal emulator using Expect and Tk text widget, v3.0 10# Author: Don Libes, July '94 11# Last updated: Mar '04 12 13# This is primarily for regression testing character-graphic applications. 14# You can certainly use it as a terminal emulator - however many features 15# in a real terminal emulator are not supported (although I'll probably 16# add some of them later). 17 18# A paper on the implementation: Libes, D., Automation and Testing of 19# Interactive Character Graphic Programs", Software - Practice & 20# Experience, John Wiley & Sons, West Sussex, England, Vol. 27(2), 21# p. 123-137, February 1997. 22 23############################### 24# Quick overview of this emulator 25############################### 26# Very good attributes: 27# Understands both termcap and terminfo 28# Understands meta-key (zsh, emacs, etc work) 29# Is fast 30# Understands X selections 31# Looks best with fixed-width font but doesn't require it 32# Supports scrollbars 33# Good-enough-for-starters attributes: 34# Understands one kind of standout mode (reverse video) 35# Should-be-fixed-soon attributes: 36# Does not support resize 37# Probably-wont-be-fixed-soon attributes: 38# Assumes only one terminal exists 39 40############################################### 41# To try out this package, just run it. Using it in 42# your scripts is simple. Here are directions: 43############################################### 44# 0) make sure Expect is linked into your Tk-based program (or vice versa) 45# 1) modify the variables/procedures below these comments appropriately 46# 2) source this file 47# 3) pack the text widget ($term) if you have so configured it (see 48# "term_alone" below). As distributed, it packs into . automatically. 49 50############################################# 51# Variables that must be initialized before using this: 52############################################# 53set rows 24 ;# number of rows in term 54set rowsDumb $rows ;# number of rows in term when in dumb mode - this can 55 ;# increase during runtime 56set cols 80 ;# number of columns in term 57set term .t ;# name of text widget used by term 58set sb .sb ;# name of scrollbar used by term in dumb mode 59set term_alone 1 ;# if 1, directly pack term into . 60 ;# else you must pack 61set termcap 1 ;# if your applications use termcap 62set terminfo 1 ;# if your applications use terminfo 63 ;# (you can use both, but note that 64 ;# starting terminfo is slow) 65set term_shell $env(SHELL) ;# program to run in term 66 67############################################# 68# Readable variables of interest 69############################################# 70# cur_row ;# current row where insert marker is 71# cur_col ;# current col where insert marker is 72# term_spawn_id ;# spawn id of term 73 74############################################# 75# Procs you may want to initialize before using this: 76############################################# 77 78# term_exit is called if the spawned process exits 79proc term_exit {} { 80 exit 81} 82 83# term_chars_changed is called after every change to the displayed chars 84# You can use if you want matches to occur in the background (a la bind) 85# If you want to test synchronously, then just do so - you don't need to 86# redefine this procedure. 87proc term_chars_changed {} { 88} 89 90# term_cursor_changed is called after the cursor is moved 91proc term_cursor_changed {} { 92} 93 94# Example tests you can make 95# 96# Test if cursor is at some specific location 97# if {$cur_row == 1 && $cur_col == 0} ... 98# 99# Test if "foo" exists anywhere in line 4 100# if {[string match *foo* [$term get 4.0 4.end]]} 101# 102# Test if "foo" exists at line 4 col 7 103# if {[string match foo* [$term get 4.7 4.end]]} 104# 105# Test if a specific character at row 4 col 5 is in standout 106# if {-1 != [lsearch [$term tag names 4.5] standout]} ... 107# 108# Return contents of screen 109# $term get 1.0 end 110# 111# Return indices of first string on lines 4 to 6 that is in standout mode 112# $term tag nextrange standout 4.0 6.end 113# 114# Replace all occurrences of "foo" with "bar" on screen 115# for {set i 1} {$i<=$rows} {incr i} { 116# regsub -all "foo" [$term get $i.0 $i.end] "bar" x 117# $term delete $i.0 $i.end 118# $term insert $i.0 $x 119# } 120 121############################################# 122# End of things of interest 123############################################# 124 125# Terminal definitions are provided in both termcap and terminfo 126# styles because we cannot be sure which a system might have. The 127# definitions generally follow that of xterm which in turn follows 128# that of vt100. This is useful for the most common archaic software 129# which has vt100 definitions hardcoded. 130 131unset env(DISPLAY) 132set env(LINES) $rows 133set env(COLUMNS) $cols 134 135if {$termcap} { 136 set env(TERM) "tt" 137 set env(TERMCAP) {tt: 138 :ks=\E[?1h\E: 139 :ke=\E[?1l\E>: 140 :cm=\E[%d;%dH: 141 :up=\E[A: 142 :nd=\E[C: 143 :cl=\E[H\E[J: 144 :ce=\E[K: 145 :do=^J: 146 :so=\E[7m: 147 :se=\E[m: 148 :k1=\EOP: 149 :k2=\EOQ: 150 :k3=\EOR: 151 :k4=\EOS: 152 :k5=\EOT: 153 :k6=\EOU: 154 :k7=\EOV: 155 :k8=\EOW: 156 :k9=\EOX: 157 } 158} 159 160if {$terminfo} { 161 # ncurses ignores 2-char term names so use a longer name here 162 set env(TERM) "tkterm" 163 set env(TERMINFO) /tmp 164 set ttsrc "/tmp/tt.src" 165 set file [open $ttsrc w] 166 167 puts $file {tkterm|Don Libes' tk text widget terminal emulator, 168 smkx=\E[?1h\E, 169 rmkx=\E[?1l\E>, 170 cup=\E[%p1%d;%p2%dH, 171 cuu1=\E[A, 172 cuf1=\E[C, 173 clear=\E[H\E[J, 174 el=\E[K, 175 ind=\n, 176 cr=\r, 177 smso=\E[7m, 178 rmso=\E[m, 179 kf1=\EOP, 180 kf2=\EOQ, 181 kf3=\EOR, 182 kf4=\EOS, 183 kf5=\EOT, 184 kf6=\EOU, 185 kf7=\EOV, 186 kf8=\EOW, 187 kf9=\EOX, 188 } 189 close $file 190 191 set oldpath $env(PATH) 192 set env(PATH) "$env(PATH):/usr/5bin:/usr/lib/terminfo" 193 if {1==[catch {exec tic $ttsrc} msg]} { 194 puts "WARNING: tic failed - if you don't have terminfo support on" 195 puts "your system, change \"set terminfo 1\" to \"set terminfo 0\"." 196 puts "Here is the original error from running tic:" 197 puts $msg 198 } 199 set env(PATH) $oldpath 200 201 exec rm $ttsrc 202} 203 204set term_standout 0 ;# if in standout mode or not 205 206log_user 0 207 208# start a shell and text widget for its output 209set stty_init "-tabs" 210eval spawn $term_shell 211stty rows $rows columns $cols < $spawn_out(slave,name) 212set term_spawn_id $spawn_id 213 214# this shouldn't be needed if Ousterhout fixes text bug 215text $term \ 216 -yscroll "$sb set" \ 217 -relief sunken -bd 1 -width $cols -height $rows -wrap none -setgrid 1 218 219# define scrollbars 220scrollbar .sb -command "$term yview" 221 222proc graphicsGet {} {return $::graphics(mode)} 223proc graphicsSet {mode} { 224 set ::graphics(mode) $mode 225 226 if {$mode} { 227 # in graphics mode, no scroll bars 228 grid forget $::sb 229 } else { 230 grid $::sb -column 0 -row 0 -sticky ns 231 } 232} 233 234if {$term_alone} { 235 grid $term -column 1 -row 0 -sticky nsew 236 # let text box only expand 237 grid rowconfigure . 0 -weight 1 238 grid columnconfigure . 1 -weight 1 239} 240 241$term tag configure standout -background black -foreground white 242 243proc term_clear {} { 244 global term 245 246 $term delete 1.0 end 247 term_init 248} 249 250# pine is the only program I know that requires clear_to_eol, sigh 251proc term_clear_to_eol {} { 252 global cols cur_col cur_row 253 254 # save current col/row 255 set col $cur_col 256 set row $cur_row 257 258 set space_rem_on_line [expr $cols - $cur_col] 259 term_insert [format %[set space_rem_on_line]s ""] 260 261 # restore current col/row 262 set cur_col $col 263 set cur_row $row 264} 265 266proc term_init {} { 267 global rows cols cur_row cur_col term 268 269 # initialize it with blanks to make insertions later more easily 270 set blankline [format %*s $cols ""]\n 271 for {set i 1} {$i <= $rows} {incr i} { 272 $term insert $i.0 $blankline 273 } 274 275 set cur_row 1 276 set cur_col 0 277 278 $term mark set insert $cur_row.$cur_col 279 280 set ::rowsDumb $rows 281} 282 283# NOT YET COMPLETE! 284proc term_resize {rowsNew colsNew} { 285 global rows cols term 286 287 foreach {set r 1} {$r < $rows} {incr r} { 288 if {$colsNew > $cols} { 289 # add columns 290 $term insert $i.$column $blanks 291 } elseif {$colsNew < $cols} { 292 # remove columns 293 # ? 294 } 295 } 296 297 if {$rowsNew > $rows} { 298 # add rows 299 } elseis {$rowsNew < $rows} { 300 # remove rows 301 } 302} 303 304proc term_down {} { 305 global cur_row rows cols term 306 307 if {$cur_row < $rows} { 308 incr cur_row 309 } else { 310 if {[graphicsGet]} { 311 # in graphics mode 312 313 # already at last line of term, so scroll screen up 314 $term delete 1.0 "1.end + 1 chars" 315 316 # recreate line at end 317 $term insert end [format %*s $cols ""]\n 318 } else { 319 # in dumb mode 320 incr cur_row 321 322 if {$cur_row > $::rowsDumb} { 323 set ::rowsDumb $cur_row 324 } 325 326 $term insert $cur_row.0 [format %*s $cols ""]\n 327 $term see $cur_row.0 328 } 329 } 330} 331 332proc term_up {} { 333 global cur_row rows cols term 334 335 set cur_rowOld $cur_row 336 incr cur_row -1 337 338 if {($cur_rowOld > $rows) && ($cur_rowOld == $::rowsDumb)} { 339 if {[regexp "^ *$" [$::term get $cur_rowOld.0 $cur_rowOld.end]]} { 340 # delete line 341 $::term delete $cur_rowOld.0 end 342 } 343 incr ::rowsDumb -1 344 } 345} 346 347proc term_insert {s} { 348 global cols cur_col cur_row 349 global term term_standout 350 351 set chars_rem_to_write [string length $s] 352 set space_rem_on_line [expr $cols - $cur_col] 353 354 if {$term_standout} { 355 set tag_action "add" 356 } else { 357 set tag_action "remove" 358 } 359 360 ################## 361 # write first line 362 ################## 363 364 if {$chars_rem_to_write > $space_rem_on_line} { 365 set chars_to_write $space_rem_on_line 366 set newline 1 367 } else { 368 set chars_to_write $chars_rem_to_write 369 set newline 0 370 } 371 372 $term delete $cur_row.$cur_col $cur_row.[expr $cur_col + $chars_to_write] 373 $term insert $cur_row.$cur_col [ 374 string range $s 0 [expr $space_rem_on_line-1] 375 ] 376 377 $term tag $tag_action standout $cur_row.$cur_col $cur_row.[expr $cur_col + $chars_to_write] 378 379 # discard first line already written 380 incr chars_rem_to_write -$chars_to_write 381 set s [string range $s $chars_to_write end] 382 383 # update cur_col 384 incr cur_col $chars_to_write 385 # update cur_row 386 if {$newline} { 387 term_down 388 } 389 390 ################## 391 # write full lines 392 ################## 393 while {$chars_rem_to_write >= $cols} { 394 $term delete $cur_row.0 $cur_row.end 395 $term insert $cur_row.0 [string range $s 0 [expr $cols-1]] 396 $term tag $tag_action standout $cur_row.0 $cur_row.end 397 398 # discard line from buffer 399 set s [string range $s $cols end] 400 incr chars_rem_to_write -$cols 401 402 set cur_col 0 403 term_down 404 } 405 406 ################# 407 # write last line 408 ################# 409 410 if {$chars_rem_to_write} { 411 $term delete $cur_row.0 $cur_row.$chars_rem_to_write 412 $term insert $cur_row.0 $s 413 $term tag $tag_action standout $cur_row.0 $cur_row.$chars_rem_to_write 414 set cur_col $chars_rem_to_write 415 } 416 417 term_chars_changed 418} 419 420proc term_update_cursor {} { 421 global cur_row cur_col term 422 423 $term mark set insert $cur_row.$cur_col 424 425 term_cursor_changed 426} 427 428term_init 429graphicsSet 0 430 431set flush 0 432proc screen_flush {} { 433 global flush 434 incr flush 435 if {$flush == 24} { 436 update idletasks 437 set flush 0 438 } 439} 440 441expect_background { 442 -i $term_spawn_id 443 -re "^\[^\x01-\x1f]+" { 444 # Text 445 term_insert $expect_out(0,string) 446 term_update_cursor 447 } "^\r" { 448 # (cr,) Go to beginning of line 449 screen_flush 450 set cur_col 0 451 term_update_cursor 452 } "^\n" { 453 # (ind,do) Move cursor down one line 454 term_down 455 term_update_cursor 456 } "^\b" { 457 # Backspace nondestructively 458 incr cur_col -1 459 term_update_cursor 460 } "^\a" { 461 bell 462 } "^\t" { 463 # Tab, shouldn't happen 464 send_error "got a tab!?" 465 } eof { 466 term_exit 467 } "^\x1b\\\[A" { 468 # (cuu1,up) Move cursor up one line 469 term_up 470 term_update_cursor 471 } "^\x1b\\\[C" { 472 # (cuf1,nd) Non-destructive space 473 incr cur_col 474 term_update_cursor 475 } -re "^\x1b\\\[(\[0-9]*);(\[0-9]*)H" { 476 # (cup,cm) Move to row y col x 477 set cur_row [expr $expect_out(1,string)+1] 478 set cur_col $expect_out(2,string) 479 term_update_cursor 480 } "^\x1b\\\[H\x1b\\\[J" { 481 # (clear,cl) Clear screen 482 term_clear 483 term_update_cursor 484 } "^\x1b\\\[K" { 485 # (el,ce) Clear to end of line 486 term_clear_to_eol 487 term_update_cursor 488 } "^\x1b\\\[7m" { 489 # (smso,so) Begin standout mode 490 set term_standout 1 491 } "^\x1b\\\[m" { 492 # (rmso,se) End standout mode 493 set term_standout 0 494 } "^\x1b\\\[?1h\x1b" { 495 # (smkx,ks) start keyboard-transmit mode 496 # terminfo invokes these when going in/out of graphics mode 497 graphicsSet 1 498 } "^\x1b\\\[?1l\x1b>" { 499 # (rmkx,ke) end keyboard-transmit mode 500 graphicsSet 0 501 } 502} 503 504# New and incomplete! 505bind $term <Configure> { 506 scan [wm geometry .] "%dx%dx" rows cols 507 stty rows $rows columns $cols < $spawn_out(slave,name) 508 509 # when this is working, uncomment ... 510 # term_resize $rows $cols 511} 512 513bind $term <Any-Enter> { 514 focus %W 515} 516 517bind $term <Meta-KeyPress> { 518 if {"%A" != ""} { 519 exp_send -i $term_spawn_id "\033%A" 520 } 521} 522 523bind $term <KeyPress> { 524 exp_send -i $term_spawn_id -- %A 525 break 526} 527 528bind $term <Control-space> {exp_send -null} 529bind $term <Control-at> {exp_send -null} 530 531bind $term <F1> {exp_send -i $term_spawn_id "\033OP"} 532bind $term <F2> {exp_send -i $term_spawn_id "\033OQ"} 533bind $term <F3> {exp_send -i $term_spawn_id "\033OR"} 534bind $term <F4> {exp_send -i $term_spawn_id "\033OS"} 535bind $term <F5> {exp_send -i $term_spawn_id "\033OT"} 536bind $term <F6> {exp_send -i $term_spawn_id "\033OU"} 537bind $term <F7> {exp_send -i $term_spawn_id "\033OV"} 538bind $term <F8> {exp_send -i $term_spawn_id "\033OW"} 539bind $term <F9> {exp_send -i $term_spawn_id "\033OX"} 540