#!/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) 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') drafts_dir = DraftsDirectory(self._app.drafts_dir()) drafts_dir.create_if_missing() pathname = self._pick_draft_pathname(drafts_dir) self._create_draft(pathname, args[0]) self._app.edit_file(pathname) def _pick_draft_pathname(self, drafts_dir): for i in range(1000): pathname = drafts_dir.get_draft_pathname(i) if not os.path.exists(pathname): return pathname else: raise cliapp.AppException('ERROR: too many existing drafts') def _create_draft(self, pathname, title): values = { 'title': title, 'date': time.strftime('%Y-%m-%d %H:%M') } with open(pathname, 'w') as f: f.write(template % values) class ListCommand(Command): def run(self, args): for draft_id, filename in self._app.find_drafts(): print draft_id, self._app.get_draft_title(filename) or "" class EditCommand(Command): def run(self, args): filename = self._app.choose_draft(args) self._app.edit_file(filename) class AttachCommand(Command): def run(self, args): if len(args) < 2: raise cliapp.AppException('Usage: journal-note attach ID file...') drafts_dir = DraftsDir(self._app.draft_dir()) filename = drafts_dir.get_draft_pathname(args[0]) dirname, ext = os.path.splitext(filename) assert ext == '.mdwn' 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') filename = self._app.draft_name(args[0]) os.remove(filename) dirname, ext = os.path.splitext(filename) assert ext == '.mdwn' if os.path.exists(dirname): shutil.rmtree(dirname) class FinishCommand(Command): def run(self, args): draft_mdwn = self._app.choose_draft(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 draft_name(self, draft_id): return os.path.join(self.drafts_dir(), '%s.mdwn' % draft_id) 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 find_drafts(self): drafts_dir = self.drafts_dir() for name in os.listdir(drafts_dir): # .# is what Emacs autosave files start with. if name.endswith('.mdwn') and not name.startswith('.#'): yield name[:-len('.mdwn')], os.path.join(drafts_dir, name) 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, args): if len(args) == 0: drafts = list(self.find_drafts()) if len(drafts) == 1: draft_id, filename = drafts[0] return filename else: raise cliapp.AppException('Cannot choose entry draft automatically') elif len(args) == 1: filename = self.draft_name(args[0]) if not os.path.exists(filename): raise cliapp.AppException('draft %s does not exist' % args[0]) return filename elif len(args) > 1: raise cliapp.AppException('Must give only 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()