This commit is contained in:
GitHub Merge Button 2012-04-15 05:23:38 -07:00
commit f44545d1c0

119
jrnl.py
View file

@ -1,20 +1,30 @@
#!/usr/bin/python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
import os
import tempfile
import parsedatetime.parsedatetime as pdt import parsedatetime.parsedatetime as pdt
import parsedatetime.parsedatetime_consts as pdc import parsedatetime.parsedatetime_consts as pdc
import subprocess
import re import re
import argparse import argparse
from datetime import datetime from datetime import datetime
import time import time
import json import json
import sys
import readline, glob
from Crypto.Cipher import AES
import getpass
import mimetypes
config = { default_config = {
'journal': "/home/manuel/Dropbox/Notes/journal.txt", 'journal': os.path.expanduser("~/journal.txt"),
'editor': "",
'encrypt': False,
'key': "",
'default_hour': 9, 'default_hour': 9,
'default_minute': 0, 'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M", 'timeformat': "%Y-%m-%d %H:%M",
'tagsymbols': '#@' 'tagsymbols': '@'
} }
class Entry: class Entry:
@ -37,7 +47,7 @@ class Entry:
space = "\n" space = "\n"
return "%(date)s %(title)s %(body)s %(space)s" % { return "%(date)s %(title)s %(body)s %(space)s" % {
'date': date_str, 'date': date_str,
'title': self.title, 'title': self.title,
'body': body, 'body': body,
'space': space 'space': space
@ -63,10 +73,17 @@ class Journal:
consts = pdc.Constants() consts = pdc.Constants()
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
self.dateparse = pdt.Calendar(consts) self.dateparse = pdt.Calendar(consts)
self.crypto = None
self.entries = self.open() self.entries = self.open()
self.sort() self.sort()
def _block_tail(self, s, b=16, force=False):
"""Appends spaces to a string until length is a multiple of b"""
if force and len(s) % 16 == 0:
return s + " "*16
return s + " "*(b - len(s) % b)
def open(self, filename=None): def open(self, filename=None):
"""Opens the journal file defined in the config and parses it into a list of Entries. """Opens the journal file defined in the config and parses it into a list of Entries.
Entries have the form (date, title, body).""" Entries have the form (date, title, body)."""
@ -80,8 +97,28 @@ class Journal:
entries = [] entries = []
current_entry = None current_entry = None
journal_file = open(filename) with open(filename) as f:
for line in journal_file.readlines(): if config['encrypt']:
journal_encrypted = f.read()
key = config['key'] or getpass.getpass()
key = self._block_tail(key)
self.crypto = AES.new(key, AES.MODE_ECB)
journal_plain = self.crypto.decrypt(journal_encrypted)
# encrypted files should end with spaces. No spaces, no luck.
attempts = 1
while journal_plain and attempts < 3 and journal_plain[-1] != " ":
attempts += 1
key = getpass.getpass('Wrong password. Try again: ')
key = self._block_tail(key)
self.crypto = AES.new(key, AES.MODE_ECB)
journal_plain = self.crypto.decrypt(journal_encrypted)
if attempts >= 3:
print("Extremely wrong password.")
sys.exit(-1)
else:
journal_plain = f.read()
for line in journal_plain.split(os.linesep):
if line: if line:
try: try:
new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], config['timeformat']))) new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], config['timeformat'])))
@ -97,7 +134,6 @@ class Journal:
# Append last entry # Append last entry
if current_entry: if current_entry:
entries.append(current_entry) entries.append(current_entry)
journal_file.close()
for entry in entries: for entry in entries:
entry.parse_tags() entry.parse_tags()
return entries return entries
@ -117,10 +153,13 @@ class Journal:
def write(self, filename = None): def write(self, filename = None):
"""Dumps the journal into the config file, overwriting it""" """Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal'] filename = filename or self.config['journal']
journal_file = open(filename, 'w') journal_plain = os.linesep.join([str(e) for e in self.entries])
for entry in self.entries: with open(filename, 'w') as journal_file:
journal_file.write(str(entry)+"\n") if self.crypto:
journal_file.close() journal_padded = self._block_tail(journal_plain, force=True)
journal_file.write(self.crypto.encrypt(journal_padded))
else:
journal_file.write(journal_plain)
def sort(self): def sort(self):
"""Sorts the Journal's entries by date""" """Sorts the Journal's entries by date"""
@ -133,13 +172,13 @@ class Journal:
def filter(self, tags=[], start_date=None, end_date=None, strict=False): def filter(self, tags=[], start_date=None, end_date=None, strict=False):
"""Removes all entries from the journal that don't match the filter. """Removes all entries from the journal that don't match the filter.
tags is a list of tags, each being a string that starts with one of the tags is a list of tags, each being a string that starts with one of the
tag symbols defined in the config, e.g. ["@John", "#WorldDomination"]. tag symbols defined in the config, e.g. ["@John", "#WorldDomination"].
start_date and end_date define a timespan by which to filter. start_date and end_date define a timespan by which to filter.
If strict is True, all tags must be present in an entry. If false, the If strict is True, all tags must be present in an entry. If false, the
entry is kept if any tag is present.""" entry is kept if any tag is present."""
search_tags = set(tags) search_tags = set(tags)
end_date = self.parse_date(end_date) end_date = self.parse_date(end_date)
@ -162,7 +201,7 @@ class Journal:
return date return date
date, flag = self.dateparse.parse(date) date, flag = self.dateparse.parse(date)
if not flag: # Oops, unparsable. if not flag: # Oops, unparsable.
return None return None
@ -198,6 +237,36 @@ class Journal:
self.sort() self.sort()
if __name__ == "__main__": if __name__ == "__main__":
config_path = os.path.expanduser('~/.jrnl_config')
if not os.path.exists(config_path):
def autocomplete(text, state):
expansions = glob.glob(os.path.expanduser(text)+'*')
expansions = [e+"/" if os.path.isdir(e) else e for e in expansions]
expansions.append(None)
return expansions[state]
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(autocomplete)
path_query = 'Path to your journal file (leave blank for ~/journal.txt): '
journal_path = raw_input(path_query).strip() or os.path.expanduser('~/journal.txt')
default_config['journal'] = os.path.expanduser(journal_path)
key = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
if key:
default_config['encrypt'] = True
print("Journal will be encrypted.")
print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.")
open(default_config['journal'], 'a').close() # Touch to make sure it's there
with open(config_path, 'w') as f:
json.dump(default_config, f, indent=2)
config = default_config
if key:
config['key'] = key
else:
with open(config_path) as f:
config = json.load(f)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments')
composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"')
@ -211,9 +280,6 @@ if __name__ == "__main__":
reading.add_argument('-json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') reading.add_argument('-json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal')
args = parser.parse_args() args = parser.parse_args()
# open journal
journal = Journal(config=config)
# Guess mode # Guess mode
compose = True compose = True
if args.start_date or args.end_date or args.limit or args.json or args.strict: if args.start_date or args.end_date or args.limit or args.json or args.strict:
@ -225,15 +291,26 @@ if __name__ == "__main__":
# No text? Query # No text? Query
if compose and not args.text: if compose and not args.text:
raw = raw_input("Compose Entry: ") if config['editor']:
tmpfile = os.path.join(tempfile.gettempdir(), "jrnl")
subprocess.call(config['editor'].split() + [tmpfile])
with open(tmpfile) as f:
raw = f.read()
os.remove(tmpfile)
else:
raw = raw_input("Compose Entry: ")
if raw: if raw:
args.text = [raw] args.text = [raw]
else: else:
compose = False compose = False
# open journal
journal = Journal(config=config)
# Writing mode # Writing mode
if compose: if compose:
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
journal.new_entry(raw, args.date) journal.new_entry(raw, args.date)
print journal print journal
journal.write() journal.write()