--- mnemosyne-blog-0.12.orig/setup.py
+++ mnemosyne-blog-0.12/setup.py
@@ -19,9 +19,9 @@
         'Programming Language :: Python',
         ],
     package_dir = {'': 'lib'},
-    packages = ['mnemosyne'],
-    scripts = ['mnemosyne'],
+    packages = ['mnemosyne_blog'],
+    scripts = ['mnemosyne-blog'],
     data_files=[
-        # ('share/man1', ['mnemosyne.1', 'etc...']),
+        # ('share/man1', ['mnemosyne-blog.1', 'etc...']),
         ],
     )
--- mnemosyne-blog-0.12.orig/mnemosyne-blog
+++ mnemosyne-blog-0.12/mnemosyne-blog
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+
+import sys
+import getopt
+
+from mnemosyne_blog import get_conf
+from mnemosyne_blog.muse import Muse
+
+if __name__ == '__main__':
+    shortopts = 'fh'
+    longopts = ['force', 'help']
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
+    except getopt.GetoptError, e:
+        print >>sys.stderr, 'error: %s' % e
+        sys.exit(1)
+
+    force=False
+    for opt, arg in opts:
+        if opt in ('--force', '-f'):
+            force = True
+        if opt in ('--help', '-h'):
+            print "usage: mnemosyne-blog [--force] [configfile]"
+            sys.exit(0)
+
+    try:
+        config = args[0]
+    except IndexError:
+        config = get_conf('config.py')
+
+    try:
+        muse = Muse(config, force)
+        muse.sing()
+    except RuntimeError, e:
+        print >>sys.stderr, e
+        sys.exit(1)
--- mnemosyne-blog-0.12.orig/lib/mnemosyne_blog/entry.py
+++ mnemosyne-blog-0.12/lib/mnemosyne_blog/entry.py
@@ -0,0 +1,182 @@
+import os
+import time
+import docutils.core
+import email, email.Message, email.Header, email.Utils
+
+from mnemosyne_blog import cook, clean
+
+class BaseEntry:
+    """Base class for all entries. Initialized with an open file object, so it
+    may be passed to maildir.Maildir as a factory class. Parses the file's
+    contents as a Message object, setting a date attribute from the parsed
+    date and an mtime attribute from the Maildir filename."""
+
+    def __init__(self, fp):
+        def fixdate(d):
+            # For some bizarre reason, parsedate doesn't set wday/yday/isdst.
+            return time.localtime(time.mktime(d))
+        def getstamp(fp):
+            # _ProxyFile is a disgusting kludge. I take no responsibility.
+            try: path = fp.name # 2.4 or earlier
+            except AttributeError: path = fp._file.name
+            stamp, id, host = os.path.split(path)[1].split('.', 2)
+            return int(stamp)
+
+        self.msg = email.message_from_file(fp, Message)
+        self.date = fixdate(email.Utils.parsedate(self.msg['Date']))
+        self.mtime = time.localtime(getstamp(fp))
+
+    def __cmp__(self, other):
+        if other:
+            return cmp(time.mktime(self.date), time.mktime(other.date))
+        else:
+            return 1
+
+    def get_content(self):
+        """Read in the message's body, strip any signature, and format using
+        reStructedText."""
+
+        s = self.msg.get_body()
+        parts = docutils.core.publish_parts(s, writer_name='html')
+        return parts['body']
+
+    byday = {}
+    def get_subject(self):
+        """Get the contents of the Subject: header and a cleaned, uniq'd
+        version of same."""
+
+        try:
+            subject = self.msg['Subject']
+            cleaned = clean(subject, 3)
+        except KeyError:
+            subject = ''
+            cleaned = 'entry'
+
+        # Grab the namespace for the day of this entry
+        day = self.byday.setdefault(self.date[0:3], UniqueDict())
+
+        # This is not quite right. I think maybe it should be cook's
+        # reponsibility to encode things.
+        slug = day.setdefault(hash(self.msg), cleaned)
+        return cook(subject, slug.encode('utf-8', 'replace'))
+
+    def get_id(self):
+        """Get the Message-ID and a globally unique tag: URL based on it, for
+        use in feeds."""
+
+        try:
+            id = self.msg['Message-Id'][1:-1]
+            local, host = id.split('@')
+            date = time.strftime('%Y-%m-%d', self.date)
+            return cook(id, 'tag:%s,%s:%s' % (host, date, local))
+        except KeyError:
+            return ''
+
+    def get_author(self):
+        """Get the real name portion of the From: address."""
+        author, addr = email.Utils.parseaddr(self.msg.get('From'))
+        return cook(author, clean(author))
+
+    def get_email(self):
+        """Get the author's email address and a trivially spam-protected
+        version of same."""
+        try:
+            author, addr = email.Utils.parseaddr(self.msg['From'])
+            cleaned = addr.replace('@', ' at ')
+            cleaned = cleaned.replace('.', ' dot ')
+            cleaned = cleaned.replace('-', ' dash ')
+            return cook(addr, cleaned)
+        except KeyError:
+            return ''
+
+    def get_tags(self):
+        """Get a list of tags from the comma-delimited X-Tags: header."""
+        try:
+            tags = [t.strip() for t in self.msg['X-Tags'].split(',')]
+            return [cook(t, clean(t)) for t in tags]
+        except KeyError:
+            return []
+
+    def get_year(self):
+        """Extract the year from the Date: header."""
+        return cook(self.date[0], time.strftime('%Y', self.date))
+
+    def get_month(self):
+        """Extract the month from the Date: header."""
+        return cook(self.date[1], time.strftime('%m', self.date))
+
+    def get_day(self):
+        """Extract the day of the month from the Date: header."""
+        return cook(self.date[2], time.strftime('%d', self.date))
+
+class Entry(BaseEntry):
+    """Actual entry class. To look up an attribute, will search the
+    user-provided mixin classes and then BaseEntry for methods of the
+    form get_*, caching the results (it is assumed that values are
+    referentially transparent)."""
+
+    def __init__(self, fp):
+        # might want to load this from disk, keyed on hash(self.msg)
+        self.cache = {}
+        for _class in self.__class__.__bases__:
+            try: _class.__init__(self, fp)
+            except AttributeError: pass
+
+    def __getattr__(self, attr):
+        try:
+            return self.cache[attr]
+        except KeyError:
+            for _class in self.__class__.__bases__:
+                try:
+                    method = getattr(_class, 'get_'+attr)
+                except AttributeError:
+                    continue
+                return self.cache.setdefault(attr, method(self))
+            else:
+                raise AttributeError("Entry has no attribute '%s'" % attr)
+
+class Message(email.Message.Message):
+    """Non-broken version of email's Message class. Returns unicode headers
+    when necessary and raises KeyError when appropriate."""
+
+    def __getitem__(self, item):
+        header = email.Message.Message.__getitem__(self, item)
+        if not header:
+            raise KeyError
+        def actually_decode(s, e):
+            try: return s.decode(e)
+            except: return s.decode('utf-8', 'replace')
+        parts = email.Header.decode_header(header)
+        parts = [actually_decode(s, encoding) for s, encoding in parts]
+        return ' '.join(parts)
+
+    def get_body(self):
+        """Returns the message payload with any signature stripped."""
+        body = self.get_payload(decode=True) or self.get_payload(decode=False)
+
+        if isinstance(body, list):
+            return ''.join([payload.get_body() for payload in body])
+        else:
+            return body[:body.rfind('-- \n')].decode('utf-8', 'replace')
+
+class UniqueDict(dict):
+    """A read-only dict which munges its values so that they are unique. If an
+    existing key has the value 'foo', attempting to set another key to 'foo'
+    will cause it to become 'foo-1', then 'foo-2', etc. These numberings are
+    stable as long as each key is assigned to in the same order; attempting to
+    set an existing key will cause a ValueError. """
+
+    def __getitem__(self, k):
+        k, i = dict.__getitem__(self, k)
+        if i: return '%s-%d' % (k, i)
+        else: return k
+
+    def __setitem__(self, k, v):
+        if k in self: raise ValueError
+        n = len([x for x, y in self.iteritems() if y[0] == v])
+        dict.__setitem__(self, k, (v, n))
+
+    # Yes, we must. Le sigh.
+    def setdefault(self, key, failobj=None):
+        if not self.has_key(key): self[key] = failobj
+        return self[key]
--- mnemosyne-blog-0.12.orig/lib/mnemosyne_blog/__init__.py
+++ mnemosyne-blog-0.12/lib/mnemosyne_blog/__init__.py
@@ -0,0 +1,52 @@
+"""Mnemosyne -- a static weblog generator."""
+
+import os
+
+__version__ = '0.12'
+__author__ = 'Decklin Foster'
+__email__ = 'decklin@red-bean.com'
+__url__ = 'http://www.red-bean.com/decklin/mnemosyne/'
+
+__all__ = ['muse', 'entry']
+
+def get_conf(s):
+    return os.path.expanduser('~/.mnemosyne-blog/%s' % s)
+
+def cook(obj, rep):
+    """Create an object exactly like obj, except its repr() is rep. This will
+    allow layouts to use the "cooked" rep (by convention, this is how we
+    format stuff for URLs etc.) without caring how or when or why it was
+    set."""
+
+    _class = type("Cooked", (type(obj),), {'__repr__': lambda self: rep})
+    return _class(obj)
+
+def clean(s, maxwords=None):
+    """Split the given string into words, lowercase and strip all
+    non-alphanumerics from them, and join them with '-'. If maxwords is given,
+    limit the returned string to that many words. If the string is None,
+    return None."""
+
+    try:
+        words = s.strip().lower().split()[:maxwords]
+        words = [filter(lambda c: c.isalnum(), w) for w in words]
+        return '-'.join(words) or '-'
+    except AttributeError:
+        return None
+
+def cheapiter(x):
+    """DWIM-style iterator which, if given a sequence, will iterate over that
+    sequence, unless it is a string type. For a string or any other atomic
+    type, create an iterator which will return the given value once and then
+    stop. Unless it's None. This is a horrible, horrible kludge."""
+
+    try:
+        if isinstance(x, basestring):
+            return iter((x,))
+        else:
+            return iter(x)
+    except TypeError:
+        if x != None:
+            return iter((x,))
+        else:
+            return iter(())
--- mnemosyne-blog-0.12.orig/lib/mnemosyne_blog/muse.py
+++ mnemosyne-blog-0.12/lib/mnemosyne_blog/muse.py
@@ -0,0 +1,155 @@
+import os
+import sys
+import mailbox
+import time
+import stat
+import shutil
+import kid
+import StringIO
+
+from entry import Entry
+from mnemosyne_blog import get_conf, cheapiter
+
+class Muse:
+    def __init__(self, config, force):
+        self.force = force
+        self.where = []
+
+        self.conf = {
+            'entry_dir': get_conf('entries'),
+            'layout_dir': get_conf('layout'),
+            'style_dir': get_conf('style'),
+            'output_dir': get_conf('htdocs'),
+            'ignore': ('.hg', '_darcs', '.git', 'MT', '.svn', 'CVS'),
+            'locals': {},
+            'mixins': [],
+            }
+
+        try:
+            exec file(config) in self.conf
+        except Exception, e:
+            raise RuntimeError("Error running config: %s" % e)
+
+        Entry.__bases__ = tuple(self.conf['mixins']) + Entry.__bases__
+
+        for d in ('entry_dir', 'layout_dir', 'style_dir', 'output_dir'):
+            if not os.path.exists(self.conf[d]):
+                raise RuntimeError("%s %s does not exist" % (d, self.conf[d]))
+
+        self.box = mailbox.Maildir(self.conf['entry_dir'], Entry)
+        self.entries = [e for e in self.box]
+        print 'Sorting %d entries...' % len(self.entries)
+        self.entries.sort()
+
+    def sing(self, entries=None, spath=None, dpath=None, what=None):
+        """From the contents of spath, build output in dpath, based on the
+        provided entries. For each entry in spath, will be called recursively
+        with a tuple what representing the source and dest file. For any
+        source files starting with __attr__ will recur several times based on
+        which entries match each value of that attribute. For regularly named
+        files, evaluate them as layout scripts if they are executable and
+        simply copy them if they are not."""
+
+        if not entries: entries = self.entries
+        if not spath: spath = self.conf['layout_dir']
+        if not dpath: dpath = self.conf['output_dir']
+
+        def stale(dpath, spath, entries=None):
+            """Test if the file named by dpath is nonexistent or older than
+            either the file named by spath or any entry in the given list of
+            entries. If --force has been turned on, always return True."""
+
+            if self.force or not os.path.exists(dpath):
+                return True
+            else:
+                dmtime = os.path.getmtime(dpath)
+                smtimes = [os.path.getmtime(spath)]
+                if entries: smtimes += [time.mktime(e.mtime) for e in entries]
+                return dmtime < max(smtimes)
+
+        if what:
+            source, dest = what
+            spath = os.path.join(spath, source)
+            dpath = os.path.join(dpath, dest)
+            if source not in self.conf['ignore']:
+                if os.path.isfile(spath):
+                    if os.stat(spath).st_mode & stat.S_IXUSR:
+                        if stale(dpath, spath, entries):
+                            self.sing_file(entries, spath, dpath)
+                    else:
+                        if stale(dpath, spath):
+                            shutil.copyfile(spath, dpath)
+                            print 'Copied %s' % dpath
+                elif os.path.isdir(spath):
+                    self.sing(entries, spath, dpath)
+        else:
+            if not os.path.isdir(dpath): os.makedirs(dpath)
+            for f in os.listdir(spath):
+                if f.startswith('__'):
+                    self.sing_instances(entries, spath, dpath, f)
+                else:
+                    self.where.append(f)
+                    self.sing(entries, spath, dpath, (f, f))
+                    self.where.pop()
+
+    def sing_instances(self, entries, spath, dpath, what):
+        """Given a source and dest file in the tuple what, where the source
+        starts with __attr__, group the provided entries by the values of that
+        attribute over all the provided entries. For an entry e and attribute
+        attr, e.attr may be an atomic value or a sequence of values. For each
+        value so encountered, evaluate the source file given all entries in
+        entries that match that value."""
+
+        subst = what[:what.rindex('__')+2]
+
+        inst = {}
+        for e in entries:
+            mv = getattr(e, subst[2:-2])
+            for m in cheapiter(mv):
+                inst.setdefault(repr(m), []).append(e)
+
+        for k, entries in inst.iteritems():
+            self.where.append(k)
+            self.sing(entries, spath, dpath, (what, what.replace(subst, k)))
+            self.where.pop()
+
+    def template(self, name, kwargs):
+        """Open a Kid template in the configuration's style directory, and
+        initialize it with any given keyword arguments."""
+
+        path = os.path.join(self.conf['style_dir'], '%s.kid' % name)
+        return KidTemplate(path, kwargs)
+
+    def sing_file(self, entries, spath, dpath):
+        """Given an source layout and and dest file, exec it with the locals
+        from config plus muse (ourself) and entries (the ones we're actually
+        looking at)."""
+
+        locals = self.conf['locals'].copy()
+        locals['muse'] = self
+        locals['entries'] = entries
+
+        stdout = sys.stdout
+        sys.stdout = StringIO.StringIO()
+
+        try:
+            exec file(spath) in globals(), locals
+        except Exception, e:
+            print >>sys.stderr, "Error running layout %s: %s" % (spath, e)
+        else:
+            print >>stdout, 'Wrote %s' % dpath
+            try:
+                file(dpath, 'w').write(sys.stdout.getvalue())
+            except Exception, e:
+                print >>sys.stderr, "Error writing file: %s" % e
+
+        sys.stdout = stdout
+
+class KidTemplate:
+    def __init__(self, filename, kwargs):
+        module = kid.load_template(filename)
+        self.template = module.Template(assume_encoding='utf-8', **kwargs)
+    def __str__(self):
+        return self.template.serialize(output='xhtml-strict')
+    def __getattr__(self, attr):
+        return getattr(self.template, attr)
