#!/usr/bin/env python # encoding: utf-8 """ jrnl license: MIT, see LICENSE for more details. """ import Journal import exporters from install import * import os import tempfile import subprocess import argparse import sys try: import simplejson as json except ImportError: import json __title__ = 'jrnl' __version__ = '0.3.2' __author__ = 'Manuel Ebert, Stephan Gabler' __license__ = 'MIT' CONFIG_PATH = os.path.expanduser('~/.jrnl_config') PYCRYPTO = module_exists("Crypto") def update_config(config): """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. This essentially automatically ports jrnl installations if new config parameters are introduced in later versions.""" missing_keys = set(default_config).difference(config) if missing_keys: for key in missing_keys: config[key] = default_config[key] with open(CONFIG_PATH, 'w') as f: json.dump(config, f, indent=2) print("[.jrnl_conf updated to newest version]") def parse_args(): parser = argparse.ArgumentParser() 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('text', metavar='text', nargs="*", help='Log entry (or tags by which to filter in viewing mode)') reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date') reading.add_argument('-to', dest='end_date', metavar="DATE", help='View entries before this date') reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)') reading.add_argument('-n', dest='limit', default=None, metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) reading.add_argument('-short', dest='short', action="store_true", help='Show only titles or line containing the search tags') exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal') exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences') exporting.add_argument('--json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') exporting.add_argument('--markdown', dest='markdown', action="store_true", help='Returns a Markdown-formated version of the Journal') exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None) exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) exporting.add_argument('--delete-last', dest='delete_last', help='Deletes the last entry from your journal file.', action="store_true") return parser.parse_args() def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" compose = True export = False if args.json or args.decrypt is not False or args.encrypt is not False or args.markdown or args.tags or args.delete_last: compose = False export = True elif args.start_date or args.end_date or args.limit or args.strict or args.short: # Any sign of displaying stuff? compose = False elif not args.date and args.text and all(word[0] in config['tagsymbols'] for word in args.text): # No date and only tags? compose = False return compose, export def get_text_from_editor(config): tmpfile = os.path.join(tempfile.gettempdir(), "jrnl") subprocess.call(config['editor'].split() + [tmpfile]) if os.path.exists(tmpfile): with open(tmpfile) as f: raw = f.read() os.remove(tmpfile) else: print('[Nothing saved to file]') raw = '' return raw def encrypt(journal, filename=None): """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ journal.make_key(prompt="Enter new password:") journal.config['encrypt'] = True journal.config['password'] = "" if not filename: journal.write() journal.save_config(CONFIG_PATH) print("Journal encrypted to %s." % journal.config['journal']) else: journal.write(filename) print("Journal encrypted to %s." % os.path.realpath(filename)) def decrypt(journal, filename=None): """ Decrypts into new file. If filename is not set, we encrypt the journal file itself. """ journal.config['encrypt'] = False journal.config['password'] = "" if not filename: journal.write() journal.save_config() print("Journal decrypted to %s." % journal.config['journal']) else: journal.write(filename) print("Journal encrypted to %s." % os.path.realpath(filename)) def print_tags(journal): """Prints a list of all tags and the number of occurances.""" # Astute reader: should the following line leave you as puzzled as me the first time # I came across this construction, worry not and embrace the ensuing moment of enlightment. tags = [tag for entry in journal.entries for tag in set(entry.tags) ] # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] tag_counts = {(tags.count(tag), tag) for tag in tags} for n, tag in sorted(tag_counts, reverse=True): print("{:20} : {}".format(tag, n)) def touch_journal(filename): """If filename does not exist, touch the file""" if not os.path.exists(filename): print("[Journal created at {}]".format(filename)) open(filename, 'a').close() def cli(): if not os.path.exists(CONFIG_PATH): config = install_jrnl(CONFIG_PATH) else: with open(CONFIG_PATH) as f: config = json.load(f) update_config(config) # check if the configuration is supported by available modules if config['encrypt'] and not PYCRYPTO: print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") sys.exit(-1) args = parse_args() # If the first textual argument points to a journal file, # use this! journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' if journal_name is not 'default': args.text = args.text[1:] config['journal'] = config['journals'].get(journal_name) touch_journal(config['journal']) mode_compose, mode_export = guess_mode(args, config) # open journal file journal = Journal.Journal(config=config) if mode_compose and not args.text: if config['editor']: raw = get_text_from_editor(config) else: raw = raw_input("[Compose Entry] ") if raw: args.text = [raw] else: mode_compose = False # Writing mode if mode_compose: raw = " ".join(args.text).strip() journal.new_entry(raw, args.date) print("[Entry added to {} journal]").format(journal_name) journal.write() # Reading mode elif not mode_export: journal.filter(tags=args.text, start_date=args.start_date, end_date=args.end_date, strict=args.strict, short=args.short) journal.limit(args.limit) print(journal) # Various export modes elif args.tags: print_tags(journal) elif args.json: # export to json print(exporters.to_json(journal)) elif args.markdown: # export to json print(exporters.to_md(journal)) elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") elif args.encrypt is not False: encrypt(journal, filename=args.encrypt) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) elif args.delete_last: last_entry = journal.entries.pop() print("[Deleted Entry:]") print(last_entry) journal.write() if __name__ == "__main__": cli()