summaryrefslogtreecommitdiffstats
path: root/jrnl/jrnl.py
blob: 59991150715991cd5536e3e02af360a8654f4d7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#!/usr/bin/env python
# encoding: utf-8

"""
    jrnl

    license: MIT, see LICENSE for more details.
"""

import Journal
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.0'
__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)

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)

    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 or args.encrypt or args.markdown or args.tags:
        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 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()
    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.")
        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(journal.to_json())

    elif args.markdown: # export to json
        print(journal.to_md())

    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)


if __name__ == "__main__":
    cli()