#!/usr/bin/env wish-f

# $Id: tkdiff,v 1.6 1995/02/08 18:06:56 klassa Exp $

###############################################################################
#                                                                             #
#                    TkDiff -- graphical diff, using Tcl/Tk                   #
#                                                                             #
# Author: John Klassa (klassa@aur.alcatel.com)                                #
# Usage:  tkdiff <file1> <file2>                                              #
#                                                                             #
###############################################################################

###############################################################################
#
# THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
# NO EVENT SHALL JOHN KLASSA OR ALCATEL NETWORK SYSTEMS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
###############################################################################

global opts

###############################################################################
# Source Paul Raines' emacs bindings file if possible.
###############################################################################

set emacs_bindings_file "/homes/klassa/public/lib/tcl-tk-lib/bindings.tk"
catch {source $emacs_bindings_file}

###############################################################################
# Set defaults...
###############################################################################

set g(cfilo)     ""
set g(cfiln)     ""
set g(currdiff)  ""
set g(destroy)   ""

set opts(diffopt) "-w"
set opts(currtag) "-background black -foreground white"
set opts(difftag) "-background white -foreground black -font 6x13bold"
set opts(textopt) "-background white -foreground black -font 6x13"
set opts(expfilo) 1
set opts(expfiln) 1
set opts(cmplimt) 100

###############################################################################
# Source ~/.tkdiffrc, to override defaults (if desired).
###############################################################################

catch {source ~/.tkdiffrc}

###############################################################################
# Throw up a modal error dialog.
###############################################################################

proc do-error {msg} {
    tk_dialog .tkerror "tk error" "$msg" warning {0} "Ack!"
}

###############################################################################
# Copy and count the args.
###############################################################################

set count  0
set extras {}

foreach arg "$argv" {
    incr count
    if {$count > 2} {
        lappend extras $arg
    } elseif {$count == 1} {
        set g(filo) $arg
    } elseif {$count == 2} {
        set g(filn) $arg
    }
}

if {$count == 1} {
    set g(filn) $g(filo)
    incr count
} elseif {$count > 2} {
    set extras [join [split $extras] ", "]
    do-error "Extra argument(s) \"$extras\" ignored."
}

###############################################################################
# Set up the display...
###############################################################################

# Pack the frame that holds the text widgets and scrollbars (.f).

pack [frame .f -background black] -ipady 2 -ipadx 2 -fill both -expand y

# Pack the "old" and "new" widgets (.f.o and .f.n).

foreach mod {o n} {

    pack [frame .f.$mod -background black] \
	    -ipadx 2 -ipady 2 -fill both -expand y -side left
    pack [label .f.$mod.name -textvariable g(cfil$mod)] \
	    -side top -fill x -pady 1
    pack [frame .f.$mod.f] \
	    -side top -fill x
    pack [label .f.$mod.f.lbl -text "change filename: "] \
	    -side left
    pack [entry .f.$mod.f.entry -textvariable g(fil$mod) -relief sunken] \
	    -side left -fill x -expand y -pady 1
    pack [checkbutton .f.$mod.f.exp -text "expand" \
	    -variable opts(expfil$mod)] \
	    -side left
    pack [button .f.$mod.f.clr -text "clear" \
	    -command ".f.$mod.f.entry delete 0 end"] \
	    -side left

    catch "bind_emacsentry .f.$mod.f.entry"

    bind .f.$mod.f.entry <Return> {do-diff}

    bind .f.$mod.f.entry <Tab> \
	    "if {\$opts(expfil$mod)} { fnc:complete g(fil$mod) }"
}

# Pack the text widgets and the scrollbars.

pack [scrollbar .f.o.scr -command {.f.o.text yview}] \
        -side left -fill y
pack [text .f.o.text -yscroll scrolls-set -height 40 -width 87] \
        -side left -fill both -expand y -padx 1 -pady 1
pack [scrollbar .f.n.scr -command {.f.n.text yview}] \
        -side right -fill y
pack [text .f.n.text -yscroll {.f.n.scr set} -height 40 -setgrid 1 -width 87] \
        -side right -fill both -expand y -padx 1 -pady 1

# Pack the meta-scrollbar (.f.scr).

pack [scrollbar .f.scr -command texts-yview] \
        -after .f.o -side left -fill y -padx 2 -pady 2

# Pack the bottom-row buttons (inside .b).

pack [frame .b] \
        -side bottom -fill x -ipady 2 -ipadx 2

# Pack the quit, help, swap and rediff buttons.

pack [button .b.quit -text quit -command {destroy .}] \
        -side left -fill x -expand y -padx 4
pack [button .b.help -text help -command {do-help}] \
        -side left -fill x -expand y -padx 4
pack [button .b.swap -text swap -command {
    set tmp $g(filo) ; set g(filo) $g(filn) ; set g(filn) $tmp ; do-diff }] \
	    -side left -fill x -expand y -padx 4
pack [button .b.rediff -text rediff -command do-diff] \
        -side left -fill x -expand y -padx 4

# Pack the options widget.

pack [frame .b.opts -relief raised] \
        -side left -padx 4 -ipady 1 -ipadx 1
pack [label .b.opts.label -text "opts:"] \
        -side left -padx 1
pack [entry .b.opts.entry -textvariable opts(diffopt) \
	-relief sunken] \
        -side left -fill x -expand y -padx 2
catch {bind_emacsentry .b.opts.entry}

# Pack the "current diff" widgets.

pack [frame .b.pos -relief raised] \
        -side left -padx 4 -ipady 1 -ipadx 1
pack [label .b.pos.clabel -text "curr ("] \
        -side left -padx 1
pack [menubutton .b.pos.menubutton -menu .b.pos.menubutton.menu \
        -width 5 -textvariable g(pos) -relief raised] \
        -side left
menu .b.pos.menubutton.menu
pack [label .b.pos.nlabel -text "of"] \
        -side left
pack [label .b.pos.num -textvariable g(count)] \
        -side left
pack [label .b.pos.phdr -text "):"] \
        -side left
pack [label .b.pos.curr -textvariable g(currdiff) -width 30 -relief raised] \
        -side left

# Pack the next and prev buttons.

pack [button .b.prev -text prev -command {move -1}] \
        -side right -fill x -expand y -padx 4
pack [button .b.next -text next -command {move 1}] \
        -side right -fill x -expand y -padx 4

# Give the window a name & allow it to be resized.

wm title   . "TkDiff v1.0b6"
wm minsize . 20 5
wm maxsize . 160 70

# Set up text tags for the 'current diff' (the one chosen by the 'next'
# and 'prev' buttons) and any ol' diff region.  All diff regions are
# given the 'diff' tag initially...  As 'next' and 'prev' are pressed,
# to scroll through the differences, one particular diff region is
# always chosen as the 'current diff', and is set off from the others
# via the 'diff' tag -- in particular, so that it's obvious which diffs
# in the left and right-hand text widgets match.

eval ".f.o.text configure $opts(textopt)"
eval ".f.n.text configure $opts(textopt)"
eval ".f.o.text tag configure curr $opts(currtag)"
eval ".f.n.text tag configure curr $opts(currtag)"
eval ".f.o.text tag configure diff $opts(difftag)"
eval ".f.n.text tag configure diff $opts(difftag)"

###############################################################################
# Read (while untabifying) the text in the file "fn" into the variable "var".
###############################################################################

proc untabify {var fn} {
    upvar $var v

    set hndl [open "$fn" r]
    set v [read $hndl]
    close $hndl

    # Pipe through an embedded Perl script.  Thanks to Wayne Throop for
    # the concept.

    set v [exec perl -e {
	while (<>) {
	    @chunks = split(/([\b\t\r])/);
	    $pos    = 0;
	    for $chunk (@chunks) {
		if ($chunk eq "\t") {
		    $spaces = 8 - ($pos % 8);
		    print " " x $spaces;
		    $pos += $spaces;
		}
		else {
		    if ($chunk eq "\r") {
			$pos = 0;
		    }
		    elsif ($chunk eq "\b") {
			--$pos unless $pos < 1;
		    }
		    else {
			$pos += length($chunk);
		    }
		    print "$chunk";
		}
	    }
	}
    } << $v]\n
}

###############################################################################
# Scroll all windows.  Credit to Wayne Throop...
###############################################################################

proc texts-yview {amt} {
    global g

    .f.o.text yview $amt

    set newamt [expr $amt - int([.f.o.text index end]) + \
                            int([.f.n.text index end])]

    for {set low 1; set high $g(count); set i [expr ($low+$high)/2]} \
        {$i >= $low}                                                 \
	{set i [expr ($low+$high)/2]} {

        scan $g(pdiff,$i) "%s %d %d %d %d" line s1 e1 s2 e2

        if {$s1 > $amt} {
            set newamt [expr $amt - $s1 + $s2]
            set high [expr $i-1]
        } else {
            set low [expr $i+1]
        }
    }

    .f.n.text yview $newamt

    # patch from joe@morton.rain.com (Joe Moss) -- thanks!
    if {[llength $g(diff)] > 0} {
	move [expr $i + 1] 0 0
    }
}

###############################################################################
# Set all scrollbars.  Credit to Wayne Throop...
###############################################################################

proc scrolls-set {a1 a2 a3 a4} {
    .f.o.scr set $a1 $a2 $a3 $a4
    .f.scr   set $a1 $a2 $a3 $a4
    # allow the "new" (rightmost) window's scrollbar to drift
    # .f.n.scr set $a1 $a2 $a3 $a4
}

###############################################################################
# Extract the start and end lines for file1 and file2 from the diff
# stored in "line".
###############################################################################

proc extract {line} {

    if [regexp {^([0-9]+)(a|c|d)} $line d digit action] {
        set s1 $digit
        set e1 $digit
    } elseif [regexp {^([0-9]+),([0-9]+)(a|c|d)} $line d start end action] {
	set s1 $start
	set e1 $end
    }

    if [regexp {(a|c|d)([0-9]+)$} $line d action digit] {
        set s2 $digit
        set e2 $digit
    } elseif [regexp {(a|c|d)([0-9]+),([0-9]+)$} $line d action start end] {
	set s2 $start
	set e2 $end
    }

    return "$line $s1 $e1 $s2 $e2 $action"
}

###############################################################################
# Add a tag to a region.
###############################################################################

proc add-tag {wgt tag start end type new} {
    if {$type == "c" || ($type == "a" && $new) || ($type == "d" && !$new)} {
        $wgt tag add $tag $start.0 [expr $end + 1].0
    } else {
        for {set idx $start} {$idx <= $end} {incr idx} {
            $wgt tag add $tag $idx.0 $idx.6

        }
    }
}

###############################################################################
# Move the "current" diff indicator (i.e. go to the next or previous diff
# region if "relative" is 1; go to an absolute diff number if "relative"
# is 0).
###############################################################################

proc move {value {relative 1} {setpos 1}} {
    global g

    scan $g(pdiff,$g(pos)) "%s %d %d %d %d %s" dummy s1 e1 s2 e2 dt

    # Replace the 'diff' tag (and remove the 'curr' tag) on the current
    # 'current' region.

    .f.o.text tag remove curr $s1.0 [expr $e1 + 1].0
    .f.n.text tag remove curr $s2.0 [expr $e2 + 1].0

    add-tag .f.o.text diff $s1 $e1 $dt 0
    add-tag .f.n.text diff $s2 $e2 $dt 1

    # Bump 'pos' (one way or the other).

    if {$relative} {
        set g(pos) [expr $g(pos) + $value]
    } else {
        set g(pos) $value
    }

    # Range check 'pos'.

    if {$g(pos) > [llength $g(diff)]} {
        set g(pos) [llength $g(diff)]
    }
    if {$g(pos) < 1} {
        set g(pos) 1
    }

    # Figure out which lines we need to address...

    scan $g(pdiff,$g(pos)) "%s %d %d %d %d %s" g(currdiff) s1 e1 s2 e2 dt

    # Remove the 'diff' tag and add the 'curr' tag to the new 'current'
    # diff region.

    .f.o.text tag remove diff $s1.0 [expr $e1 + 1].0
    .f.n.text tag remove diff $s2.0 [expr $e2 + 1].0

    add-tag .f.o.text curr $s1 $e1 $dt 0
    add-tag .f.n.text curr $s2 $e2 $dt 1

    # Move the view on both text widgets so that the new region is
    # visible.

    if {$setpos} {
	.f.o.text yview -pickplace [expr $s1 - 1]
	.f.n.text yview -pickplace [expr $s2 - 1]
    }
}

###############################################################################
# Expand filenames by replacing $foo with $env(foo) & evaluating the result.
###############################################################################

proc fnc:expand {fn} {
    global env
    upvar $fn f
    regsub -all {\$([^ /\.]+)} $f   {$env(\1)}         tmp
    regsub      {^~$}          $tmp {$env(HOME)}       tmp
    regsub      {^~/}          $tmp {$env(HOME)/}      tmp
    regsub      {^~([^/]+)}    $tmp {$env(HOME)/../\1} tmp
    catch "set f $tmp"
}

###############################################################################
# Finish off a filename if possible.  If not, and the number of possibilities
# is less than 'max', complete as much of the filename as possible.  Note
# that the default value for 'max' is 100.
###############################################################################

proc fnc:finish {fn {max 100}} {
    upvar $fn f

    set choices [glob -nocomplain $f*]
    set numchoices [llength "$choices"]

    if {$numchoices == 1} {
        set f $choices
	return 1
    } elseif {$numchoices > $max} {
	return 0
    }

    set choice [lindex "$choices" 0]
    set min    [string length "$choice"]
    set golden "$choice"

    for {set idx 1} {$idx < [llength "$choices"]} {incr idx} {
	set choice [lindex "$choices" $idx]
	set count [string length $choice]
	if {$count < $min} {
	    set min $count
	    set golden "$choice"
	}
    }

    while {$min > 0} {
	set match 1
	foreach choice "$choices" {
	    if {[string range "$choice" 0 $min] != "$golden"} {
		set match 0
		break
	    }
	}
	if {$match} {
	    if {[string length $golden] > 0} {
		set f "$golden"
	    }
	    break;
	}
	incr min -1
	set golden [string range "$golden" 0 $min]
    }

    return 0
}

###############################################################################
# Do filename completion by expanding environment variables, resolving
# directories, and then completing as much of the filename as possible.
###############################################################################

proc fnc:complete {fn {max 100}} {
    upvar $fn f

    fnc:expand f
    fnc:finish f $max

    if {[file isdirectory $f]} {
	set dst $f
	set suf {}
    } else {
	set dst [file dirname $f]
	set suf [file tail $f]
    }

    if {[file isdirectory $dst]} {
	set c [pwd]
	catch {cd $dst}
	set f [pwd]
	cd $c
	append f /$suf
    }

    set f [string trimright $f /]

    if {[llength [glob -nocomplain $f*]] < 2 && [file isdirectory $f]} {
	append f /
    }
}

###############################################################################
# Change the state on all of the diff-sensitive buttons.
###############################################################################

proc buttons {{newstate "normal"}} {
    foreach b {.b.pos.menubutton .b.next .b.prev} {
	eval "$b configure -state $newstate"
    }
}

###############################################################################
# Wipe the slate clean...
###############################################################################

proc wipe {} {
    global g

    set g(pos)   0
    set g(count) 0
    set g(cfilo) ""
    set g(cfiln) ""

    foreach mod {o n} {
	.f.$mod.text configure -state normal
	.f.$mod.text tag remove diff 1.0 end
	.f.$mod.text tag remove curr 1.0 end
	.f.$mod.text delete 1.0 end
    }

    if {[string length $g(destroy)] > 0} {
        eval $g(destroy)
        set g(destroy) ""
    }

    .b.pos.menubutton.menu delete 0 last

    buttons disabled
}

###############################################################################
# Ready the display...
###############################################################################

proc ready-display {} {
    global g

    set g(cfilo) $g(filo)
    set g(cfiln) $g(filn)
}

###############################################################################
# Resolve the two filename arguments.
###############################################################################

proc resolve-filenames {} {
    global g
    global opts

    if {$opts(expfilo)} { fnc:complete g(filo) $opts(cmplimt) }
    if {$opts(expfiln)} { fnc:complete g(filn) $opts(cmplimt) }

    if {[file isdirectory $g(filo)] && [file isdirectory $g(filn)]} {
        do-error "Either <file1> or <file2> must be a plain file."
        return 1
    }

    if {[file isdirectory $g(filo)]} {
	set g(filo) "[string trimright $g(filo) /]/[file tail $g(filn)]"
    } elseif {[file isdirectory $g(filn)]} {
	set g(filn) "[string trimright $g(filn) /]/[file tail $g(filo)]"
    }

    if {[file exists $g(filo)] != 1} {
        do-error "No such file: $g(filo)"
        return 1
    } elseif {[file exists $g(filn)] != 1} {
        do-error "No such file: $g(filn)"
        return 1
    }

    return 0
}

###############################################################################
# Mark difference regions and build up the jump menu.
###############################################################################

proc mark-diffs {} {
    global g

    set different 0
    set numdiff [llength [split "$g(diff)" \n]]

    # If there are <= 30 diffs, do a one-level jump menu.  If there are
    # more than 30, do a two-level jump menu with sqrt(numdiff) in each
    # level.

    if {$numdiff <= 30} {

        set g(destroy) "$g(destroy) \
                catch \"eval .b.pos.menubutton.menu delete 0 last\"\n"

        foreach d [split "$g(diff)" \n] {

            incr g(count)

            set g(pdiff,$g(count)) "[extract $d]"

            scan $g(pdiff,$g(count)) "%s %d %d %d %d %s" dummy s1 e1 s2 e2 dt

            add-tag .f.o.text diff $s1 $e1 $dt 0
            add-tag .f.n.text diff $s2 $e2 $dt 1

            set different 1

            .b.pos.menubutton.menu add command \
                    -font 6x12 \
                    -label [format "%-6d --> %s" $g(count) $d] \
                    -command "move $g(count) 0"
        }
    } else {

        set target 0
        set increment [expr int(pow($numdiff,0.5))]

        foreach d [split "$g(diff)" \n] {

            incr g(count)

            if {$g(count) >= $target} {

                .b.pos.menubutton.menu add cascade -label $target \
                        -menu .b.pos.menubutton.menu.$target
                menu .b.pos.menubutton.menu.$target

                set current $target
                set target [expr $target + $increment]

                set g(destroy) \
                      "$g(destroy) \
                      catch \"eval .b.pos.menubutton.menu.$current \
                      delete 0 last\"\n \
                      catch \"eval destroy .b.pos.menubutton.menu.$current\"\n"
            }

            set g(pdiff,$g(count)) "[extract $d]"

            scan $g(pdiff,$g(count)) "%s %d %d %d %d %s" dummy s1 e1 s2 e2 dt

            add-tag .f.o.text diff $s1 $e1 $dt 0
            add-tag .f.n.text diff $s2 $e2 $dt 1

            set different 1

            .b.pos.menubutton.menu.$current add command \
                    -font 6x12 \
                    -label [format "%-6d --> %s" $g(count) $d] \
                    -command "move $g(count) 0"
        }
    }

    return $different
}

###############################################################################
# Compute differences (start over, basically).
###############################################################################

proc rediff {} {
    global g
    global opts

    wipe
    if {[resolve-filenames]} { return }
    ready-display

    # Read the files into their respective widgets & add line numbers.

    foreach mod {o n} {
	eval "untabify ${mod}txt $g(fil$mod)"
	eval ".f.$mod.text insert 1.0 $${mod}txt"
	set tgt [expr [lindex [split [.f.$mod.text index end] .] 0] - 1]
	for {set i 1} {$i <= $tgt} {incr i} {
	    .f.$mod.text insert $i.0 [format "%-6d " $i]
	}
    }

    # Diff the two files and store the summary lines into 'diff'.

    set g(diff) [exec sh -c "diff $opts(diffopt) $g(filo) $g(filn) |
                             egrep -v '^(<|>|\-)' ; exit 0"]


    # Mark up the two text widgets and go to the first diff (if there
    # is one).

    if {[mark-diffs]} {
	set g(pos) 1
        move 1 0
	buttons normal
    } else {
	buttons disabled
    }

    # Prevent tampering in the text widgets.

    foreach mod {o n} {
	.f.$mod.text configure -state disabled
    }
}

###############################################################################
# Set the X cursor to "watch" for a window and all of its descendants.
###############################################################################

proc set-cursor {w} {
    global current

    if [string compare $w "."] {
	set current($w) [lindex [$w configure -cursor] 4]
	$w configure -cursor watch
    }
    foreach child [winfo children $w] {
	set-cursor $child
    }
}

###############################################################################
# Restore the X cursor for a window and all of its descendants.
###############################################################################

proc restore-cursor {w} {
    global current

    if [string compare $w "."] {
	catch {$w configure -cursor $current($w)}
    }
    foreach child [winfo children $w] {
	restore-cursor $child
    }
}

###############################################################################
# Flash the "rediff" button and then kick off a rediff.
###############################################################################

proc do-diff {} {
    set cur [lindex [. configure -cursor] 4]
    set-cursor .
    .b.rediff flash
    .b.rediff configure -state active
    rediff
    .b.rediff configure -state normal
    restore-cursor .
}

###############################################################################
# Throw up a help window.  Note: Couldn't get .help.f.text to do the
# equivalent of an ipadx without resorting to another level of frames...
# What gives?
###############################################################################

proc do-help {} {

    catch {destroy .help}
    toplevel .help
    wm title .help "TkDiff Help"

    pack [frame .help.f -background black] \
	    -expand y -fill both
    pack [scrollbar .help.f.scr -command {.help.f.f.text yview}] \
            -side left -fill y -padx 1
    pack [frame .help.f.f -background white] \
	    -expand y -fill both
    pack [text .help.f.f.text -wrap word -setgrid true \
	    -width 55 -yscroll {.help.f.scr set} \
            -background white -foreground black] \
            -side left -expand y -fill both -padx 5
    pack [button .help.done -text done -command {destroy .help}] \
            -side bottom -fill x
    put-text .help.f.f.text {

<hdr>TkDiff</hdr>

  This tool is intended to be a graphical front-end to the standard Unix <itl>diff</itl> utility.

<hdr>Startup</hdr>

  The proper way to start <itl>TkDiff</itl> is:

<cmp>
    tkdiff file1 file2 &
</cmp>

  One or the other (or both) of the arguments must be the name of a plain old text file.  Symbolic links (and other such magic) are acceptable, but at least one or the other (or both) of the filename arguments must point to a real file rather than to a directory.

<hdr>Layout</hdr>

  The left-most text widget (or window) displays the contents of <cmp>file1</cmp>; the right-most widget (window) displays the contents of <cmp>file2</cmp>.  Line numbers are automatically prepended to the lines in both widgets.

  All difference regions (DRs) are automatically highlighted in <bld>bold-face</bld> type.  The <itl>current</itl> DR (or CDR) is highlighted in <rev>reverse</rev> video, to set it apart from the other DRs.

  The CDR on the left matches the one on the right.  The CDR can be moved by means of the <btn>.next.</btn> and <btn>.prev.</btn> buttons, as well as by the menu under the <cmp>curr (<btn>.X.</btn> of Y)</cmp> button on the bottom of the screen.

<hdr>Operations</hdr>

  <btn>.quit.</btn>:  Terminates <itl>TkDiff</itl>.

  <btn>.help.</btn>:  Generates this information.

  <btn>.swap.</btn>:  Swaps the filenames and does a rediff.

  <btn>.rediff.</btn>:  Recomputes the differences between the two files whose names appear at the top of the <itl>TkDiff</itl> window.

  At the heart of <itl>TkDiff</itl> is the standard Unix <itl>diff</itl>.  The <cmp>opts</cmp> entry widget allows you to set the options that are put on <itl>diff</itl>'s command line -- any option listed in the man page ought to work.  The option that comes up by default is <cmp>-w</cmp>, unless otherwise specified in <cmp>~/.tkdiffrc</cmp> (see below).

  The label next to the <cmp>curr (<btn>.X.</btn> of Y)</cmp> area shows the <itl>diff</itl> mnemonic for the CDR.  The <cmp>curr (<btn>.X.</btn> of Y)</cmp> button itself allows you to select a DR to become the CDR.  This allows you to jump to any DR without having to traverse the intervening list one step at a time.

  <btn>.next.</btn>:  Takes you to the "next" DR.

  <btn>.prev.</btn>:  Takes you to the "previous" DR.

<hdr>Changing Filenames</hdr>

  The filenames next to the <cmp>change filename</cmp> labels may be changed at any time.  Note, however, that differences are only recomputed when the <btn>.rediff.</btn> button is pushed, or when <cmp>[Enter]</cmp> is pressed in either of the two associated filename entry widgets.  The <btn>.clr.</btn> button to the right of each <btn>.exp.</btn> provides a quick way to erase an entire filename.

  Each <btn>.exp.</btn> checkbutton toggles Emacs-style environment variable expansion and filename completion for the associated filename.  When <btn>.exp.</btn> is <cmp>off</cmp>, the filename is used as-is.  When <btn>.exp.</btn> is <cmp>on</cmp>, pushing either <cmp>[Tab]</cmp> or <cmp>[Enter]</cmp> causes the filename to be expanded and completed, as much as possible.

  Again, <cmp>[Enter]</cmp> causes a <btn>.rediff.</btn> to occur once the expansion and completion are finished.  <cmp>[Tab]</cmp> causes the expansion and completion to take place, but nothing more.

  The default value for both <btn>.exp.</btn> buttons is <cmp>on</cmp>, unless specified otherwise in <cmp>~/.tkdiffrc</cmp> (see below).

  Whenever a <btn>.rediff.</btn> occurs, for any reason, the filenames at the very top of the display are brought up to date.

<hdr>Scrolling</hdr>

  The left and right text widgets can be scrolled independently via the left-most and right-most scrollbars, respectively.  The middle scrollbar is a "meta" scrollbar, and scrolls both text widgets in a synchronized fashion.

<hdr>Saving Options</hdr>

  The <cmp>opts</cmp> information can be made permanent...  To do so, create (or edit) the file <cmp>.tkdiffrc</cmp> in your home directory and add a line that looks like:
<cmp>
    set opts(diffopt) {opts}
</cmp>
  Replace <cmp>opts</cmp> with whatever you'd like your default <itl>diff</itl> options to be.  Note, though, that if your options don't cause <itl>diff</itl> to produce the flavor of output that it does with <cmp>-w</cmp> (the default), <itl>TkDiff</itl> may fail to work.  In particular, if <itl>diff</itl> no longer spits out lines of the form:
<cmp>
    10,12d22
</cmp>
(for example), you may need to choose different options.  The default value of <cmp>-w</cmp> causes <itl>diff</itl> to ignore differences in whitespace.

  The other six options that you may set in your configuration file are:
<cmp>
    opts(currtag)
    opts(difftag)
    opts(textopt)
    opts(expfilo)
    opts(expfiln)
    opts(cmplimt)
</cmp>
  The first three affect the "look" of the CDRs, regular DRs and the two text widgets, respectively.  The next two turn filename expansion on or off.  The last one sets the upper limit on the number of possible completions a partial filename may have before the filename completion algorithm stops attempting to do a maximal completion.  The bigger this number, the slower the completion algorithm will be in large directories with many similar filenames.

  The default settings (which take effect in the absence of a configuration file, or in the presence of a configuration file that does not override them) are:
<cmp>
    set opts(currtag) {-background black \ 
                       -foreground white}
    set opts(difftag) {-background white \ 
		       -foreground black \ 
                       -font 6x13bold}
    set opts(textopt) {-background white \ 
		       -foreground black \ 
		       -font 6x13}
    set opts(expfilo) {1}
    set opts(expfiln) {1}
    set opts(cmplimt) {100}
</cmp>
  The first three tell <itl>TkDiff</itl> to make the CDRs show up in white on a black background, DRs to show up in black on a white background in a bold 6x13 font, and the text widgets to show up in black on a white background in a regular 6x13 font, respectively.  The next two tell <itl>TkDiff</itl> to expand both filenames.  The last one tells <itl>TkDiff</itl> to stop trying to do a maximal match on a non-unique filename fragment if the number of possible completions is more than 100.  You may modify these to suit your needs and tastes.

<hdr>Credits</hdr>

  Thanks go to Wayne Throop for beta testing and for giving valuable suggestions along the way.  Wayne also came up with the synchronized scrolling mechanism... Additional credit goes to John Heidemann, author of <itl>Klondike</itl> (a great Tk-based Solitaire game).  I shamelessly stole John's window tags routines out of <itl>Klondike</itl> and used them here.

<hdr>Comments</hdr>

  Questions and comments should be sent to John Klassa at <itl>klassa@aur.alcatel.com</itl>.

<hdr>Disclaimer</hdr>

  <bld>THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL JOHN KLASSA OR ALCATEL NETWORK SYSTEMS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</bld>

    }

    .help.f.f.text configure -state disabled
}

###############################################################################
# Get things going...
###############################################################################

if {$count >= 2} {
#   wm iconify .
    toplevel .message
    wm title .message "TkDiff Notice"
    pack [label .message.msg -foreground black -background white \
	    -width 30 -text "Computing differences..."] \
	    -side left -fill x -expand yes
    update
    do-diff
    destroy .message
#   wm deiconify .
} else {
    wipe
}

######################################################################
#
# text formatting routines derived from Klondike
# Reproduced here with permission from their author.
#
# Copyright (C) 1993,1994 by John Heidemann <johnh@ficus.cs.ucla.edu>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of John Heidemann may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY JOHN HEIDEMANN ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL JOHN HEIDEMANN BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
######################################################################

proc put-text {tw txt} {

    $tw configure -font -*-Times-Medium-R-Normal-*-140-*

    $tw tag configure bld -font -*-Times-Bold-R-Normal-*-140-*
    $tw tag configure cmp -font -*-Courier-Bold-R-Normal-*-120-*
    $tw tag configure hdr -font -*-Times-Bold-R-Normal-*-180-* -underline 1
    $tw tag configure itl -font -*-Times-Medium-I-Normal-*-140-*
    $tw tag configure rev -foreground white -background black

    $tw tag configure btn \
	    -font -*-Courier-Medium-R-Normal-*-120-* \
	    -foreground black -background white \
	    -relief groove -borderwidth 2
	    
    $tw mark set insert 0.0

    set t $txt

    while {[regexp -indices {<([^@>]*)>} $t match inds] == 1} {

	set start [lindex $inds 0]
	set end [lindex $inds 1]
	set keyword [string range $t $start $end]

	set oldend [$tw index end]

	$tw insert end [string range $t 0 [expr $start - 2]]

	purge-all-tags $tw $oldend insert

	if {[string range $keyword 0 0] == "/"} {
	    set keyword [string trimleft $keyword "/"]
	    if {[info exists tags($keyword)] == 0} {
		error "end tag $keyword without beginning"
	    }
	    $tw tag add $keyword $tags($keyword) insert
	    unset tags($keyword)
	} else {
	    if {[info exists tags($keyword)] == 1} {
		error "nesting of begin tag $keyword"
	    }
	    set tags($keyword) [$tw index insert]
	}
	
	set t [string range $t [expr $end + 2] end]
    }

    set oldend [$tw index end]
    $tw insert end $t
    purge-all-tags $tw $oldend insert
}

proc purge-all-tags {w start end} {
    foreach tag [$w tag names $start] {
	$w tag remove $tag $start $end
    }
}

