#!/bin/sh
# the next line restarts using wish \
		exec wish8.2 "$0" "$@"

#!/usr/local/bin/wish
## runit (tkRunIt) 12/4/98
## Ethan Gold <etgold@cs.columbia.edu>
##
## 

set version "0.94.1: 2/16/99"
set histfile "$env(HOME)/.runit_history"
set rcfile "$env(HOME)/.runitrc"
#### settable options ####
## maxhist currently unused
set maxhist 50
set runprompt "Run Command:"
set matchprompt "Matches:"
set resultprompt "Output:"
set histprompt "History:"
set running "Running: "
set wtitle "tkRunIt"
set location "+400+400"
set bgcolor "grey90"
set fgcolor "black"
set hicolor "grey85"
set vbhicolor "grey85"
set entrywidth 40
set mesgwidth $entrywidth
set maxwidth 80
set maxheight 60
set defaultfont "Helvetica -12"
set entryfont $defaultfont
set labelfont $defaultfont
set mesgfont $defaultfont
set listfont $defaultfont
set borderwidth 2
set defaultcmd ""
set showlabel 1
set verbose 1
set wait 0
set persistent 0
set prettyprint 1
set mouseloc 0
set autokill 1

#### end settable options ####
set histitem -1
## headdir of the form "directory/" - trailing slash included!
set headdir ""
set last_output ""
set selecting 0

##################################################################
############### Begin procedure definitions ######################
##################################################################

###################################
## place output into text widget
## and display it
###################################
proc show_output {results} {
    global mframepackcmd maxwidth mesgwidth history
    global maxheight verbose runprompt resultprompt
    
    if {![string length $results]} {
		.label configure -text "$runprompt"
		hide_output
		return
    }

    ## make sure we don't make a mess if verbose is off
    ## this is to catch side effects like history view
    #if {!$verbose} { pack forget .mframe }
    

    ## clean it out and make find the longest line
    .mframe.text configure -state normal
    .mframe.text delete 0.0 end
    ## note this test will always be true because
    ## of the above catch...
    if {[string length $results] > 0} {
		set longest 0
		foreach line [split $results \n] {
			set newlength [string length $line]
			if {$newlength > $longest} {
				set longest $newlength
			}
		}
		
		## get the message width correct
		if {$longest >= $maxwidth} {
			.mframe.text configure -width $maxwidth
		} elseif {$longest > $mesgwidth && $longest < $maxwidth} {
			.mframe.text configure -width $longest
		} else {
			.mframe.text configure -width $mesgwidth
		}
		
		## get the height correct
		set numlines [llength [split $results \n]]
		if {$numlines > $maxheight} {
			.mframe.text configure -height $maxheight
		} else {.mframe.text configure -height $numlines}

		.mframe.text insert 0.0 $results
		
		## generate the result prompt tail string
		## properly and display it
		if {[string length [lindex $history end]] > \
				[expr $longest - [string length $resultprompt]]} {
			set tailstring "[string range \
					[lindex $history end] 0 \
					[expr $longest - [string length $resultprompt]]]..."
		} else {set tailstring [lindex $history end]}
		.label configure -text "$resultprompt $tailstring"
		
    } else {
		.label configure -text "$runprompt"
		pack forget .mframe
		.mframe.text delete 0.0 end
    }
    
    pretty_print
    
    .mframe.text configure -state disabled

    if {$verbose} {
		update
		eval $mframepackcmd
		focus .mframe.text
		update
		fix_windowsize
    }

}

###################################
## make pretty colors in the output
###################################
proc pretty_print {} {
    global prettyprint
    
    if {$prettyprint} {

    }
}

###################################
## hide output window
###################################
proc hide_output {} {
    pack forget .mframe
    .label configure -text "Run Command:"
    focus .entry
    update
    fix_windowsize
}

###################################
## check the history or
## run the given commandline
###################################
proc runit {} {
    global cmdline history wait persistent histitem
    global running runprompt verbose last_output env histprompt autokill

    ## do ! history completion
    if {[regexp {^!!$} $cmdline]} {
		## last item, replace and return
		set cmdline [lindex $history end]
		.entry icursor end
		return
    } elseif {[regexp {^!!!$} $cmdline]} {
		## show numbered history in output window
		set i 0
		foreach item $history {
			if {[info exists histview]} {
				set histview "$histview\n$i: $item"
			} else { set histview "$i: $item" }
			incr i
		}
		if {![info exists histview]} {set histview ""}
		show_output $histview
		.label configure -text $histprompt
		set last_output $histview
		.mframe.text see "[llength [split $histview ":"]].0"
		if {!$verbose} {toggle_verbose}
		return
    } elseif {[regexp {^!([0-9]+)} $cmdline all index]} {
		## repeat numbered command
		set cmdline [lindex $history $index]
		return
    } elseif {[regexp {^!.*} $cmdline]} {
		## match a history item
		set matchstring "^[string trimleft $cmdline !].*"
		#puts $matchstring
		set max [expr [llength $history] - 1]
		for {set i $max} {$i > 0} {set i [expr $i-1]} {
			## if we match, replace the current and return
			if {[regexp $matchstring [lindex $history $i]]} {
				set cmdline [lindex $history $i]
				.entry icursor end
				return
			}
		}
		## no matches, select string and return
		.entry selection range 1 end
		return
    }
    
    ## run it, foreground or background
    hide_completions
    ## make sure we don't complain about filename& or cmd|cmd
    if {[regexp {.*[^\ ]&$} $cmdline]} {
		set cmdlength [string length $cmdline]
		set cmdlength [expr $cmdlength - 2]
		set cmdline "[string range $cmdline 0 $cmdlength] &"
    }

    ## Do wildcard, environment, tilde expansion and pipe spacing
    set newcmd $cmdline
    set oldcmd $cmdline
    if {[regexp {\*|\?|\[*\]|\||\$|\~} $cmdline]} {
		#puts "cmdline contains wildcards, tildes, variables, or pipes"
		set newcmd ""
		foreach item [split $cmdline] {
			## pad "|" with spaces
			if {[regexp {(.*[^\ ]+)\|([^\ ]+.*)} $item all cmdA cmdB]} {
				#puts "substituting \"$cmdA | $cmdB\" for \"$item\""
				set item "$cmdA | $cmdB"
			}
			## expand tilde
			if {[regexp {(.*)\~(.*)} $item all first second]} {
				if {[info exists env(HOME)]} {
					set item "$first$env(HOME)$second"
				}
			}
			## expand environment variables
			if {[regexp {(.*)\$([A-Z]+)(.*)} $item all first var second]} {
				if {[info exists env($var)]} {
					set item "$first$env($var)$second"
				}
			}
			## glob items with wildcards...
			if {[regexp {\*|\?|\[*\]} $item]} {
				set tmp [glob -nocomplain $item]
				set newcmd "$newcmd $tmp"
			} else { ;# but not items without wildcards...
				set newcmd "$newcmd $item "
			}
		}
    }
    
    ## grey out the contents first
    .label configure -text "$running $cmdline"
    .entry selection range 0 end
    update
    ## try to do multiple command execution
    ## and pick out cd references
    set numcmds [llength [split $newcmd \;]]
    set cmdcount 1
    foreach subcmd [split $newcmd \;] {
		#puts "subcmd = $subcmd"
		
		if {[regexp {^[\ ]*cd (.*)$} $subcmd all subdir]} {
			#puts "caught cd command to $subdir"
			catch {cd $subdir} errors
		} elseif {[regexp {^[\ ]*cd[\ ]*$} $subcmd all subdir]} {
			#puts "caught cd command to homedir"
			catch {cd} errors
		} else {
			if {!$wait && $cmdcount == $numcmds} {
				catch {eval exec "$subcmd" &} errors
			} else {
				catch {eval exec "$subcmd"} errors
			}
		}
		incr cmdcount
    }

    update_history
    set last_output $errors
    show_output $last_output
    ## if we've got a process number from a "&" commandline
    ## and autokill is set, then force a commandline
    if {$autokill \
			&& [regexp {.*&[\ ]*$} [lindex $history end]] \
			&& [regexp {[0-9]+} [lindex [split $errors] 0]]} {
		set cmdline "kill $errors"
    }

    if {!$persistent} {exitfunc}
}

###################################
## update history file
###################################
proc update_history {} {
    global maxhist cmdline histitem history histfile

	 set histout [open $histfile a]
    ## write it to the history file, save it locally
	 if {[string compare $cmdline ""]} {
		  puts $histout "$cmdline"
		  flush $histout
		  lappend history $cmdline
	 }
	 close $histout
	 unset histout
    set cmdline ""
    set histitem -1
}


###################################
## traverse the history list
###################################
proc history_traverse {direction} {
    global histitem cmdline history
    
    ## save whatever's been typed - current is last item in history
    if {$histitem == -1} {
		lappend history $cmdline
		set histitem [expr [llength $history] - 1]
    } elseif {$histitem == [expr [llength $history] - 1]} {
		set history [lrange $history 0 [expr [llength $history]-2]]
		lappend history $cmdline
    }
    
    if {![string compare $direction "up"]} {
		if {$histitem > 0} {
			set cmdline [lindex $history [expr $histitem-1]]
			.entry icursor end
			set histitem [expr $histitem-1]
		}
    } elseif {![string compare $direction "down"]} {
		if {$histitem < [expr [llength $history] - 1]} {
			set cmdline [lindex $history [expr $histitem+1]]
			.entry icursor end
			incr histitem
		}
    }
}

###################################
## try to do filename completion - assume
## we're at the end of the string
###################################
proc complete {} {
    global cmdline headdir lframepackcmd verbose
    global maxheight mesgwidth env
	 
    set name [lindex $cmdline end]
    set options ""
    set varflag 0
	 
    ## first see if it's an environment variable
    if {[regexp {^\$([A-Z]+)} $name all varstub]} {
		  set varflag 1
		  foreach var [array names env] {
				if {[regexp "^$varstub.*" $var]} {
					 set options "$options \$$var"
				}
		  }
    } else {
		  ## filename completion
		  set options [glob -nocomplain -- "$name*"]
	 }
    
    #puts "$name, $options"
    
    if {[llength $options] == 0} {
		## if no matches
		
		## if it's an environment variable handle it this way:
		if {$varflag} {
			set start [expr [string length $cmdline] \
					- [string length $name] + 1]
			.entry selection range $start end
			return
		}
		## otherwise it's a file, so handle it this way:
		## look for the last good section and select
		## the remaining bad piece in the entry widget
		if {[regexp {^\/.*} $name]} {
			set absolute 1
		} else {
			set absolute 0
		}
		set tmpname1 ""
		set tmpname2 ""
		set i 1
		foreach piece [split $name /] {
			if {$absolute && $i == 1} {
				set tmpname1 "/$piece"
			} elseif {!$absolute && $i == 1} {
				set tmpname1 "$piece"
			} else {
				set tmpname1 "$tmpname2/$piece"
			}

			if {![file exists $tmpname1]} {break}
			set tmpname2 $tmpname1
			incr i
		}
		#puts $tmpname2
		set start [expr [string length $cmdline] \
				- [string length $name] + [string length $tmpname2] + 1]
		.entry selection range $start end
    } elseif {[llength $options] == 1} {
		## if one unique match

		## do this in either case
		if {$verbose} {pack forget .lframe}
		
		## if it's an environment variable handle it this way:
		## assume no embedded spaces or special characters
		## needed to escape such things
		if {$varflag} {
			set newend [expr [llength $cmdline] - 2]
			set cmdline "[lrange $cmdline 0 $newend] $options"
			.entry icursor end
			return
		}
		## otherwise it's a file, so handle it this way:
		set newend [expr [llength $cmdline] - 2]
		## baskslashify spaces and make sure we don't
		## include those infernal braces
		regsub -all { } $options {\ } options
		set options [string trim $options \{\}]
		set cmdline "[lrange $cmdline 0 $newend] $options"
		if {[file isdirectory [lindex $cmdline end]]} {
			set cmdline "$cmdline/"
		}
		.entry icursor end
    } elseif {[llength $options] > 1} {
		## if several matches

		## if it's a file, set headdir
		## save the head directory
		  if {!$varflag} {
				set headdir "[file dirname [lindex $options 0]]/"
		  } else {set headdir ""}
		## generate a list of short names with directories indicated
		foreach option $options {
			if {[file isdirectory $option]} {
				set shortoption "[file tail $option]/"
			} else {set shortoption "[file tail $option]"}
			lappend shortoptions $shortoption
		}
		
		set submatch ""
		set done 0
		for {set i 0} {$i < [string length [lindex $shortoptions 0]]} {incr i} {
			set matchchar [string index [lindex $shortoptions 0] $i]
			foreach option $shortoptions {
				if {[string index $option $i] != $matchchar} {set done 1}
			}
			if {!$done} {set submatch "$submatch$matchchar"} else {break}
		}

		## complete the common initial substring
		set cmdline "[lrange $cmdline 0 [expr [llength $cmdline] - 2]] $headdir$submatch"

		.entry icursor end

		## only do this if we're in verbose mode
		if {$verbose} {
			set listoptions ""
			.label configure -text "Matches:"
			.lframe.listbox delete 0 end
			set longest 0
			set shortoptions [lsort -dictionary $shortoptions]
			foreach option $shortoptions {
				.lframe.listbox insert end "$option"
				if {[string length $option] > $longest} {
					set longest [string length $option]
				}
			}
			.lframe.listbox configure -width $mesgwidth
			if {[llength $options] >= $maxheight} {
				.lframe.listbox configure -height $maxheight
			} else {.lframe.listbox configure -height [llength $options]}
			pack forget .mframe
			eval $lframepackcmd
			.lframe.listbox selection set 0
			focus .lframe.listbox
			update
			fix_windowsize
		}
	}
}

#################################
## make things behave after a
## manual resize
#################################
proc fix_windowsize {} {
    ## make sure we sanitize things after a manual resize
    if {[winfo reqheight .] != [winfo height .] \
			|| [winfo reqwidth .] != [winfo width .]} {
		puts "window has been resized"
		wm geometry . "[winfo reqwidth .]x[winfo reqheight .]"
    }
}

#################################
## hide completion listbox
#################################
proc hide_completions {} {
    global selecting

    set selecting 0
    pack forget .lframe
    .label configure -text "Run Command:"
    focus .entry
    update
    fix_windowsize
}

#################################
## pick a file from the listbox
#################################
proc getfile {} {
    global cmdline headdir runprompt selecting

    ## if we're not doing multiple keyboard selections
    ## then replace the last item, otherwise just append them.
    set newcmdline $cmdline
    if {!$selecting} {
		set lastindex [expr [llength $cmdline] - 2]
		if {$lastindex < 0} {set newcmdline ""} else {
			set newcmdline [lrange $cmdline 0 $lastindex]
		}
    }

    ## append the items
    foreach item [.lframe.listbox curselection] {
		set itemname [.lframe.listbox get $item]
		## baskslashify spaces and make sure we don't
		## include those infernal braces
		regsub -all { } $itemname {\ } itemname
		set itemname [string trim $itemname \{\}]
		set newcmdline "$newcmdline $headdir$itemname"
    }

    set cmdline $newcmdline
    #pack forget .lframe
    focus .entry
    .entry icursor end
    .label configure -text "$runprompt"
    set selecting 1
}

###########################
## toggle verbose operation
###########################
proc toggle_verbose {} {
    global verbose labelpackcmd last_output
    global wtitle hicolor vbhicolor showlabel

    if {!$verbose} {
		set verbose 1
		.entry configure -highlightcolor $vbhicolor
		if {$showlabel} {
			eval $labelpackcmd
		}
		show_output $last_output
		wm title . "$wtitle verbose"
		update
    } else {
		set verbose 0
		if {$showlabel} {
			pack forget .label
		}
		hide_output
		hide_completions
		.entry configure -highlightcolor $hicolor
		focus .entry
		wm title . "$wtitle"
		update
		fix_windowsize
    }
}

###########################
## clean up and exit
###########################
proc exitfunc {} {
	 #global histout
	 #flush $histout
	 #close $histout
	 exit
}
##############################################
########## Begin Main Execution ##############
##############################################

## read in resource file
if {[file readable $rcfile]} {source $rcfile}
set cmdline $defaultcmd
wm title . $wtitle

set options 0
## evaluate command line - overrides config file
if {$argc > 0} {
    foreach arg $argv {
		if {![string compare $arg "--wait"]\
				|| ![string compare $arg "-w"]} {
			set wait 1
			incr options
		} elseif {![string compare $arg "++wait"]\
				|| ![string compare $arg "+w"]} {
			set wait 0
			incr options
		} elseif {![string compare $arg "--persistent"]\
				|| ![string compare $arg "-p"]} {
			set persistent 1
			incr options
		} elseif {![string compare $arg "++persistent"]\
				|| ![string compare $arg "+p"]} {
			set persistent 0
			incr options
		} elseif {![string compare $arg "--location"]\
				|| ![string compare $arg "-l"]} {
			set location [lindex $argv [expr $options+1]]
			incr options
			incr options
		} elseif {![string compare $arg "--mouseloc"]\
				|| ![string compare $arg "-m"]} {
			set mouseloc 1
			incr options
		} elseif {![string compare $arg "++mouseloc"]\
				|| ![string compare $arg "+m"]} {
			set mouseloc 0
			incr options
		}
    }
    if {$options < $argc} {set cmdline [lrange $argv $options end]}
}

## open/create history file, enforcing maxhist conditions
if {[file readable $histfile]} {
    set histin [open $histfile r]
    while {[gets $histin line] >= 0} {
		lappend history $line
    }
    close $histin
    if {[info exists history]} {
		if {[llength $history] > $maxhist} {
			set history [lrange $history \
					[expr [llength $history] - $maxhist] end]
			file delete $histfile
			set histout [open $histfile w]
			foreach item $history {puts $histout $item}
			close $histout
		}
    } else {lappend history ""}
}

## and appending new lines
#set histout [open $histfile a]

## main entry widget
entry .entry -width $entrywidth -textvariable cmdline -exportselection 0 \
		-foreground $fgcolor \
		-background $bgcolor \
		-insertbackground $fgcolor \
		-highlightbackground $bgcolor \
		-highlightcolor $hicolor -highlightthickness $borderwidth
## output/status label
label .label -text $runprompt -wraplength [expr $maxwidth*10] \
		-justify left -font $labelfont \
		-foreground $fgcolor -background $bgcolor
## file completion listbox
frame .lframe
listbox .lframe.listbox -yscrollcommand ".lframe.scroll set" \
		-highlightcolor $hicolor -background white -font $listfont \
		-width [expr $entrywidth - 5] -selectmode extended \
		-foreground $fgcolor -background $bgcolor
scrollbar .lframe.scroll -command ".lframe.listbox yview" -takefocus 0 \
		-background $bgcolor -troughcolor $hicolor

## output box
frame .mframe
text .mframe.text -yscrollcommand ".mframe.scroll set" \
		-highlightcolor $hicolor -font $listfont \
		-width [expr $mesgwidth - 5] -wrap char -font $mesgfont \
		-relief groove -height 1 \
		-foreground $fgcolor -background $bgcolor
scrollbar .mframe.scroll -command ".mframe.text yview" -takefocus 0 \
		-background $bgcolor -troughcolor $hicolor

. configure -background $bgcolor

## if the mouseloc flag is true
## find where the mouse is and show up there
if {$mouseloc} {
    set mx [winfo pointerx .]
    set my [winfo pointery .]
    if {$mx >= 0 && $my >= 0} {
		  set mx [expr $mx - [winfo reqwidth .]]
		  set my [expr $my - 12]
		  ## make sure we don't end up off the screen
		  set scx [winfo screenwidth .]
		  set scy [winfo screenheight .]
		  if {$mx < 0} {
				set mx "+0"
		  } elseif {$mx > [expr $scx - [winfo reqwidth .entry]]} {
				set mx "-0"
		  } else {set mx "+$mx"}

		  if {$my < 0} {
				set my "+0"
		  } elseif {$my > [expr $scy - [winfo reqheight .entry]]} {
				set my "-[winfo reqheight .entry]"
		  } else { set my "+$my"}
		  set location "$mx$my"
    }
}
wm geometry . $location

## pack it all
pack .lframe.listbox -side left -fill x -fill y -expand 1
pack .lframe.scroll -side right -expand 1 -fill y
pack .mframe.text -side left -fill x -fill y -expand 1
pack .mframe.scroll -side right -expand 1 -fill y
pack .entry -padx 1 -pady 1 -fill x

set labelpackcmd "pack .label -fill x -anchor w"
set lframepackcmd "pack .lframe -fill x -fill y -expand 1 -pady 2"
set mframepackcmd "pack .mframe -fill x -fill y -expand 1 -pady 2"

if {$verbose} {set verbose 0; toggle_verbose}


## make sure the cursor is at the end of the cmdline
## at startup - makes sense if a cmdstring has been
## passed in from the commandline
.entry icursor end

## bind keys for entry widget
bind .entry <Return> {
    if {[string compare $cmdline ""]} {runit} else {
		.label configure -text $runprompt
		hide_output
		hide_completions
    }
}
bind .entry <Tab> {complete;break}
bind .entry <Control-p> {history_traverse up}
bind .entry <Up> {history_traverse up}
bind .entry <Control-n> {history_traverse down}
bind .entry <Down> {history_traverse down}
bind .entry <Control-v> {toggle_verbose; break}
bind .entry <Control-r> {source $rcfile}
bind .entry <Control-c> {set cmdline ""; break}
bind .entry <Control-w> {exitfunc}
bind .entry <Control-q> {exitfunc}
bind .entry <Escape> {exitfunc}
bind .entry <Shift-Tab> {
    if [winfo ismapped .mframe.text] {
		focus .mframe.text; break
    } elseif [winfo ismapped .lframe.listbox] {
		focus .lframe.listbox; break
    }
}

## bind keys in listbox
bind .lframe.listbox <Return> {getfile; hide_completions}
bind .lframe.listbox <Control-Return> {getfile; focus .lframe.listbox}
bind .lframe.listbox <Double-Button-1> {getfile}
bind .lframe.listbox <Button-1> {focus .lframe.listbox}
bind .lframe.listbox <Tab> {focus .entry; break}
bind .lframe.listbox <Control-v> {toggle_verbose; break}
bind .lframe.listbox <Control-a> {.lframe.listbox selection set 0 end}
bind .lframe.listbox <Control-n> {event generate .lframe.listbox <Down>}
bind .lframe.listbox <Control-p> {event generate .lframe.listbox <Up>}
bind .lframe.listbox <Control-c> {hide_completions; break}
bind .lframe.listbox <Control-w> {exitfunc}
bind .lframe.listbox <Control-q> {exitfunc}
bind .lframe.listbox <Escape> {exitfunc}

## bind keys in textbox
bind .mframe.text <Tab> {focus .entry;break}
bind .mframe.text <Return> {focus .entry;break}
bind .mframe.text <Button-1> {focus .mframe.text}
bind .mframe.text <Control-v> {toggle_verbose; break}
#bind .mframe.text <Control-a> {.mframe.text tag add "sel" 0.0 end}
bind .mframe.text <Control-n> {event generate .mframe.text <Down>; break}
bind .mframe.text <Control-p> {event generate .mframe.text <Up>; break}
bind .mframe.text <Control-c> {set last_output "";hide_output; break}
bind .mframe.text <Control-w> {exitfunc}
bind .mframe.text <Control-q> {exitfunc}
bind .mframe.text <Escape> {exitfunc}

## bind keys for main window, in case
## it doesn't get focus (shouldn't be a problem anymore)
bind . <Control-c> {set cmdline ""; break}
bind . <Control-w> {exitfunc}
bind . <Control-q> {exitfunc}
bind .entry <Escape> {exitfunc}

focus .entry
