#!/usr/bin/python
# Copyright 2010-2014 Lars Wirzenius
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import cliapp
import optparse
import os
import re
import shutil
import string
import subprocess
import sys
import tempfile
import time
import traceback
__version__ = '0.2'
template = '''\
[[!meta title="%(title)s"]]
[[!tag ]]
[[!meta date="%(date)s"]]
'''
class DraftsDirectory(object):
def __init__(self, dirname):
self.dirname = dirname
def create_if_missing(self):
if not os.path.exists(self.dirname):
os.mkdir(self.dirname)
def get_draft_pathname(self, draft_number):
return os.path.join(self.dirname, '%s.mdwn' % draft_number)
def get_draft_attachments_dirname(self, draft_number):
return os.path.join(self.dirname, '%s' % draft_number)
def create_draft(self, content):
draft_number = self._pick_available_draft_number()
pathname = self.get_draft_pathname(draft_number)
with open(pathname, 'w') as f:
f.write(content)
return draft_number
def _pick_available_draft_number(self):
for i in range(1000):
pathname = self.get_draft_pathname(i)
if not os.path.exists(pathname):
return i
else:
raise cliapp.AppException('ERROR: too many existing drafts')
def get_drafts(self):
for basename in os.listdir(self.dirname):
# .# is what Emacs autosave files start with.
if basename.endswith('.mdwn') and not basename.startswith('.#'):
yield basename[:-len('.mdwn')], os.path.join(self.dirname, basename)
def remove_draft(self, draft_number):
filename = self.get_draft_pathname(draft_number)
os.remove(filename)
dirname = self.get_draft_attachments_dirname(draft_number)
if os.path.exists(dirname):
shutil.rmtree(dirname)
class Command(object):
def __init__(self, app):
self._app = app
def run(self, args):
raise NotImplementedError()
class NewCommand(Command):
def run(self, args):
if not args:
raise cliapp.AppException('Usage: journal-note new TITLE')
self._app.settings.require('source')
self._app.settings.require('layout')
values = {
'title': args[0],
'date': time.strftime('%Y-%m-%d %H:%M')
}
drafts_dir = DraftsDirectory(self._app.drafts_dir())
drafts_dir.create_if_missing()
draft_number = drafts_dir.create_draft(template % values)
self._app.edit_file(drafts_dir.get_draft_pathname(draft_number))
class ListCommand(Command):
def run(self, args):
drafts_dir = DraftsDirectory(self._app.drafts_dir())
for draft_id, filename in drafts_dir.get_drafts():
print draft_id, self._app.get_draft_title(filename) or ""
class EditCommand(Command):
def run(self, args):
if len(args) > 1:
raise cliapp.AppException('Must be given at most one draft ID')
drafts_dir = DraftsDirectory(self._app.drafts_dir())
pathmame = self._app.choose_draft(drafts_dir, args)
self._app.edit_file(pathname)
class AttachCommand(Command):
def run(self, args):
if len(args) < 2:
raise cliapp.AppException('Usage: journal-note attach ID file...')
drafts_dir = DraftsDirectory(self._app.draft_dir())
dirname = drafts_dir.get_draft_attachments_dirname(args[0])
if not os.path.exists(dirname):
os.mkdir(dirname)
for filename in args[1:]:
shutil.copy(filename, dirname)
class RemoveCommand(Command):
def run(self, args):
if not args:
raise cliapp.AppException('Usage: journal-note remove ID')
drafts_dir = DraftsDirectory(self._app.drafts_dir())
drafts_dir.remove_draft(args[0])
class FinishCommand(Command):
def run(self, args):
drafts_dir = DraftsDirectory(self._app.drafts_dir())
draft_mdwn = self._app.choose_draft(drafts_dir, args)
draft_attch, ext = os.path.splitext(draft_mdwn)
assert ext == '.mdwn'
pub_attch = os.path.join(
self.published_dir(), self.published_basename(draft_mdwn))
pub_mdwn = pub_attch + '.mdwn'
if os.path.exists(pub_mdwn):
raise cliapp.AppException('%s already exists' % pub_mdwn)
if not os.path.exists(self.published_dir()):
os.makedirs(self.published_dir())
os.rename(draft_mdwn, pub_mdwn)
if os.path.exists(draft_attch):
os.rename(draft_attch, pub_attch)
if self._app.settings['git']:
argv = ['git', 'add', pub_mdwn]
if os.path.exists(pub_attch):
argv.append(pub_attch)
cliapp.runcmd(argv, cwd=self._app.settings['source'])
cliapp.runcmd(
['git', 'commit', '-m', 'Publish log entry'],
cwd=self._app.settings['source'])
if self.settings['push']:
cliapp.runcmd(
['git', 'push', 'origin', 'HEAD'],
cwd=self._app.settings['source'])
def published_dir(self):
subdirs = {
'liw': 'notes',
'ct': 'log/%d' % time.localtime().tm_year,
'pkb': time.strftime('notes/%Y/%m/%d'),
}
subdir = subdirs[self._app.settings['layout']]
return os.path.join(self._app.settings['source'], subdir)
def published_basename(self, draft_mdwn):
if self._app.settings['layout'] in ('liw', 'ct'):
basename = time.strftime('%Y-%m-%d-%H:%M:%S')
elif self._app.settings['layout'] == 'pkb':
title = self._app.get_draft_title(draft_mdwn)
if not title:
raise Exception("%s has no title" % draft_mdwn)
basename = self._app.summarise_title(title)
else:
raise Exception(
'Setting --layout=%s is unknown' % self._app.settings['layout'])
return basename
class NewPersonCommand(Command):
def run(self, args):
if len(args) != 1:
raise cliapp.AppException(
'Need the name of a person (in Last, First form)')
def normalise(name):
s = name.lower()
s = ' '.join(s.split(','))
s = '.'.join(s.split())
return s
name = args[0]
basename = normalise(name)
pathname = os.path.join(
self._app.settings['source'], 'people', basename + '.mdwn')
if os.path.exists(pathname):
raise cliapp.AppException('File %s already exists' % pathname)
with open(pathname, 'w') as f:
f.write('''\
[[!meta title="%(name)s"]]
[[!inline archive=yes pages="link(.)"]]
''' %
{
'name': name,
'basename': basename,
})
class JournalTool(cliapp.Application):
cmd_synopsis = {
'attach': 'DRAFT-ID [FILE]...',
'edit': '[DRAFT-ID]',
'finish': '[DRAFT-ID]',
'list': '',
'new': 'TITLE',
'remove': 'DRAFT-ID',
}
def add_settings(self):
self.settings.string(
['source'],
'use journal source tree in DIR',
metavar='DIR')
self.settings.choice(
['layout'],
['ct', 'liw', 'pkb'],
'use journal layout (one of liw, ct, pkb)',
metavar='LAYOUT')
self.settings.boolean(
['git'],
'add entries to git automatically',
default=True)
self.settings.string(
['editor'],
'editor to launch for journal entries. Must include %s to '
'indicate where the filename goes',
default='sensible-editor %s')
self.settings.boolean(
['push'],
'push finished articles with git?')
self.settings.string(
['pretend-time'],
'pretend that the time is NOW (form: YYYY-MM-DD HH:MM:DD form)',
metavar='NOW')
def cmd_new(self, args):
'''Create a new journal entry draft.'''
NewCommand(self).run(args)
def drafts_dir(self):
return os.path.join(self.settings['source'], 'drafts')
def edit_file(self, pathname):
safe_pathname = cliapp.shell_quote(pathname)
cmdline = ['sh', '-c', self.settings['editor'] % pathname]
self.runcmd(cmdline, stdin=None, stdout=None, stderr=None)
def cmd_list(self, args):
'''List journal entry drafts.'''
ListCommand(self).run(args)
def get_draft_title(self, filename):
with open(filename) as f:
for line in f:
m = re.match(
'\[\[!meta title="(?P
.*)("\]\])$',
line)
if m:
title = m.group('title')
break
else:
title = None
return title
def cmd_edit(self, args):
'''Edit a draft journal entry.'''
EditCommand(self).run(args)
def choose_draft(self, drafts_dir, args):
if len(args) == 0:
drafts = list(drafts_dir.get_drafts())
if len(drafts) == 1:
draft_id, filename = drafts[0]
return filename
elif len(drafts) == 0:
raise cliapp.AppException('No drafts to choose from')
else:
raise cliapp.AppException('Cannot choose entry draft automatically')
elif len(args) == 1:
pathname = drafts_dir.get_draft_pathname(args[0])
if not os.path.exists(pathname):
raise cliapp.AppException('draft %s does not exist' % args[0])
return pathname
elif len(args) > 1:
raise cliapp.AppException('Must give at most one draft number')
def cmd_attach(self, args):
'''Attach files to a journal entry draft.'''
AttachCommand(self).run(args)
def cmd_remove(self, args):
'''Remove a draft.'''
RemoveCommand(self).run(args)
def cmd_finish(self, args):
'''Publish a draft journal entry.'''
FinishCommand(self).run(args)
def summarise_title(self, title):
basename = ''
acceptable = set(string.ascii_letters + string.digits + '-_')
for c in title.lower():
if c in acceptable:
basename += c
elif not basename.endswith('_'):
basename += '_'
return basename
def cmd_new_person(self, args):
'''Create a page to list all notes referring to a person.
This is probably only useful to Lars's personal journal.
'''
NewPersonCommand(self).run(args)
JournalTool(version=__version__).run()