#!/usr/bin/python # Copyright 2010-2013 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 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.''' if not args: raise cliapp.AppException('Usage: journal-note new TITLE') self.settings.require('source') self.settings.require('layout') if not self.settings['source']: raise cliapp.AppException( 'The --source setting is empty or missing.') if not os.path.exists(self.drafts_dir()): os.mkdir(self.drafts_dir()) for i in range(1000): name = self.draft_name(i) if not os.path.exists(name): break else: raise cliapp.AppException('ERROR: too many existing drafts') values = { 'title': args[0], 'date': time.strftime('%Y-%m-%d %H:%M') } f = open(name, 'w') f.write(template % values) f.close() self.edit_file(name) 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.''' for draft_id, filename in self.find_drafts(): print draft_id, self.get_draft_title(filename) or "" 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.''' filename = self.choose_draft(args) self.edit_file(filename) 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.''' if len(args) < 2: raise cliapp.AppException('Usage: journal-note attach ID file...') filename = self.draft_name(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) def cmd_remove(self, args): '''Remove a draft.''' if not args: raise cliapp.AppException('Usage: journal-note remove ID') filename = self.draft_name(args[0]) os.remove(filename) dirname, ext = os.path.splitext(filename) assert ext == '.mdwn' if os.path.exists(dirname): shutil.rmtree(dirname) def cmd_finish(self, args): '''Publish a draft journal entry.''' draft_mdwn = self.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.settings['git']: argv = ['git', 'add', pub_mdwn] if os.path.exists(pub_attch): argv.append(pub_attch) cliapp.runcmd(argv, cwd=self.settings['source']) cliapp.runcmd( ['git', 'commit', '-m', 'Publish log entry'], cwd=self.settings['source']) if self.settings['push']: cliapp.runcmd( ['git', 'push', 'origin', 'HEAD'], cwd=self.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.settings['layout']] return os.path.join(self.settings['source'], subdir) def published_basename(self, draft_mdwn): if self.settings['layout'] in ('liw', 'ct'): basename = time.strftime('%Y-%m-%d-%H:%M:%S') elif self.settings['layout'] == 'pkb': title = self.get_draft_title(draft_mdwn) if not title: raise Exception("%s has no title" % draft_mdwn) basename = self.summarise_title(title) else: raise Exception( 'Setting --layout=%s is unknown' % self.settings['layout']) return basename 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. ''' 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.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, }) JournalTool(version=__version__).run()