exec expectk
"$0" ${1+"$@"}
# multixterm - drive multiple xterms separately or together
# multixterm [-xa "xterm args"]
# [-xv] (enable verbose mode)
# [xterm names or user-defined args...]
# Multixterm creates multiple xterms that can be driven together
# In its simplest form, multixterm is run with no arguments and
# commands are interactively entered in the first entry field.
# Press return (or click the "new xterm" button) to create a new
# xterm running that command.
# Keystrokes in the "stdin window" are redirected to all xterms
# started by multixterm. xterms may be driven separately simply
# The stdin window must have the focus for keystrokes to be sent
# to the xterms. When it has the focus, the color changes to
# aquamarine. As characters are entered, the color changes to
# green for a second. This provides feedback since characters
# are not echoed in the stdin window.
# Typing in the stdin window while holding down the alt or meta
# keys sends an escape character before the typed characters.
# This provides support for programs such as emacs.
# The optional -xa argument indicates arguments to pass to
# The optional -xc argument indicates a command to be run in
# each named xterm (see -xn). With no -xc argument, the command
# The optional -xd argument indicates a directory to search for
# files that will appear in the Files menu. By default, the
# directory is: ~/lib/multixterm
# The optional -xf argument indicates a file to be read at
# startup. See FILES below for more info.
# The optional -xn argument indicates a name for each xterm.
# This name will also be substituted for any %n in the command
# The optional -xv flag puts multixterm into a verbose mode
# where it will describe some of the things it is doing
# internally. The verbose output is not intended to be
# understandable to anyone but the author.
# Less common options may be changed by the startup file (see
# All the usual X and wish flags are supported (i.e., -display,
# -name). There are so many of them that to avoid colliding and
# make them easy to remember, all the multixterm flags begin
# If any arguments do not match the flags above, the remainder
# of the command line is made available for user processing. By
# default, the remainder is used as a list of xterm names in the
# style of -xn. The default behavior may be changed using the
# .multixtermrc file (see DOT FILE below).
# EXAMPLE COMMAND LINE ARGUMENTS
# The following command line starts up two xterms using ssh to
# the hosts bud and dexter.
# multixterm -xc "ssh %n" bud dexter
# Command files may be used to drive or initialize multixterm.
# The File menu may be used to invoke other files. If files
# exist in the command file directory (see -xd above), they will
# appear in the File menu. Files may also be loaded by using
# File->Open. Any filename is acceptable but the File->Open
# browser defaults to files with a .mxt suffix.
# Files are written in Tcl and may change any variables or
# invoke any procedures. The primary variables of interest are
# 'xtermCmd' which identifies the command (see -xc) and
# 'xtermNames' which is a list of names (see -xn). The
# procedure xtermStartAll, starts xterms for each name in the
# list. Other variables and procedures may be discovered by
# examining multixterm itself.
# The following file does the same thing as the earlier example
# # start two xterms connected to bud and dexter
# set xtermNames {bud dexter}
# At startup, multixterm reads ~/.multixtermrc if present. This
# is similar to the command files (see FILES above) except that
# .multixtermrc may not call xtermStartAll. Instead it is
# called implicitly, similar to the way that it is implicit in
# the command line use of -xn.
# The following example .multixtermrc file makes every xterm run
# ssh to the hosts named on the command line.
# Then multixterm could be called simply:
# If any command-line argument does not match a multixterm flag,
# the remainder of the command line is made available to
# .multixtermrc in the argv variable. If argv is non-empty when
# .multixtermrc returns, it is assigned to xtermNames unless
# xtermNames is non-empty in which case, the content of argv is
# Commands from .multixtermrc are evaluated early in the
# initialization of multixterm. Anything that must be done late
# in the initialization (such as adding additional bindings to
# the user interface) may be done by putting the commands inside
# a procedure called "initLate".
# Except as otherwise noted, the menus are self-explanatory.
# Some of the menus have dashed lines as the first entry.
# Clicking on the dashed lines will "tear off" the menus.
# USAGE SUGGESTION - ALIASES AND COMMAND FILES
# Aliases may be used to store lengthy command-line invocations.
# Command files can be also be used to store such invocations
# as well as providing a convenient way to share configurations.
# Tcl is a general-purpose language. Thus multixterm command
# files can be extremely flexible, such as loading hostnames
# from other programs or files that may change from day-to-day.
# In addition, command files can be used for other purposes.
# For example, command files may be used to prepared common
# canned interaction sequences. For example, the command to
# send the same string to all xterms is:
# xtermSend "a particularly long string"
# The File menu (torn-off) makes canned sequences particularly
# convenient. Interactions could also be bound to a mouse
# button, keystroke, or added to a menu via the .multixtermrc
# USAGE SUGGESTION - HANDLING MANY XTERMS BY TILING
# The following .multixtermrc causes tiny xterms to tile across
# and down the screen. (You may have to adjust the parameters
# for your screen.) This can be very helpful when dealing with
# large numbers of xterms.
# trace variable xtermArgs r traceArgs
# proc traceArgs {args} {
# set ::xtermArgs "-geometry 80x12+$xPos+$yPos -font 6x10"
# if {$yPos > 800} {set yPos 0}
# The xtermArgs variable in the code above is the variable
# corresponding to the -xa argument.
# xterms can be also be created directly. The following command
# file creates three xterms overlapped horizontally:
# foreach name {bud dexter hotdog} {
# set ::xtermArgs "-geometry 80x12+$xPos+0 -font 6x10"
# USAGE SUGGESTION - SELECTING HOSTS BY NICKNAME
# The following .multixtermrc shows an example of changing the
# default handling of the arguments from hostnames to a filename
# set xtermNames [exec cat $argv]
# The following is a variation, retrieving the host names from
# set xtermNames [exec ypcat $argv]
# The following hardcodes two sets of hosts, so that you can
# call multixterm with either "cluster1" or "cluster2":
# set xtermNames "bud dexter"
# set xtermNames "frank hotdog weiner"
# It is worth comparing multixterm to xkibitz. Multixterm
# connects a separate process to each xterm. xkibitz connects
# the same process to each xterm.
# Multixterm provides no way to remotely control scrollbars,
# resize, and most other window system related functions.
# Multixterm can only control new xterms that multixterm itself
# As a convenience, the File menu shows a limited number of
# files. To show all the files, use File->Open.
# $DOTDIR/.multixtermrc initial command file
# ~/.multixtermrc fallback command file
# ~/lib/multixterm/ default command file directory
# If multixterm is killed using an uncatchable kill, the xterms
# are not killed. This appears to be a bug in xterm itself.
# Send/expect sequences can be done in multixterm command files.
# However, due to the richness of the possibilities, to document
# it properly would take more time than the author has at present.
# Requires Expect 5.36.0 or later.
# Requires Tk 8.3.3 or later.
# The latest version of multixterm is available from
# http://expect.nist.gov/example/multixterm . If your version of Expect
# and Tk are too old (see REQUIREMENTS above), download a new version of
# Expect from http://expect.nist.gov
# Don Libes <don@libes.com>
# Multixterm is in the public domain; however the author would
# appreciate acknowledgement if multixterm or parts of it or ideas from
######################################################################
# user-settable things - override them in the ~/.multixtermrc file
# or via command-line options
######################################################################
set palette
#d8d8ff ;# lavender
set colorFocusIn aquamarine
set cmdDir ~
/lib
/multixterm
set inputLabel
"stdin window"
set fileMenuMax
30 ;# max number of files shown in File menu
set tearoffMenuMin
2 ;# min number of files needed to enable the File
proc initLate
{} {} ;# anything that must be done late in initialization
;# such as adding/modifying bindings, may be done by
######################################################################
# end of user-settable things
######################################################################
######################################################################
######################################################################
set versionDate
"2004/06/29"
catch
{package require Tk
} ;# early versions of Tk had no package
exit1
"requires Tk 8.3.3 or later but you are using Tk $::tk_patchLevel."
} elseif
{$tk_version == 8.3} {
if {[lindex
[split $tk_patchLevel .
] 2] < 3} tkBad
######################################################################
# process args - has to be done first to get things like -xv working ASAP
######################################################################
# set up verbose mechanism early
set proc
[lindex
[info level
-1] 0]
# read a single argument from the command line
proc arg_read1
{var args
} {
if {0 == [llength
$args]} {
if {[llength
$argv] < 2} {
exit1
"$argname requires an argument"
set argv
[lrange
$argv 2 end
]
proc xtermUsage
{{msg
{}}} {
if {![string equal
$msg ""]} {
puts
"multixtermrc: $msg"
puts
{usage
: multixterm
[flags
] ... where flags are
:
[-xv] (enable verbose mode
)
[xterm names or user-defined args...
]}
while {[llength
$argv]} {
set flag
[lindex
$argv 0]
switch
-- $flag -x?
- -xh {
if {![file exists
$cmdFile]} {
exit1
"can't read $cmdFile"
if {![file exists
$cmdDir]} {
exit1
"can't read $cmdDir"
set argv
[lrange
$argv 1 end
]
verbose
"remaining args: $argv"
break ;# let user handle remaining args later
######################################################################
# determine and load rc file - has to be done now so that widgets
######################################################################
# if user has no $DOTDIR, fall back to home directory
if {![info exists env
(DOTDIR
)]} {
# catch bogus DOTDIR, otherwise glob will lose the bogus directory
# and it won't appear in the error msg
if {[catch
{glob
$env(DOTDIR
)} dotdir
]} {
exit1
"$env(DOTDIR)/.multixtermrc can't be found because $env(DOTDIR) doesn't exist or can't be read"
set rcFile
$dotdir/.multixtermrc
{{Multixterm Files
} *.mxt
}
proc openFile
{{fn
{}}} {
if {[string equal
$fn ""]} {
-filetypes $
::fileTypes \
-title "multixterm file"]
if {[string match
$fn ""]} return
uplevel
#0 source [list $fn]
verbose
"xtermNames = \"$::xtermNames\""
verbose
"xtermCmd = $::xtermCmd"
if {[file exists
$rcFile]} {
verbose
"$rcFile: not found"
if {![string equal
"" $argv]} {
if {[string equal
$xtermNames ""]} {
######################################################################
# Describe and initialize some important globals
######################################################################
# ::activeList and ::activeArray both track which xterms to send
# (common) keystrokes to. Each element in activeArray is connected to
# the active menu. The list version is just a convenience making the
# send function easier/faster.
# ::names is an array of xterm names indexed by process spawn ids.
# ::xtermSid is an array of xterm spawn ids indexed by process spawn ids.
# ::xtermPid is an array of xterm pids indexed by process spawn id.
######################################################################
# create an xterm and establish connections
######################################################################
proc xtermStart
{cmd name
} {
verbose
"starting new xterm running $cmd with name $name"
######################################################################
######################################################################
set pid
[spawn
-noecho -pty]
verbose
"spawn -pty: pid = $pid, spawn_id = $spawn_id"
stty raw
-echo < $spawn_out(slave
,name
)
regexp
".*(.)(.)" $spawn_out(slave
,name
) dummy c1 c2
if {[string compare
$c1 "/"] == 0} {
######################################################################
# prepare to start xterm by making sure xterm name is unique
# X doesn't care but active menu won't make sense unless names are unique
######################################################################
foreach oldName
[array names
::names
] {
if {[string match
"$name" $
::names
($oldName)]} {
verbose
"uniqueness of $name: $unique"
# if not unique, look at the numerical suffixes of all matching
# names, find the biggest and increment it
foreach oldName
[array names
::names
] {
verbose
"regexp ^[set safe](\[0-9]+)$ $::names($oldName) X num"
if {[regexp
"^[set safe](\[0-9]+)$" $
::names
($oldName) X num
]} {
verbose
"matched, checking suffix"
verbose
"new suffix: $suffix"
verbose
"new name: $name"
######################################################################
######################################################################
set xtermpid
[eval exec xterm
-name [list
$name] -S$c1$c2$spawn_out(slave
,fd
) $
::xtermArgs
&]
verbose
"xterm: pid = $xtermpid"
# xterm first sends back window id, save in environment so it can be
# passed on to the new process
# note quotes must be used here to avoid diagnostic from expr
set ::env
(WINDOWID
) [expr "0x$expect_out(1,string)"]
######################################################################
######################################################################
set pid
[eval spawn
-noecho $cmd]
verbose
"$cmd: pid = $pid, spawn_id = $spawn_id"
lappend
::activeList
$sidCmd
set ::activeArray
($sidCmd) 1
######################################################################
# link everything back to spawn id of new process
######################################################################
set ::xtermSid
($sidCmd) $sidXterm
set ::names
($sidCmd) $name
set ::xtermPid
($sidCmd) $xtermpid
######################################################################
# connect proc output to xterm output
# connect xterm input to proc input
######################################################################
-re ".+" [list sendTo
$sidXterm]
eof
[list xtermKill
$sidCmd]
-re ".+" [list sendTo
$sidCmd]
eof
[list xtermKill
$sidCmd]
.m.e entryconfig Active
-state normal
.m.e.active add checkbutton
-label $name -variable activeArray
($sidCmd) \
-command [list xtermActiveUpdate
$sidCmd]
set ::activeArray
($sidCmd) 1
proc xtermActiveUpdate
{sid
} {
if {$
::activeArray
($sid)} {
verbose
"activating $sid"
verbose
"deactivating $sid"
proc activeListUpdate
{} {
foreach n
[array names
::activeArray
] {
if {$
::activeArray
($n)} {
# make a string safe to go through regexp
string map
{{[} {\
[} {*} {\
*} {+} {\
+} {^
} {\^
} {$
} {\\$
}} $s
# utility to map xterm name to spawn id
# multixterm doesn't use this but a user might want to
foreach sid
[array names
::names
] {
if {[string equal
$name $
::names
($sid)]} {
error
"no such term with name: $name"
# utility to activate an xterm
# multixterm doesn't use this but a user might want to
proc xtermActivate
{sid
} {
set ::activeArray
($sid) 1
# utility to deactivate an xterm
# multixterm doesn't use this but a user might want to
proc xtermDeactivate
{sid
} {
set ::activeArray
($sid) 0
# utility to do an explicit Expect
# multixterm doesn't use this but a user might want to
proc xtermExpect
{args
} {
# check if explicit spawn_id in args
for {set i
0} {$i < [llength
$args]} {incr i
} {
switch
-- [lindex
$args $i] "-i" {
set sidCmd
[lindex
$args [incr i
]]
if {![info exists sidCmd
]} {
# nothing explicit, so get it from the environment
# mimic expect's normal behavior in obtaining spawn_id
if {[info exists spawn_id
]} {
# turn off bg expect, do fg expect, then re-enable bg expect
expect_background
-i $sidCmd ;# disable bg expect
eval expect
$args ;# fg expect
-re ".+" [list sendTo $
::xtermSid
($sidCmd)]
eof
[list xtermKill
$sidCmd]
######################################################################
# connect main window keystrokes to all xterms
######################################################################
if {[info exists
::afterId
]} {
.input config
-bg $
::colorTyping
set ::afterId
[after
1000 {.input config
-bg $colorCurrent}]
exp_send
-raw -i $
::activeList
-- $A
exp_send
-raw -i $to -- $
::expect_out
(buffer
)
# catch the case where there's no selection
proc xtermPaste
{} {catch
{xtermSend
[selection get
]}}
######################################################################
# clean up an individual process death or xterm death
######################################################################
verbose
"killing xterm $s"
if {![info exists
::xtermPid
($s)]} {
verbose
"too late, already dead"
catch
{exec /bin
/kill -9 $
::xtermPid
($s)}
# remove sid from activeList
verbose
"removing $s from active array"
catch
{unset ::activeArray
($s)}
verbose
"removing from background handler $s"
catch
{expect_background
-i $s}
verbose
"removing from background handler $::xtermSid($s)"
catch
{expect_background
-i $
::xtermSid
($s)}
catch
{close
-i $
::xtermSid
($s)}
verbose
"waiting on proc"
# remove from active menu
verbose
"deleting active menu entry $::names($s)"
# avoid using name as an index since we haven't gone to any pains to
# make it safely interpreted by index-pattern code. instead step
# through, doing the comparison ourselves
set last
[.m.e.active index last
]
for {set i
1} {$i <= $last} {incr i
} {
if {![catch
{.m.e.active entrycget
$i -label} label
]} {
if {[string equal
$label $
::names
($s)]} break
# if none left, disable menu
# this leaves tearoff clone but that seems reasonable
if {0 == [llength
[array names
::xtermSid
]]} {
.m.e entryconfig Active
-state disable
######################################################################
######################################################################
.m add cascade
-menu .m.f
-label "File" -underline 0
.m add cascade
-menu .m.e
-label "Edit" -underline 0
.m add cascade
-menu .m.
help -label "Help" -underline 0
set files
[glob
-nocomplain $cmdDir/*]
set filesLength
[llength
$files]
if {$filesLength >= $tearoffMenuMin} {
menu .m.f
-tearoff $filesTearoff -title "multixterm files"
.m.f add
command -label Open
-command openFile
-underline 0
set files
[lrange
$files 0 $fileMenuMax]
.m.f add
command -label $f -command [list openFile
$f]
.m.f add
command -label "Exit" -command exit -underline 0
.m.e add
command -label "Paste" -command xtermPaste
-underline 0
.m.e add cascade
-label "Active" -menu .m.e.active
-underline 0
.m.
help add
command -label "About" -command about
-underline 0
.m.
help add
command -label "Man Page" -command help -underline 0
menu .m.e.active
-tearoff 1 -title "multixterm active"
.m.e entryconfig Active
-state disabled
# disable the Active menu simply because it looks goofy seeing an empty menu
# for consistency, though, it should be enabled
entry .input
-textvar inputLabel
-justify center
-state disabled
entry .cmd
-textvar xtermCmd
button .
exec -text "new xterm" -command {xtermStart
$xtermCmd $xtermCmd}
grid .
exec -sticky ew
-ipadx 3 -ipady 3
grid columnconfigure .
0 -weight 1
grid rowconfigure .
0 -weight 1 ;# let input window only expand
bind .cmd
<Return
> {xtermStart
$xtermCmd $xtermCmd}
# send all keypresses to xterm
bind .input
<KeyPress
> {xtermSend
%A
; break}
bind .input
<Alt-KeyPress
> {xtermSend
\033%A
; break}
bind .input
<Meta-KeyPress
> {xtermSend
\033%A
; break}
bind .input
<<Paste>> {xtermPaste ; break}
bind .input <<PasteSelection>> {xtermPaste ; break}
# arrow keys - note that if they've been rebound through .Xdefaults
# you'll have to change these definitions.
bind .input <Up> {xtermSend \033OA; break}
bind .input <Down> {xtermSend \033OB; break}
bind .input <Right> {xtermSend \033OC; break}
bind .input <Left> {xtermSend \033OD; break}
# Strange: od -c reports these as \033[A et al but when keypad mode
# is initialized, they send \033OA et al. Presuming most people
# want keypad mode, I'll go with the O versions. Perhaps the other
# version is just a Sun-ism anyway.
set colorCurrent [.input cget -bg]
set colorFocusOut $colorCurrent
# change color to show focus
bind .input <FocusOut> colorFocusOut
bind .input <FocusIn> colorFocusIn
proc colorFocusIn {} {.input config -bg [set ::colorCurrent $::colorFocusIn]}
proc colorFocusOut {} {.input config -bg [set ::colorCurrent $::colorFocusOut]}
# convert normal mouse events to focusIn
bind .input <1> {focus .input; break}
bind .input <Shift-1> {focus .input; break}
# ignore all other mouse events that might make selection visible
bind .input <Double-1> break
bind .input <Triple-1> break
bind .input <B1-Motion> break
bind .input <B2-Motion> break
set scriptName [info script] ;# must get while it's active
wm title $w "about multixterm"
wm iconname $w "about multixterm"
button $w.b -text Dismiss -command [list wm withdraw $w]
label $w.title -text "multixterm" -font "Times 16" -borderwidth 10 -fg red
label $w.version -text "Version $::versionString, Released $::versionDate"
label $w.author -text "Written by Don Libes <don@libes.com>"
label $w.using -text "Using Expect [exp_version],\
if {[winfo exists .help]} {
wm title .help "multixterm help"
wm iconname .help "multixterm help"
scrollbar .help.sb -command {.help.text yview}
text .help.text -width 74 -height 30 -yscroll {.help.sb set} -wrap word
button .help.ok -text Dismiss -command {destroy .help} -relief raised
bind .help <Return> {destroy .help;break}
grid .help.sb -row 0 -column 0 -sticky ns
grid .help.text -row 0 -column 1 -sticky nsew
grid .help.ok -row 1 -columnspan 2 -sticky ew -ipadx 3 -ipady 3
# let text box only expand
grid rowconfigure .help 0 -weight 1
grid columnconfigure .help 1 -weight 1
set script [auto_execok $::scriptName]
if {[llength $script] == 0} {
set script /depot/tcl/bin/multixterm ;# fallback
if {[catch {open $script} fid]} {
.help.text insert end "Could not open help file: $script"
# skip to the beginning of the actual help (starts with "NAME")
while {-1 != [gets $fid buf]} {
if {1 == [regexp "NAME" $buf]} {
.help.text insert end "\n NAME\n"
while {-1 != [gets $fid buf]} {
if {0 == [regexp "^#(.?)(.*)" $buf X key buf]} break
set buf [subst -nocommands $buf]
.help.text insert end $key$buf\n
# support scrolling beyond Tk's built-in Next/Previous
foreach w {"" .sb .text .ok} {
bind $W <space> {scrollPage 1} ;#more
bind $W <Delete> {scrollPage -1} ;#more
bind $W <BackSpace> {scrollPage -1} ;#more
bind $W <Control-v> {scrollPage 1} ;#emacs
bind $W <Meta-v> {scrollPage -1} ;#emacs
bind $W <Control-f> {scrollPage 1} ;#vi
bind $W <Control-b> {scrollPage -1} ;#vi
bind $W <F35> {scrollPage 1} ;#sun
bind $W <F29> {scrollPage -1} ;#sun
bind $W <Down> {scrollLine 1}
bind $W <Up> {scrollLine -1}
tkScrollByPages .help.sb v $dir
tkScrollByUnits .help.sb v $dir
######################################################################
######################################################################
# xtermKillAll is not intended to be user-callable. It just kills
# the processes and that's it. A user-callable version would update
# the data structures, close the channels, etc.
foreach sid [array names ::xtermPid] {
exec /bin/kill -9 $::xtermPid($sid)
proc exit {{x 0}} {xtermKillAll;_exit $x}
wm protocol . WM_DELETE_WINDOW exit
######################################################################
# start any xterms requested
######################################################################
verbose "xtermNames = \"$::xtermNames\""
foreach n $::xtermNames {
regsub -all "%n" $::xtermCmd $n cmdOut
# now that xtermStartAll and its accompanying support has been set up
# run it to start anything defined by rc file or command-line args.
xtermStartAll ;# If nothing has been requested, this is a no-op.
# finally do any explicit command file
if {[info exists cmdFile]} {