Thursday, April 10, 2008

Tcl Automates Interactive Installation

I am currently working overseas for a 48-node grid cluster installation and one of the applications installation requires to be done interactively. It would be a pain in the ass to do that 48 times, right?

In the Expect (a Tcl tool/extension for automating interactive applications) source tarball, there is an example called "multixterm" which allow the user to send a single input to multiple terminals. Since all the 48 nodes are able to be ssh in from the grid master without password, I was able to launch multixterm to do my installation interactively.

Here is a sample screen dump example that I used to launch 4 xterms in the localhost.

In case you would like to see who the multixterm works, I am copying the original expect source here. Here is a good example showing you how to install and launch Expect in Debian Linux.

#!/depot/path/expectk
#
# NAME
# multixterm - drive multiple xterms separately or together
#
# SYNOPSIS
# multixterm [-xa "xterm args"]
#     [-xc "command"]
#     [-xd "directory"]
#     [-xf "file"]
#     [-xn "xterm names"]
#     [-xv] (enable verbose mode)
#     [-xh] or [-x?] (help)
#     [xterm names or user-defined args...]
#
# DESCRIPTION
# Multixterm creates multiple xterms that can be driven together
# or separately.
#
# 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
# by focusing on them.
#
# 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.
#
# ARGUMENTS
# The optional -xa argument indicates arguments to pass to
# xterm.
#
# The optional -xc argument indicates a command to be run in
# each named xterm (see -xn).  With no -xc argument, the command
# is the current shell.
#
# 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
# argument (see -xc).
#
# 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
# FILES below).
#
# 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
# with -x.
#
# 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
#
# FILES
# 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.
#
# EXAMPLE FILE
# The following file does the same thing as the earlier example
# command line:
#
#  # start two xterms connected to bud and dexter
#  set xtermCmd "ssh %n"
#  set xtermNames {bud dexter}
#  xtermStartAll
#
# DOT FILE
# 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.
#
#  set xtermCmd "ssh %n"
#
# Then multixterm could be called simply:
#
#  multixterm bud dexter
#
# 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
# ignored.
#
# 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".
#
# MENUS
# 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
# file.
#
# 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.
#
#     set yPos 0
#     set xPos 0
#
#     trace variable xtermArgs r traceArgs
#
#     proc traceArgs {args} {
#         global xPos yPos
#         set ::xtermArgs "-geometry 80x12+$xPos+$yPos -font 6x10"
#         if {$xPos} {
#      set xPos 0
#      incr yPos 145
#      if {$yPos > 800} {set yPos 0}
#         } else {
#      set xPos 500
#         }
#     }
#
# 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:
#
#     set xPos 0
#
#     foreach name {bud dexter hotdog} {
#         set ::xtermArgs "-geometry 80x12+$xPos+0 -font 6x10"
#         set ::xtermNames $name
#         xtermStartAll
#         incr xPos 300
#     }
#
# 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
# containing hostnames:
#
#  set xtermNames [exec cat $argv]
#
# The following is a variation, retrieving the host names from
# the yp database:
#
#  set xtermNames [exec ypcat $argv]
#
# The following hardcodes two sets of hosts, so that you can
# call multixterm with either "cluster1" or "cluster2":
#
#  switch $argv {
#      cluster1 {
#   set xtermNames "bud dexter"
#      }
#      cluster2 {
#   set xtermNames "frank hotdog weiner"
#      }
#  }
#
# COMPARE/CONTRAST
# It is worth comparing multixterm to xkibitz.  Multixterm
# connects a separate process to each xterm.  xkibitz connects
# the same process to each xterm.
#
# LIMITATIONS
# 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
# has started.
#
# As a convenience, the File menu shows a limited number of
# files.  To show all the files, use File->Open.
#
# FILES
# $DOTDIR/.multixtermrc   initial command file
# ~/.multixtermrc         fallback command file
# ~/lib/multixterm/       default command file directory
#
# BUGS
# 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.
#
# REQUIREMENTS
# Requires Expect 5.36.0 or later.
# Requires Tk 8.3.3 or later.
#
# VERSION
#! $::versionString
# 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
#
# DATE
#! $::versionDate
#
# AUTHOR
# Don Libes <don@libes.com>
#
# LICENSE
# Multixterm is in the public domain; however the author would
# appreciate acknowledgement if multixterm or parts of it or ideas from
# it are used.

######################################################################
# user-settable things - override them in the ~/.multixtermrc file
#    or via command-line options
######################################################################

set palette       #d8d8ff   ;# lavender
set colorTyping   green
set colorFocusIn  aquamarine

set xtermNames    {}
set xtermCmd      $env(SHELL)
set xtermArgs     ""
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
    ;# menu to be torn off

proc initLate {} {}      ;# anything that must be done late in initialization
    ;# such as adding/modifying bindings, may be done by
    ;# redefining this

######################################################################
# end of user-settable things
######################################################################

######################################################################
# sanity checking
######################################################################

set versionString 1.8
set versionDate "2004/06/29"

package require Tcl
catch {package require Tk} ;# early versions of Tk had no package
package require Expect

proc exit1 {msg} {
    puts "multixterm: $msg"
    exit 1
}

exp_version -exit 5.36

proc tkBad {} {
    exit1 "requires Tk 8.3.3 or later but you are using Tk $::tk_patchLevel."
}

if {$tk_version < 8.3} {
    tkBad
} 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 verbose 0
proc verbose {msg} {
    if {$::verbose} {
 if {[info level] > 1} {
     set proc [lindex [info level -1] 0]
 } else {
     set proc main
 }
 puts "$proc: $msg"
    }
}

# read a single argument from the command line
proc arg_read1 {var args} {
    if {0 == [llength $args]} {
 set argname -$var
    } else {
 set argname $args
    }

    upvar argv argv
    upvar $var v

    verbose "$argname"
    if {[llength $argv] < 2} {
 exit1 "$argname requires an argument"
    }

    set v [lindex $argv 1]
    verbose "set $var $v"
    set argv [lrange $argv 2 end]
}

proc xtermUsage {{msg {}}} {
    if {![string equal $msg ""]} {
 puts "multixtermrc: $msg"
    }
    puts {usage: multixterm [flags] ... where flags are:
 [-xa "xterm args"]
 [-xc "command"]
 [-xd "directory"]
 [-xf "file"]
 [-xn "xterm names"]
 [-xv] (enable verbose mode)
 [-xh] or [-x?] (help)
 [xterm names or user-defined args...]}
    exit
}

while {[llength $argv]} {
    set flag [lindex $argv 0]
    switch -- $flag -x? - -xh {
 xtermUsage
    } -xc {
 arg_read1 xtermCmd -xc
    } -xn {
 arg_read1 xtermNames -xn
    } -xa {
 arg_read1 xtermArgs -xa
    } -xf {
 arg_read1 cmdFile -xf
 if {![file exists $cmdFile]} {
     exit1 "can't read $cmdFile"
 }
    } -xd {
 arg_read1 cmdDir -xd
 if {![file exists $cmdDir]} {
     exit1 "can't read $cmdDir"
 }
    } -xv {
 set argv [lrange $argv 1 end]
 set verbose 1
 puts "main: verbose on"
    } default {
 verbose "remaining args: $argv"
 break ;# let user handle remaining args later
    }
}

######################################################################
# determine and load rc file -  has to be done now so that widgets
#  can be affected
######################################################################

# if user has no $DOTDIR, fall back to home directory
if {![info exists env(DOTDIR)]} {
    set 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

set fileTypes {
    {{Multixterm Files} *.mxt}
    {{All Files} *}
}

proc openFile {{fn {}}} {
    verbose "opening $fn"
    if {[string equal $fn ""]} {
 set fn [tk_getOpenFile \
      -initialdir $::cmdDir \
      -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]} {
    openFile $rcFile
} else {
    verbose "$rcFile: not found"
}

if {![string equal "" $argv]} {
    if {[string equal $xtermNames ""]} {
 set xtermNames $argv
    }
}

######################################################################
# 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.

set activeList {}

# ::names is an array of xterm names indexed by process spawn ids.

set names(x) ""
unset names(x)

# ::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"

    ######################################################################
    # create pty for xterm
    ######################################################################
    set pid [spawn -noecho -pty]
    verbose "spawn -pty: pid = $pid, spawn_id = $spawn_id"
    set sidXterm $spawn_id
    stty raw -echo < $spawn_out(slave,name)

    regexp ".*(.)(.)" $spawn_out(slave,name) dummy c1 c2
    if {[string compare $c1 "/"] == 0} {
 set 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
    ######################################################################
    set unique 1
    foreach oldName [array names ::names] {
 if {[string match "$name" $::names($oldName)]} {
     set unique 0
 }
    }
    verbose "uniqueness of $name: $unique"

    set safe [safe $name]

    # if not unique, look at the numerical suffixes of all matching
    # names, find the biggest and increment it
    if {!$unique} {
 set suffix 2
 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"
  if {$num >= $suffix} {
      set suffix [expr $num+1]
      verbose "new suffix: $suffix"
  }
     }
 }
 append name $suffix
 verbose "new name: $name"
    }

    ######################################################################
    # start new xterm
    ######################################################################
    set xtermpid [eval exec xterm -name [list $name] -S$c1$c2$spawn_out(slave,fd) $::xtermArgs &]
    verbose "xterm: pid = $xtermpid"
    close -slave

    # xterm first sends back window id, save in environment so it can be
    # passed on to the new process
    log_user 0
    expect {
 eof {wait;return}
 -re (.*)\n {
     # convert hex to decimal
     # note quotes must be used here to avoid diagnostic from expr
     set ::env(WINDOWID) [expr "0x$expect_out(1,string)"]
 }
    }

    ######################################################################
    # start new process
    ######################################################################
    set pid [eval spawn -noecho $cmd]
    verbose "$cmd: pid = $pid, spawn_id = $spawn_id"
    set sidCmd $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
    ######################################################################
    expect_background {
 -i $sidCmd
 -re ".+" [list sendTo $sidXterm]
 eof [list xtermKill $sidCmd]
 -i $sidXterm
 -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"
    } else {
 verbose "deactivating $sid"
    }
    activeListUpdate
}

proc activeListUpdate {} {
    set ::activeList {}
    foreach n [array names ::activeArray] {
 if {$::activeArray($n)} {
     lappend ::activeList $n
 }
    }
}

# make a string safe to go through regexp
proc safe {s} {
    string map {{[} {\[} {*} {\*} {+} {\+} {^} {\^} {$} {\\$}} $s
}

# utility to map xterm name to spawn id
# multixterm doesn't use this but a user might want to
proc xtermGet {name} {
    foreach sid [array names ::names] {
 if {[string equal $name $::names($sid)]} {
     return $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
    xtermActiveUpdate $sid
}

# utility to deactivate an xterm
# multixterm doesn't use this but a user might want to
proc xtermDeactivate {sid} {
    set ::activeArray($sid) 0
    xtermActiveUpdate $sid
}

# 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]]
     break
 }
    }

    if {![info exists sidCmd]} {
 # nothing explicit, so get it from the environment

 upvar spawn_id spawn_id

 # mimic expect's normal behavior in obtaining spawn_id
 if {[info exists spawn_id]} {
     set sidCmd $spawn_id
 } else {
     set sidCmd $::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
     ;# reenable bg expect
    expect_background {
 -i $sidCmd
 -re ".+" [list sendTo $::xtermSid($sidCmd)]
 eof [list xtermKill $sidCmd]
    }
}

######################################################################
# connect main window keystrokes to all xterms
######################################################################
proc xtermSend {A} {
    if {[info exists ::afterId]} {
 after cancel $::afterId
    }
    .input config -bg $::colorTyping
    set ::afterId [after 1000 {.input config -bg $colorCurrent}]

    exp_send -raw -i $::activeList -- $A
}

proc sendTo {to} {
    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
######################################################################
proc xtermKill {s} {
    verbose "killing xterm $s"

    if {![info exists ::xtermPid($s)]} {
 verbose "too late, already dead"
 return
    }

    catch {exec /bin/kill -9 $::xtermPid($s)}
    unset ::xtermPid($s)

    # remove sid from activeList
    verbose "removing $s from active array"
    catch {unset ::activeArray($s)}
    activeListUpdate

    verbose "removing from background handler $s"
    catch {expect_background -i $s}
    verbose "removing from background handler $::xtermSid($s)"
    catch {expect_background -i $::xtermSid($s)}
    verbose "closing proc"
    catch {close -i $s}
    verbose "closing xterm"
    catch {close -i $::xtermSid($s)}
    verbose "waiting on proc"
    wait -i $s
    wait -i $::xtermSid($s)
    verbose "done waiting"
    unset ::xtermSid($s)

    # remove from active menu
    verbose "deleting active menu entry $::names($s)"

    # figure out which it is
    # 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]
    # skip over tearoff
    for {set i 1} {$i <= $last} {incr i} {
 if {![catch {.m.e.active entrycget $i -label} label]} {
     if {[string equal $label $::names($s)]} break
 }
    }
    .m.e.active delete $i
    unset ::names($s)

    # 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
    }
}

######################################################################
# create windows
######################################################################
tk_setPalette $palette

menu .m -tearoff 0
.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} {
    set filesTearoff 1
} else {
    set filesTearoff 0
}
menu .m.f    -tearoff $filesTearoff -title "multixterm files"
menu .m.e    -tearoff 0
menu .m.help -tearoff 0
.m.f    add command -label Open -command openFile -underline 0

if {$filesLength} {
    .m.f add separator
    set files [lsort $files]
    set files [lrange $files 0 $fileMenuMax]
    foreach f $files {
 .m.f add command -label $f -command [list openFile $f]
    }
    .m.f add separator
}

.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
. config -m .m

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 .input -sticky ewns
grid .cmd   -sticky ew
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

proc about {} {
    set w .about
    if {[winfo exists $w]} {
 wm deiconify $w
 raise $w
 return
    }
    toplevel     $w
    wm title     $w "about multixterm"
    wm iconname  $w "about multixterm"
    wm resizable $w 0 0

    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],\
                                Tcl $::tcl_patchLevel,\
                                Tk $::tk_patchLevel"
    grid $w.title
    grid $w.version
    grid $w.author
    grid $w.using
    grid $w.b -sticky ew
}

proc help {} {
    if {[winfo exists .help]} {
 wm deiconify .help
 raise .help
 return
    }
    toplevel    .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"
    } else {
 # 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"
  break
     }
 }
 
 while {-1 != [gets $fid buf]} {
     if {0 == [regexp "^#(.?)(.*)" $buf X key buf]} break
     if {$key == "!"} {
  set buf [subst -nocommands $buf]
  set key " "
     }
     .help.text insert end $key$buf\n
 }
    }

    # support scrolling beyond Tk's built-in Next/Previous
    foreach w {"" .sb .text .ok} {
 set W .help$w
 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}
    }
}

proc scrollPage {dir} {
    tkScrollByPages .help.sb v $dir
    return -code break
}

proc scrollLine {dir} {
    tkScrollByUnits .help.sb v $dir
    return -code break
}

######################################################################
# exit handling
######################################################################

# 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.

proc xtermKillAll {} {
    foreach sid [array names ::xtermPid] {
 exec /bin/kill -9 $::xtermPid($sid)
    }
}

rename exit _exit
proc exit {{x 0}} {xtermKillAll;_exit $x}

wm protocol . WM_DELETE_WINDOW exit
trap exit SIGINT

######################################################################
# start any xterms requested
######################################################################
proc xtermStartAll {} {
    verbose "xtermNames = \"$::xtermNames\""
    foreach n $::xtermNames {
 regsub -all "%n" $::xtermCmd $n cmdOut
 xtermStart $cmdOut $n
    }
    set ::xtermNames {}
}

initLate

# 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]} {
    openFile $cmdFile
}

puts hello

Tcl and UNIX scripting saved the day again.

Labels:

0 Comments:

Post a Comment

<< Home