newer mailmsg.py

Dave Long dl@silcom.com
Thu, 08 Mar 2001 15:08:13 -0800


Good points.  I've taken out the string methods,
since there's nothing in the program that calls
for excessive novelty.  (I suppose pygtk does.
Hook up string insertion and deletion properly,
and it should be easy to replace the gui)

There are now some (cryptic?) comments as well.

It's still by no means robust.

-Dave

#!/usr/bin/env python
# Dave Long, 2001-03-04 - 2001-03-08

# 2001-03-08:	some comments; consistency cleanup
#		removed python 1.6 string operations
# 2001-03-05:	~r kludges
#		body text editing
# 2001-03-04:	toggling outline display

###########################################################################
##	K S M U A U A
##
##	A user-agent for Kragen Sitaker's MUA -- reads emacs
##      outline-mode files and presents them in a GTK Text.
##
###########################################################################
## the first half of this program is a collection of functions on
## tuples and lists of tuples, where tups represent outline nodes
## as follows: (shown, depth, head, cc,cl, body, i)
##	shown	- are contents visible?
##	depth	- nesting depth
##	head	- header line
##	cc	- count of characters in body
##	cl	- count of lines in body
##	body	- node text (list of strings)
##	i	- this tup's index in the global nodelist
##		  (serves as primary key)
##
## the second half imperatively handles the current state,
## and interfaces with the display, filesystem, etc.
###########################################################################

from gtk import *
import sys
import string

###########################################################################
# recursion over an outline
###########################################################################
# 
# Similar to a reduce(), but:
#     - we default to recurring only over the subtree
#	determined by our initial node.  (fence)
#     - we don't fold over any subtrees of nodes which
#	are not shown.  (hidden)  256 ~= oo, here.
#
###########################################################################

def recursor(nodes, ctx, func, fence = None, hidden = 256):
	for tup in nodes:
		(shown, d, _, _, _, _, _) = tup
		if fence == None:	fence = d   # start at first depth
		elif not d > fence:	break	    # continue while subnode
		if d < hidden:
	                ctx = apply(func, (ctx, tup))
	                if shown: hidden = 256	    # propagate visibility
	                else:     hidden = d + 1    # or lack thereof
	return ctx

###########################################################################
# accumulators

def measure_tup(ctx, tup):
	(shown, _, head, cc, cl, _, i) = tup
	(seek, found, sumc, suml) = ctx
	if found != -1:
		return ctx

	if shown:	(dc,dl) = (len(head) + cc, 1 + cl)
	else:		(dc,dl) = (len(head)     , 1     )

	if seek <= suml + dl:
		return (seek, i, sumc, suml)
	else:
		return (seek, -1, sumc + dc, suml + dl)
	
def hide_tup(ctx, tup):
	(shown, _, head, cc, _, _, _) = tup
	if shown:	ctx = ctx + len(head) + cc
	else:		ctx = ctx + len(head)
	return ctx

def show_tup(ctx, tup):
	(shown, _, head, _, _, body, _) = tup
	ctx.append((1, head))
	if shown:
		for line in body:
			ctx.append((0, line))
	return ctx

###########################################################################
# sweeteners

def find_node_by_height(y, nodes):
	(_, i, cc, cl) = recursor(nodes, (y, -1, 0, 0), measure_tup, -1)
	return (cc, cl, i)

def hide_lines(nodes, count, fence=None):
	return recursor(nodes, count, hide_tup, fence)

def show_lines(nodes, lines, fence=None):
	return recursor(nodes, lines, show_tup, fence)


###########################################################################
# new = f(old)

# # # kludge
def smudgekludge(tup):
	(s, d, head, cc, cl, b, i) = tup
	if head[-3:] == '~r\n':
		return ((-3, -1), \
			(s, d, head[:-3] + '\n', cc, cl, b, i))
	return ((0,0), tup)
# # # kludge
# # # kludge
def no_edit(tup):
	(_, _, head, _, _, body, _) = tup
	text = reduce(lambda x,y: x+y, body, '')
	return (string.find(head,'~m')==-1 or \
	        string.find(text,'~e')==-1)
# # # kludge

def bodyoff(tup):
	(_, _, head, _, _, _, _) = tup
	return len(head)

def inserter(tup, str, len, pos):
	if len == 0:
		return tup

	(s, d, h, cc, cl, body, i) = tup
	text = reduce(lambda x,y: x+y, body, '')
	lfs = string.count(str, '\n')
	new = text[:pos] + str + text[pos:]
	return (s, d, h, cc + len, cl + lfs, [new], i)


def deleter(tup, start, end):
	if start == end:
		return tup

	(s, d, h, cc, cl, body, i) = tup
	text = reduce(lambda x,y: x+y, body, '')
	lfs = string.count(text[start:end],'\n')
	new = text[:start] + text[end:]
	return (s, d, h, cc + start - end , cl - lfs, [new], i)

def toggler(tup):
	(shown, d, h, cc, cl, b, i) = tup
	return (not shown, d, h, cc, cl, b, i)

####################################################################
# some state
####################################################################
#
# node:		the nodelist (supra)
# editing:	None if not editing, otherwise (i, off).
#		If so, we track changes to the 'i'th tup
#		in the nodelist, and 'off' allows us to
#		map display offsets to body text offsets
#
####################################################################

node = []
editing = None 

def edit_prepare(y):
	global editing
	(off, loff, i) = find_node_by_height(y, node)
	if i == None:		editing = None
	elif y < loff + 1:	editing = None
# # # kludge
	elif no_edit(node[i]):	editing = None
# # # kludge
	else: 			editing = (i, off + bodyoff(node[i]))

def edit_insert(str, len, pos):
	if editing != None:
		(i, off) = editing
		node[i] = inserter(node[i], str, len, pos - off)

def edit_delete(start, end):
	if editing != None:
		(i, off) = editing
		node[i] = deleter(node[i], start - off, end - off)


####################################################################
# pygtk specific stuff
####################################################################
#
# We want to update display state per hide/show_lines, and we want
# to sync internal state with the display via edit_insert/delete
# notifications.  Most else is gravy.
#
####################################################################

def delete_count(win, count):
	win.forward_delete(count)

def insert_list(win, lines):
	style = win.get_style()
	for (rev,text) in lines:
		if rev:	win.insert(style.font, style.white, style.black, text)
		else:	win.insert_defaults(text)

def toggle_text(win,yl):
	(off, _, i) = find_node_by_height(yl, node)
	win.set_point(off)
	win.freeze()
	delete_count(win, hide_lines(node[i:], 0))
	node[i] = toggler(node[i])
	insert_list(win, show_lines(node[i:], []))
	win.thaw()

####################################################################

def button_press_signal(win, event):

	def map_y_to_line(win, y):
		font = win.get_style().font
		yabs = win.get_vadjustment().value + y
		return yabs / (font.ascent + font.descent)

	yl = map_y_to_line(win, event.y)

	if event.state & 4:
		mainquit()
	elif event.state & 1:
		toggle_text(win, yl)
	else:
		edit_prepare(yl)
		global editing
		if editing == None:
			win.set_editable(FALSE)
			toggle_text(win, yl)
			return
		win.set_editable(TRUE)
# # # kludge
		(i, off) = editing
		(x, y) = smudgekludge(node[i])
		(s, e) = x
		if e - s > 0:
			node[i] = y
			diff = e - s
			pos = win.get_point()
			win.set_point(s + off)
			win.freeze()
			win.forward_delete(diff)
			win.thaw()
			win.set_point(pos - diff)
			editing = (i, off - diff)
# # # kludge

def do_gui():
	win = GtkWindow()
	win.set_name("kragen-mua user agent")
	win.set_usize(500, screen_height() - 80)
	win.set_uposition(20, 20)
	win.set_title("ksmuaua")

	win.connect("destroy",		mainquit)
	win.connect("delete_event",	mainquit)

	text = GtkText()
	text.set_editable(FALSE)
	text.set_line_wrap(FALSE)
	win.add(text)
	text.show()

	text.freeze()
	insert_list(text, show_lines(node, [], -1))
	text.thaw()

	text.connect("button_press_event", button_press_signal);

	def insert_signal(win, text, len, _):
		edit_insert(text[0:len], len, win.get_point())
	text.connect("insert_text", insert_signal);

	def delete_signal(_, start, end):
		edit_delete(start, end)
	text.connect("delete_text", delete_signal);

	win.show()
	mainloop()

####################################################################
# input/output
####################################################################
#
# This code has state, knows the internal representation of
# a nodelist tup, and has fun at boundaries.  C'est la guerre.
#
####################################################################

def nodein(file):
	def depth(str):
		i = 0
		while str[i] == '*': i = i + 1
		return i

	global node
        node = []
        (text, cc, cl) = ([], 0, 0)
        (odepth, oline, i) = (1, '------\n', 0)

	for line in file.readlines():
                if line[0] != '*':
                        text.append(line)
                        (cc, cl) = (cc + len(line), cl + 1)
                else:
                        if len(oline) + cc > 0:
                                node.append((0,odepth, oline, cc, cl, text,i))
                        (text, cc, cl) = ([], 0, 0)
                        (odepth, oline, i) = (depth(line), line, i+1)

        node.append((0,odepth, oline, cc, cl, text, i)); i = i + 1
        node.append((0,0, '------\n', 0, 0, [], i));

def nodeout(file):
        for (_,_,head,_,_,text,i) in node[:-1]:
		if i > 0:
			file.write(head)
                for l in text:
			file.write(l)

####################################################################
# startup/shutdown...

def main(args):
	if len(args) < 3:
		print ("Usage: %s input output\n"
"%s reads an input file in kragen-mua format and displays\n"
"it as a collapsible outline, writing it to the output file on exit.\n"
"\tclick\t\ttoggle outline visibility\n"
"\tshift-click\ttoggle even in editable area\n"
"\tctl-click\tclose program\n" % (args[0],args[0]))
		return

	if args[1] == '-':	file = sys.stdin
	else:			file = open(args[1], 'r')
 	nodein(file)
	file.close()

	do_gui()

	if args[2] == '-':	file = sys.stdout
	else:			file = open(args[2], 'w')
	nodeout(file)
	file.close()

if __name__ == '__main__': main(sys.argv)