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)