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