Initial revision
diff --git a/Demo/stdwin/wdiff.py b/Demo/stdwin/wdiff.py
new file mode 100755
index 0000000..2c4941f
--- /dev/null
+++ b/Demo/stdwin/wdiff.py
@@ -0,0 +1,483 @@
+#! /usr/local/python
+
+# A window-oriented recursive diff utility.
+# NB: This uses undocumented window classing modules.
+
+# TO DO:
+#	- faster update after moving/copying one file
+#	- diff flags (-b, etc.) should be global or maintained per window
+#	- use a few fixed windows instead of creating new ones all the time
+#	- ways to specify patterns to skip
+#	  (best by pointing at a file and clicking a special menu entry!)
+#	- add rcsdiff menu commands
+#	- add a way to view status of selected files without opening them
+#	- add a way to diff two files with different names
+#	- add a way to rename files
+#	- keep backups of overwritten/deleted files
+#	- a way to mark specified files as uninteresting for dircmp
+
+import sys
+import os
+import rand
+import commands
+import dircache
+import statcache
+import cmp
+import cmpcache
+import stdwin
+import gwin
+import textwin
+import filewin
+import tablewin
+import anywin
+
+mkarg = commands.mkarg
+mk2arg = commands.mk2arg
+
+# List of names to ignore in dircmp()
+#
+skiplist = ['RCS', '.Amake', 'tags', '.', '..']
+
+# Function to determine whether a name should be ignored in dircmp().
+#
+def skipthis(file):
+	return file[-1:] == '~' or file in skiplist
+
+
+def anydiff(a, b, flags): # Display differences between any two objects
+	print 'diff', flags, a, b
+	if os.path.isdir(a) and os.path.isdir(b):
+		w = dirdiff(a, b, flags)
+	else:
+		w = filediff(a, b, flags)
+	addstatmenu(w, [a, b])
+	w.original_close = w.close
+	w.close = close_dirwin
+	return w
+
+def close_dirwin(w):
+	close_subwindows(w, (), 0)
+	w.original_close(w)
+
+def filediff(a, b, flags): # Display differences between two text files
+	diffcmd = 'diff'
+	if flags: diffcmd = diffcmd + mkarg(flags)
+	diffcmd = diffcmd + mkarg(a) + mkarg(b)
+	difftext = commands.getoutput(diffcmd)
+	return textwin.open_readonly(mktitle(a, b), difftext)
+
+def dirdiff(a, b, flags): # Display differences between two directories
+	data = diffdata(a, b, flags)
+	w = tablewin.open(mktitle(a, b), data)
+	w.flags = flags
+	w.a = a
+	w.b = b
+	addviewmenu(w)
+	addactionmenu(w)
+	return w
+
+def diffdata(a, b, flags): # Compute directory differences.
+	#
+	a_only = [('A only:', header_action), ('', header_action)]
+	b_only = [('B only:', header_action), ('', header_action)]
+	ab_diff = [('A <> B:', header_action), ('', header_action)]
+	ab_same = [('A == B:', header_action), ('', header_action)]
+	data = [a_only, b_only, ab_diff, ab_same]
+	#
+	a_list = dircache.listdir(a)[:]
+	b_list = dircache.listdir(b)[:]
+	dircache.annotate(a, a_list)
+	dircache.annotate(b, b_list)
+	a_list.sort()
+	b_list.sort()
+	#
+	for x in a_list:
+		if x in ['./', '../']:
+			pass
+		elif x not in b_list:
+			a_only.append(x, a_only_action)
+		else:
+			ax = os.path.join(a, x)
+			bx = os.path.join(b, x)
+			if os.path.isdir(ax) and os.path.isdir(bx):
+				if flags == '-r':
+					same = dircmp(ax, bx)
+				else:
+					same = 0
+			else:
+				try:
+					same = cmp.cmp(ax, bx)
+				except (RuntimeError, os.error):
+					same = 0
+			if same:
+				ab_same.append(x, ab_same_action)
+			else:
+				ab_diff.append(x, ab_diff_action)
+	#
+	for x in b_list:
+		if x in ['./', '../']:
+			pass
+		elif x not in a_list:
+			b_only.append(x, b_only_action)
+	#
+	return data
+
+# Re-read the directory.
+# Attempt to find the selected item back.
+
+def update(w):
+	setbusy(w)
+	icol, irow = w.selection
+	if 0 <= icol < len(w.data) and 2 <= irow < len(w.data[icol]):
+		selname = w.data[icol][irow][0]
+	else:
+		selname = ''
+	statcache.forget_dir(w.a)
+	statcache.forget_dir(w.b)
+	tablewin.select(w, (-1, -1))
+	tablewin.update(w, diffdata(w.a, w.b, w.flags))
+	if selname:
+		for icol in range(len(w.data)):
+			for irow in range(2, len(w.data[icol])):
+				if w.data[icol][irow][0] == selname:
+					tablewin.select(w, (icol, irow))
+					break
+
+# Action functions for table items in directory diff windows
+
+def header_action(w, string, (icol, irow), (pos, clicks, button, mask)):
+	tablewin.select(w, (-1, -1))
+
+def a_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
+	tablewin.select(w, (icol, irow))
+	if clicks == 2:
+		w2 = anyopen(os.path.join(w.a, string))
+		if w2:
+			w2.parent = w
+
+def b_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
+	tablewin.select(w, (icol, irow))
+	if clicks == 2:
+		w2 = anyopen(os.path.join(w.b, string))
+		if w2:
+			w2.parent = w
+
+def ab_diff_action(w, string, (icol, irow), (pos, clicks, button, mask)):
+	tablewin.select(w, (icol, irow))
+	if clicks == 2:
+		w2 = anydiff(os.path.join(w.a, string), os.path.join(w.b, string),'')
+		w2.parent = w
+
+def ab_same_action(w, string, sel, detail):
+	ax = os.path.join(w.a, string)
+	if os.path.isdir(ax):
+		ab_diff_action(w, string, sel, detail)
+	else:
+		a_only_action(w, string, sel, detail)
+
+def anyopen(name): # Open any kind of document, ignore errors
+	try:
+		w = anywin.open(name)
+	except (RuntimeError, os.error):
+		stdwin.message('Can\'t open ' + name)
+		return 0
+	addstatmenu(w, [name])
+	return w
+
+def dircmp(a, b): # Compare whether two directories are the same
+	# To make this as fast as possible, it uses the statcache
+	print '  dircmp', a, b
+	a_list = dircache.listdir(a)
+	b_list = dircache.listdir(b)
+	for x in a_list:
+		if skipthis(x):
+			pass
+		elif x not in b_list:
+			return 0
+		else:
+			ax = os.path.join(a, x)
+			bx = os.path.join(b, x)
+			if statcache.isdir(ax) and statcache.isdir(bx):
+				if not dircmp(ax, bx): return 0
+			else:
+				try:
+					if not cmpcache.cmp(ax, bx): return 0
+				except (RuntimeError, os.error):
+					return 0
+	for x in b_list:
+		if skipthis(x):
+			pass
+		elif x not in a_list:
+			return 0
+	return 1
+
+
+# View menu (for dir diff windows only)
+
+def addviewmenu(w):
+	w.viewmenu = m = w.menucreate('View')
+	m.action = []
+	add(m, 'diff -r A B', diffr_ab)
+	add(m, 'diff A B', diff_ab)
+	add(m, 'diff -b A B', diffb_ab)
+	add(m, 'diff -c A B', diffc_ab)
+	add(m, 'gdiff A B', gdiff_ab)
+	add(m, ('Open A   ', 'A'), open_a)
+	add(m, ('Open B   ', 'B'), open_b)
+	add(m, 'Rescan', rescan)
+	add(m, 'Rescan -r', rescan_r)
+
+# Action menu (for dir diff windows only)
+
+def addactionmenu(w):
+	w.actionmenu = m = w.menucreate('Action')
+	m.action = []
+	add(m, 'cp A B', cp_ab)
+	add(m, 'rm B', rm_b)
+	add(m, '', nop)
+	add(m, 'cp B A', cp_ba)
+	add(m, 'rm A', rm_a)
+
+# Main menu (global):
+
+def mainmenu():
+	m = stdwin.menucreate('Wdiff')
+	m.action = []
+	add(m, ('Quit wdiff', 'Q'), quit_wdiff)
+	add(m, 'Close subwindows', close_subwindows)
+	return m
+
+def add(m, text, action):
+	m.additem(text)
+	m.action.append(action)
+
+def quit_wdiff(w, m, item):
+	if askyesno('Really quit wdiff altogether?', 1):
+		sys.exit(0)
+
+def close_subwindows(w, m, item):
+	while 1:
+		for w2 in gwin.windows:
+			if w2.parent == w:
+				close_subwindows(w2, m, item)
+				w2.close(w2)
+				break # inner loop, continue outer loop
+		else:
+			break # outer loop
+
+def diffr_ab(w, m, item):
+	dodiff(w, '-r')
+
+def diff_ab(w, m, item):
+	dodiff(w, '')
+
+def diffb_ab(w, m, item):
+	dodiff(w, '-b')
+
+def diffc_ab(w, m, item):
+	dodiff(w, '-c')
+
+def gdiff_ab(w, m, item): # Call SGI's gdiff utility
+	x = getselection(w)
+	if x:
+		a, b = os.path.join(w.a, x), os.path.join(w.b, x)
+		if os.path.isdir(a) or os.path.isdir(b):
+			stdwin.fleep() # This is for files only
+		else:
+			diffcmd = 'gdiff'
+			diffcmd = diffcmd + mkarg(a) + mkarg(b) + ' &'
+			print diffcmd
+			sts = os.system(diffcmd)
+			if sts: print 'Exit status', sts
+
+def dodiff(w, flags):
+	x = getselection(w)
+	if x:
+		w2 = anydiff(os.path.join(w.a, x), os.path.join(w.b, x), flags)
+		w2.parent = w
+
+def open_a(w, m, item):
+	x = getselection(w)
+	if x:
+		w2 = anyopen(os.path.join(w.a, x))
+		if w2:
+			w2.parent = w
+
+def open_b(w, m, item):
+	x = getselection(w)
+	if x:
+		w2 = anyopen(os.path.join(w.b, x))
+		if w2:
+			w2.parent = w
+
+def rescan(w, m, item):
+	w.flags = ''
+	update(w)
+
+def rescan_r(w, m, item):
+	w.flags = '-r'
+	update(w)
+
+def rm_a(w, m, item):
+	x = getselection(w)
+	if x:
+		if x[-1:] == '/': x = x[:-1]
+		x = os.path.join(w.a, x)
+		if os.path.isdir(x):
+			if askyesno('Recursively remove A directory ' + x, 1):
+				runcmd('rm -rf' + mkarg(x))
+		else:
+			runcmd('rm -f' + mkarg(x))
+		update(w)
+
+def rm_b(w, m, item):
+	x = getselection(w)
+	if x:
+		if x[-1:] == '/': x = x[:-1]
+		x = os.path.join(w.b, x)
+		if os.path.isdir(x):
+			if askyesno('Recursively remove B directory ' + x, 1):
+				runcmd('rm -rf' + mkarg(x))
+		else:
+			runcmd('rm -f' + mkarg(x))
+		update(w)
+
+def cp_ab(w, m, item):
+	x = getselection(w)
+	if x:
+		if x[-1:] == '/': x = x[:-1]
+		ax = os.path.join(w.a, x)
+		bx = os.path.join(w.b, x)
+		if os.path.isdir(ax):
+			if os.path.exists(bx):
+				m = 'Can\'t copy directory to existing target'
+				stdwin.message(m)
+				return
+			runcmd('cp -r' + mkarg(ax) + mkarg(w.b))
+		else:
+			runcmd('cp' + mkarg(ax) + mk2arg(w.b, x))
+		update(w)
+
+def cp_ba(w, m, item):
+	x = getselection(w)
+	if x:
+		if x[-1:] == '/': x = x[:-1]
+		ax = os.path.join(w.a, x)
+		bx = os.path.join(w.b, x)
+		if os.path.isdir(bx):
+			if os.path.exists(ax):
+				m = 'Can\'t copy directory to existing target'
+				stdwin.message(m)
+				return
+			runcmd('cp -r' + mkarg(bx) + mkarg(w.a))
+		else:
+			runcmd('cp' + mk2arg(w.b, x) + mkarg(ax))
+		update(w)
+
+def nop(args):
+	pass
+
+def getselection(w):
+	icol, irow = w.selection
+	if 0 <= icol < len(w.data):
+		if 0 <= irow < len(w.data[icol]):
+			return w.data[icol][irow][0]
+	stdwin.message('no selection')
+	return ''
+
+def runcmd(cmd):
+	print cmd
+	sts, output = commands.getstatusoutput(cmd)
+	if sts or output:
+		if not output:
+			output = 'Exit status ' + `sts`
+		stdwin.message(output)
+
+
+# Status menu (for all kinds of windows)
+
+def addstatmenu(w, files):
+	w.statmenu = m = w.menucreate('Stat')
+	m.files = files
+	m.action = []
+	for file in files:
+		m.additem(commands.getstatus(file))
+		m.action.append(stataction)
+
+def stataction(w, m, item): # Menu item action for stat menu
+	file = m.files[item]
+	try:
+		m.setitem(item, commands.getstatus(file))
+	except os.error:
+		stdwin.message('Can\'t get status for ' + file)
+
+
+# Compute a suitable window title from two paths
+
+def mktitle(a, b):
+	if a == b: return a
+	i = 1
+	while a[-i:] == b[-i:]: i = i+1
+	i = i-1
+	if not i:
+		return a + '  ' + b
+	else:
+		return '{' + a[:-i] + ',' + b[:-i] + '}' + a[-i:]
+
+
+# Ask a confirmation question
+
+def askyesno(prompt, default):
+	try:
+		return stdwin.askync(prompt, default)
+	except KeyboardInterrupt:
+		return 0
+
+
+# Display a message "busy" in a window, and mark it for updating
+
+def setbusy(w):
+	left, top = w.getorigin()
+	width, height = w.getwinsize()
+	right, bottom = left + width, top + height
+	d = w.begindrawing()
+	d.erase((0, 0), (10000, 10000))
+	text = 'Busy...'
+	textwidth = d.textwidth(text)
+	textheight = d.lineheight()
+	h, v = left + (width-textwidth)/2, top + (height-textheight)/2
+	d.text((h, v), text)
+	del d
+	w.change((0, 0), (10000, 10000))
+
+
+# Main function
+
+def main():
+	print 'wdiff: warning: this program does NOT make backups'
+	argv = sys.argv
+	flags = ''
+	if len(argv) >= 2 and argv[1][:1] == '-':
+		flags = argv[1]
+		del argv[1]
+	m = mainmenu() # Create menu earlier than windows
+	if len(argv) == 2: # 1 argument
+		w = anyopen(argv[1])
+		if not w: return
+	elif len(argv) == 3: # 2 arguments
+		w = anydiff(argv[1], argv[2], flags)
+		w.parent = ()
+	else:
+		sys.stdout = sys.stderr
+		print 'usage:', argv[0], '[diff-flags] dir-1 [dir-2]'
+		sys.exit(2)
+	del w # It's preserved in gwin.windows
+	while 1:
+		try:
+			gwin.mainloop()
+			break
+		except KeyboardInterrupt:
+			pass	# Just continue...
+
+# Start the main function (this is a script)
+main()