From abe586d84ead60420a7019b570c6fac1474e2f86 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 30 Nov 2013 14:55:37 -0800 Subject: [PATCH 01/13] Deleting the last entry --- jrnl/Journal.py | 6 +++++- jrnl/cli.py | 56 +++++++++++++++++++++++++++++-------------------- jrnl/util.py | 19 +++++++++++++++++ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 41d7a225..906294bb 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -54,6 +54,10 @@ class Journal(object): self.entries = self.parse(journal_txt) self.sort() + def __len__(self): + """Returns the number of entries""" + return len(self.entries) + def _colorize(self, string): if colorama: return colorama.Fore.CYAN + string + colorama.Fore.RESET @@ -184,7 +188,7 @@ class Journal(object): def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" filename = filename or self.config['journal'] - journal = "\n".join([e.__unicode__() for e in self.entries]) + journal = u"\n".join([e.__unicode__() for e in self.entries]) if self.config['encrypt']: journal = self._encrypt(journal) with open(filename, 'wb') as journal_file: diff --git a/jrnl/cli.py b/jrnl/cli.py index b53d7b33..a0150163 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -20,8 +20,6 @@ except (SystemError, ValueError): import install import jrnl import os -import tempfile -import subprocess import argparse import sys @@ -51,7 +49,8 @@ def parse_args(args=None): exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', default=False, const=None) 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") + exporting.add_argument('--delete', dest='delete', help='Deletes the selected entries your journal file.', action="store_true") + exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true") return parser.parse_args(args) @@ -59,7 +58,7 @@ def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" compose = True export = False - if args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.delete_last)): + if args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.delete, args.edit)): compose = False export = True elif any((args.start_date, args.end_date, args.limit, args.strict, args.starred)): @@ -71,20 +70,6 @@ def guess_mode(args, config): 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: - util.prompt('[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. """ password = util.getpass("Enter new password: ") @@ -176,7 +161,7 @@ def run(manual_args=None): # Piping data into jrnl raw = util.py23_read() elif config['editor']: - raw = get_text_from_editor(config) + raw = util.get_text_from_editor(config) else: raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n") if raw: @@ -193,6 +178,7 @@ def run(manual_args=None): util.prompt("[Entry added to {0} journal]".format(journal_name)) journal.write() else: + old_entries = journal.entries journal.filter(tags=args.text, start_date=args.start_date, end_date=args.end_date, strict=args.strict, @@ -231,10 +217,34 @@ def run(manual_args=None): update_config(original_config, {"encrypt": False}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) - elif args.delete_last: - last_entry = journal.entries.pop() - util.prompt("[Deleted Entry:]") - print(last_entry.pprint()) + elif args.delete: + other_entries = [e for e in old_entries if e not in journal.entries] + util.prompt("Following entries will be deleted:") + for e in journal.entries[:10]: + util.prompt(" "+e.pprint(short=True)) + if len(journal) > 10: + q = "...and {0} more. Do you really want to delete these entries?".format(len(journal) - 10) + else: + q = "Do you really want to delete these entries?" + ok = util.yesno(q, default=False) + if ok: + util.prompt("[Deleted {0} entries]".format(len(journal))) + journal.entries = other_entries + journal.write() + + elif args.edit: + other_entries = [e for e in old_entries if e not in journal.entries] + # Edit + old_num_entries = len(journal) + template = u"\n".join([e.__unicode__() for e in journal.entries]) + edited = util.get_text_from_editor(config, template) + journal.entries = journal.parse(edited) + num_deleted = old_num_entries - len(journal) + if num_deleted: + util.prompt("[Deleted {0} entries]".format(num_deleted)) + else: + util.prompt("[Edited {0} entries]".format(len(journal))) + journal.entries += other_entries journal.write() if __name__ == "__main__": diff --git a/jrnl/util.py b/jrnl/util.py index d461015b..e1766c9d 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -9,6 +9,9 @@ import pytz try: import simplejson as json except ImportError: import json import re +import tempfile +import subprocess +import codecs PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 @@ -121,3 +124,19 @@ def load_and_fix_json(json_path): prompt("[Entry was NOT added to your journal]") sys.exit(1) +def get_text_from_editor(config, template=""): + tmpfile = os.path.join(tempfile.gettempdir(), "jrnl") + if template: + with codecs.open(tmpfile, 'w', "utf-8") as f: + f.write(template) + subprocess.call(config['editor'].split() + [tmpfile]) + if os.path.exists(tmpfile): + with codecs.open(tmpfile, "r", "utf-8") as f: + raw = f.read() + os.remove(tmpfile) + else: + prompt('[Nothing saved to file]') + raw = '' + + return raw + From 209228ee6a9fff13b2b77ab6eef0bc5641344f05 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 30 Nov 2013 15:10:34 -0800 Subject: [PATCH 02/13] Docs update --- docs/advanced.rst | 4 ++-- docs/usage.rst | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 6e49ea11..fa530d4d 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -6,12 +6,12 @@ Advanced Usage Configuration File ------------------- -You can configure the way jrnl behaves in a configuration file. By default, this is `~/.jrnl_conf`. If you have the `XDG_CONFIG_HOME` variable set, the configuration file will be saved under `$XDG_CONFIG_HOME/jrnl`. The configuration file is a simple JSON file with the following options. +You can configure the way jrnl behaves in a configuration file. By default, this is ``~/.jrnl_conf``. If you have the ``XDG_CONFIG_HOME`` variable set, the configuration file will be saved under ``$XDG_CONFIG_HOME/jrnl``. The configuration file is a simple JSON file with the following options. - ``journals`` paths to your journal files - ``editor`` - if set, executes this command to launch an external editor for writing your entries, e.g. ``vim`` or ``subl -w`` (note the ``-w`` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal. If you're using MacVim, that would be ``mvim -f``). + if set, executes this command to launch an external editor for writing your entries, e.g. ``vim`` or ``subl -w`` (note the ``-w`` flag to make sure *jrnl* waits for Sublime Text to close the file before writing into the journal. If you're using MacVim, that would be ``mvim -f``). - ``encrypt`` if ``true``, encrypts your journal using AES. - ``tagsymbols`` diff --git a/docs/usage.rst b/docs/usage.rst index fec5cbb9..f6abf668 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -83,5 +83,24 @@ the last five entries containing both ``@pineapple`` **and** ``@lubricant``. You .. note:: - ``jrnl @pinkie @WorldDomination`` will switch to viewing mode because although _no_ command line arguments are given, all the input strings look like tags - *jrnl* will assume you want to filter by tag. + ``jrnl @pinkie @WorldDomination`` will switch to viewing mode because although **no** command line arguments are given, all the input strings look like tags - *jrnl* will assume you want to filter by tag. + +Editing and deleting entries +---------------------------- + +Use ``--delete`` to delete entries from your journal. This will only affect selected entries, e.g. :: + + jrnl -n 1 --delete + +will delete the last entry, :: + + jrnl @girlfriend -until 'june 2012' --delete + +will delete all entries tagged with ``@girlfriend`` written before June 2012. ``jrnl --delete`` would delete your **entire** journal, which is often not what you want. You will be shown the titles of the entries which are about to be deleted before you have to confirm the deletion. + +You can also edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `). It behaves the same way ``--delete`` does, ie. :: + + jrnl -until 1950 @texas -and @history --edit + +Will edit all entries tagged with ``@texas`` and ``@history`` before 1950. Of course, if you are using multiple journals, you can also edit e.g. the entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor. From 8a8d19477f0baaddab44d3c4e75adad726a85ba1 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 30 Nov 2013 15:13:48 -0800 Subject: [PATCH 03/13] Subheaders --- docs/usage.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index f6abf668..04ffb7aa 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -88,6 +88,10 @@ the last five entries containing both ``@pineapple`` **and** ``@lubricant``. You Editing and deleting entries ---------------------------- +Deleting +~~~~~~~~ + + Use ``--delete`` to delete entries from your journal. This will only affect selected entries, e.g. :: jrnl -n 1 --delete @@ -98,6 +102,9 @@ will delete the last entry, :: will delete all entries tagged with ``@girlfriend`` written before June 2012. ``jrnl --delete`` would delete your **entire** journal, which is often not what you want. You will be shown the titles of the entries which are about to be deleted before you have to confirm the deletion. +Editing +~~~~~~~ + You can also edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `). It behaves the same way ``--delete`` does, ie. :: jrnl -until 1950 @texas -and @history --edit From 7e6c7845564ef8fef9990b4321d7648eedf4f6b3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 30 Nov 2013 15:14:53 -0800 Subject: [PATCH 04/13] Version bump --- CHANGELOG.md | 6 ++++++ jrnl/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ba7ec9..9820367f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= + +### 1.7 (December 1, 2013) + +* __1.7.0__ Edit encrypted journals with `--edit` and `--delete`. Deprecates `--delete-last` (use `-n 1 --delete` instead). + + ### 1.6 (November 5, 2013) * __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py diff --git a/jrnl/__init__.py b/jrnl/__init__.py index ca25074f..032cecb2 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.6.6' +__version__ = '1.7.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From fabb8d5f1b9c0d420813c1fcccd1f2bac6ad7474 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 2 Dec 2013 22:39:34 -0800 Subject: [PATCH 05/13] Line break --- docs/advanced.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/advanced.rst b/docs/advanced.rst index fa530d4d..b00c4525 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -51,6 +51,7 @@ Using your DayOne journal instead of a flat text file is dead simple -- instead * ``~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/`` if you're syncing with iCloud. Instead of all entries being in a single file, each entry will live in a separate `plist` file. + Multiple journal files ---------------------- From 4d8949bbed1c638395b7373281661c6d1247674c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 9 Dec 2013 13:28:19 -0800 Subject: [PATCH 06/13] Get rid of --delete --- docs/usage.rst | 32 +++++++++++--------------------- jrnl/cli.py | 18 +----------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 04ffb7aa..63597236 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -85,29 +85,19 @@ the last five entries containing both ``@pineapple`` **and** ``@lubricant``. You ``jrnl @pinkie @WorldDomination`` will switch to viewing mode because although **no** command line arguments are given, all the input strings look like tags - *jrnl* will assume you want to filter by tag. -Editing and deleting entries ----------------------------- +Editing older entries +--------------------- -Deleting -~~~~~~~~ - - -Use ``--delete`` to delete entries from your journal. This will only affect selected entries, e.g. :: - - jrnl -n 1 --delete - -will delete the last entry, :: - - jrnl @girlfriend -until 'june 2012' --delete - -will delete all entries tagged with ``@girlfriend`` written before June 2012. ``jrnl --delete`` would delete your **entire** journal, which is often not what you want. You will be shown the titles of the entries which are about to be deleted before you have to confirm the deletion. - -Editing -~~~~~~~ - -You can also edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `). It behaves the same way ``--delete`` does, ie. :: +You can edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted or if you're using a DayOne journal. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `): jrnl -until 1950 @texas -and @history --edit -Will edit all entries tagged with ``@texas`` and ``@history`` before 1950. Of course, if you are using multiple journals, you can also edit e.g. the entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor. +Will open your editor with all entries tagged with ``@texas`` and ``@history`` before 1950. You can make any changes to them you want; after you save the file and close the editor, your journal will be updated. +Of course, if you are using multiple journals, you can also edit e.g. the entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor. + +You can also use this feature for deleting entries from your journal:: + + jrnl @girlfriend -until 'june 2012' --edit + +Just select all text, press delete, and everything is gone... diff --git a/jrnl/cli.py b/jrnl/cli.py index a0150163..fb5a03ae 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -49,7 +49,6 @@ def parse_args(args=None): exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', default=False, const=None) 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', dest='delete', help='Deletes the selected entries your journal file.', action="store_true") exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true") return parser.parse_args(args) @@ -58,7 +57,7 @@ def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" compose = True export = False - if args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.delete, args.edit)): + if args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)): compose = False export = True elif any((args.start_date, args.end_date, args.limit, args.strict, args.starred)): @@ -217,21 +216,6 @@ def run(manual_args=None): update_config(original_config, {"encrypt": False}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) - elif args.delete: - other_entries = [e for e in old_entries if e not in journal.entries] - util.prompt("Following entries will be deleted:") - for e in journal.entries[:10]: - util.prompt(" "+e.pprint(short=True)) - if len(journal) > 10: - q = "...and {0} more. Do you really want to delete these entries?".format(len(journal) - 10) - else: - q = "Do you really want to delete these entries?" - ok = util.yesno(q, default=False) - if ok: - util.prompt("[Deleted {0} entries]".format(len(journal))) - journal.entries = other_entries - journal.write() - elif args.edit: other_entries = [e for e in old_entries if e not in journal.entries] # Edit From e4bc0794f13e01cbaa5cc5173d2ab3a097303cf5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 20 Dec 2013 16:16:35 +0100 Subject: [PATCH 07/13] Modified flag for entries --- jrnl/Entry.py | 1 + jrnl/Journal.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 4948cc1d..a3c20b40 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -13,6 +13,7 @@ class Entry: self.body = body.strip() self.tags = self.parse_tags() self.starred = starred + self.modified = False def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 906294bb..8a14d008 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -308,6 +308,7 @@ class Journal(object): if not date: # Still nothing? Meh, just live in the moment. date = self.parse_date("now") entry = Entry.Entry(self, date, title, body, starred=starred) + entry.modified = True self.entries.append(entry) if sort: self.sort() @@ -350,21 +351,17 @@ class DayOne(Journal): def write(self): """Writes only the entries that have been modified into plist files.""" for entry in self.entries: - # Assumption: since jrnl can not manipulate existing entries, all entries - # that have a uuid will be old ones, and only the one that doesn't will - # have a new one! - if not hasattr(entry, "uuid"): + if entry.modified: + if not hasattr(entry, "uuid"): + entry.uuid = uuid.uuid1().hex utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple())) - new_uuid = uuid.uuid1().hex - filename = os.path.join(self.config['journal'], "entries", new_uuid+".doentry") + filename = os.path.join(self.config['journal'], "entries", entry.uuid+".doentry") entry_plist = { 'Creation Date': utc_time, 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, 'Time Zone': util.get_local_timezone(), - 'UUID': new_uuid, + 'UUID': entry.uuid, 'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags] } - # print entry_plist - plistlib.writePlist(entry_plist, filename) From ca6b16a5a12d671f0e7c62dae174f01abd03879d Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 20 Dec 2013 21:11:47 +0100 Subject: [PATCH 08/13] Cleaner parsing --- jrnl/Journal.py | 21 +++++++-------------- jrnl/cli.py | 3 +-- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 8a14d008..52ab7676 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -285,13 +285,8 @@ class Journal(object): raw = raw.replace('\\n ', '\n').replace('\\n', '\n') starred = False # Split raw text into title and body - title_end = len(raw) - for separator in ["\n", ". ", "? ", "! "]: - sep_pos = raw.find(separator) - if 1 < sep_pos < title_end: - title_end = sep_pos - title = raw[:title_end+1] - body = raw[title_end+1:].strip() + sep = re.search("[\n!?.]+", raw) + title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "") starred = False if not date: if title.find(":") > 0: @@ -338,15 +333,13 @@ class DayOne(Journal): timezone = pytz.timezone(util.get_local_timezone()) date = dict_entry['Creation Date'] date = date + timezone.utcoffset(date) - entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) - entry.starred = dict_entry["Starred"] + raw = dict_entry['Entry Text'] + sep = re.search("[\n!?.]+", raw) + title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "") + entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"]) entry.uuid = dict_entry["UUID"] entry.tags = dict_entry.get("Tags", []) - # We're using new_entry to create the Entry object, which adds the entry - # to self.entries already. However, in the original Journal.__init__, this - # method is expected to return a list of newly created entries, which is why - # we're returning the obvious. - return self.entries + self.entries.append(entry) def write(self): """Writes only the entries that have been modified into plist files.""" diff --git a/jrnl/cli.py b/jrnl/cli.py index fb5a03ae..529b6f0b 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -148,11 +148,10 @@ def run(manual_args=None): else: journal = Journal.Journal(journal_name, **config) + # How to quit writing? if "win32" in sys.platform: - # for Windows systems _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" else: - # for *nix systems (and others?) _exit_multiline_code = "press Ctrl+D" if mode_compose and not args.text: From 0e637d26d0ba9893727856bacca785d7d8937cb0 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 21 Dec 2013 14:53:59 +0100 Subject: [PATCH 09/13] DayOne support for --edit --- jrnl/Entry.py | 16 +++++++- jrnl/Journal.py | 103 ++++++++++++++++++++++++++++++++++++++++++------ jrnl/cli.py | 15 +++---- 3 files changed, 113 insertions(+), 21 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index a3c20b40..142286ed 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -9,8 +9,8 @@ class Entry: def __init__(self, journal, date=None, title="", body="", starred=False): self.journal = journal # Reference to journal mainly to access it's config self.date = date or datetime.now() - self.title = title.strip() - self.body = body.strip() + self.title = title.strip("\n ") + self.body = body.strip("\n ") self.tags = self.parse_tags() self.starred = starred self.modified = False @@ -68,6 +68,18 @@ class Entry: def __repr__(self): return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) + def __eq__(self, other): + if not isinstance(other, Entry) \ + or self.title.strip() != other.title.strip() \ + or self.body.strip() != other.body.strip() \ + or self.date != other.date \ + or self.starred != other.starred: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + def to_dict(self): return { 'title': self.title.strip(), diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 52ab7676..328dfd87 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -50,9 +50,7 @@ class Journal(object): self.search_tags = None # Store tags we're highlighting self.name = name - journal_txt = self.open() - self.entries = self.parse(journal_txt) - self.sort() + self.open() def __len__(self): """Returns the number of entries""" @@ -119,9 +117,10 @@ class Journal(object): else: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() - return journal + self.entries = self._parse(journal) + self.sort() - def parse(self, journal): + def _parse(self, journal_txt): """Parses a journal that's stored in a string and returns a list of entries""" # Entries start with a line that looks like 'date title' - let's figure out how @@ -132,7 +131,7 @@ class Journal(object): entries = [] current_entry = None - for line in journal.splitlines(): + for line in journal_txt.splitlines(): try: # try to parse line as date => new entry begins line = line.strip() @@ -309,20 +308,30 @@ class Journal(object): self.sort() return entry + def editable_str(self): + """Turns the journal into a string of entries that can be edited + manually and later be parsed with eslf.parse_editable_str.""" + return u"\n".join([e.__unicode__() for e in self.entries]) + + def parse_editable_str(self, edited): + """Parses the output of self.editable_str and updates it's entries.""" + mod_entries = self._parse(edited) + # Match those entries that can be found in self.entries and set + # these to modified, so we can get a count of how many entries got + # modified and how many got deleted later. + for entry in mod_entries: + entry.modified = not any(entry == old_entry for old_entry in self.entries) + self.entries = mod_entries class DayOne(Journal): """A special Journal handling DayOne files""" def __init__(self, **kwargs): self.entries = [] + self._deleted_entries = [] super(DayOne, self).__init__(**kwargs) def open(self): - files = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))] - return files - - def parse(self, filenames): - """Instead of parsing a string into an entry, this method will take a list - of filenames, interpret each as a plist file and create a new entry from that.""" + filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))] self.entries = [] for filename in filenames: with open(filename, 'rb') as plist_entry: @@ -340,6 +349,7 @@ class DayOne(Journal): entry.uuid = dict_entry["UUID"] entry.tags = dict_entry.get("Tags", []) self.entries.append(entry) + self.sort() def write(self): """Writes only the entries that have been modified into plist files.""" @@ -358,3 +368,72 @@ class DayOne(Journal): 'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags] } plistlib.writePlist(entry_plist, filename) + for entry in self._deleted_entries: + print "DELETING", entry.uuid, entry.title + filename = os.path.join(self.config['journal'], "entries", entry.uuid+".doentry") + os.remove(filename) + + def editable_str(self): + """Turns the journal into a string of entries that can be edited + manually and later be parsed with eslf.parse_editable_str.""" + return u"\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries]) + + def parse_editable_str(self, edited): + """Parses the output of self.editable_str and updates it's entries.""" + # Method: create a new list of entries from the edited text, then match + # UUIDs of the new entries against self.entries, updating the entries + # if the edited entries differ, and deleting entries from self.entries + # if they don't show up in the edited entries anymore. + date_length = len(datetime.today().strftime(self.config['timeformat'])) + + # Initialise our current entry + entries = [] + current_entry = None + + for line in edited.splitlines(): + # try to parse line as UUID => new entry begins + line = line.strip() + m = re.match("# *([a-f0-9]+) *$", line.lower()) + if m: + if current_entry: + entries.append(current_entry) + current_entry = Entry.Entry(self) + current_entry.modified = False + current_entry.uuid = m.group(1).lower() + else: + try: + new_date = datetime.strptime(line[:date_length], self.config['timeformat']) + if line.endswith("*"): + current_entry.starred = True + line = line[:-1] + current_entry.title = line[date_length+1:] + current_entry.date = new_date + except ValueError: + if current_entry: + current_entry.body += line + "\n" + + # Append last entry + if current_entry: + entries.append(current_entry) + + # Now, update our current entries if they changed + for entry in entries: + entry.parse_tags() + matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid] + if matched_entries: + # This entry is an existing entry + match = matched_entries[0] + if match != entry: + self.entries.remove(match) + entry.modified = True + self.entries.append(entry) + else: + # This entry seems to be new... save it. + entry.modified = True + self.entries.append(entry) + # Remove deleted entries + edited_uuids = [e.uuid for e in entries] + self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids] + self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids] + return entries + diff --git a/jrnl/cli.py b/jrnl/cli.py index 529b6f0b..20e39f9a 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -219,14 +219,15 @@ def run(manual_args=None): other_entries = [e for e in old_entries if e not in journal.entries] # Edit old_num_entries = len(journal) - template = u"\n".join([e.__unicode__() for e in journal.entries]) - edited = util.get_text_from_editor(config, template) - journal.entries = journal.parse(edited) + edited = util.get_text_from_editor(config, journal.editable_str()) + journal.parse_editable_str(edited) num_deleted = old_num_entries - len(journal) - if num_deleted: - util.prompt("[Deleted {0} entries]".format(num_deleted)) - else: - util.prompt("[Edited {0} entries]".format(len(journal))) + num_edited = len([e for e in journal.entries if e.modified]) + prompts = [] + if num_deleted: prompts.append("{0} entries deleted".format(num_deleted)) + if num_edited: prompts.append("{0} entries modified".format(num_edited)) + if prompts: + util.prompt("[{0}]".format(", ".join(prompts).capitalize())) journal.entries += other_entries journal.write() From 67c012a98aabe0239ebefd00e4d476e825a0f095 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 22 Dec 2013 14:52:06 +0100 Subject: [PATCH 10/13] Fix for #117 --- jrnl/Journal.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 328dfd87..86def80c 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -11,6 +11,7 @@ try: import parsedatetime.parsedatetime_consts as pdt except ImportError: import parsedatetime.parsedatetime as pdt import re from datetime import datetime +import dateutil import time import sys try: @@ -252,7 +253,12 @@ class Journal(object): elif isinstance(date_str, datetime): return date_str - date, flag = self.dateparse.parse(date_str) + try: + date = dateutil.parser.parse(date_str) + flag = 1 if date.hour == 0 and date.minute == 0 else 2 + date = date.timetuple() + except: + date, flag = self.dateparse.parse(date_str) if not flag: # Oops, unparsable. try: # Try and parse this as a single year @@ -288,11 +294,11 @@ class Journal(object): title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "") starred = False if not date: - if title.find(":") > 0: - starred = "*" in title[:title.find(":")] - date = self.parse_date(title[:title.find(":")]) + if title.find(": ") > 0: + starred = "*" in title[:title.find(": ")] + date = self.parse_date(title[:title.find(": ")]) if date or starred: # Parsed successfully, strip that from the raw text - title = title[title.find(":")+1:].strip() + title = title[title.find(": ")+1:].strip() elif title.strip().startswith("*"): starred = True title = title[1:].strip() From c283a328e023895ea60bbb574db7572cdb698972 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 22 Dec 2013 14:52:13 +0100 Subject: [PATCH 11/13] Tests for #117 --- features/regression.feature | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/features/regression.feature b/features/regression.feature index da0ef277..af2c01f8 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -13,3 +13,10 @@ Feature: Zapped bugs should stay dead. When we run "jrnl Herro" Then we should get an error Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either" + + Scenario: Date with time should be parsed correctly + # https://github.com/maebert/jrnl/issues/117 + Given we use the config "basic.json" + When we run "jrnl 2013-11-30 15:42: Project Started." + Then we should see the message "Entry added" + and the journal should contain "2013-11-30 15:42 Project Started." From 91fd821bcccd74b7b5d8d5d5a681c0711ff40d80 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 22 Dec 2013 17:11:32 +0100 Subject: [PATCH 12/13] Changelog, dogs --- CHANGELOG.md | 5 +++-- docs/usage.rst | 22 ++++++++++++++++++++-- jrnl/Journal.py | 1 - jrnl/__init__.py | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9820367f..2b2fff7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ Changelog ========= -### 1.7 (December 1, 2013) +### 1.7 (December 22, 2013) -* __1.7.0__ Edit encrypted journals with `--edit` and `--delete`. Deprecates `--delete-last` (use `-n 1 --delete` instead). +* __1.7.1__ Fixes issues with parsing time information in entries. +* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`. ### 1.6 (November 5, 2013) diff --git a/docs/usage.rst b/docs/usage.rst index 63597236..6ef0f390 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -88,16 +88,34 @@ the last five entries containing both ``@pineapple`` **and** ``@lubricant``. You Editing older entries --------------------- -You can edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted or if you're using a DayOne journal. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `): +You can edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted or if you're using a DayOne journal. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage `):: jrnl -until 1950 @texas -and @history --edit Will open your editor with all entries tagged with ``@texas`` and ``@history`` before 1950. You can make any changes to them you want; after you save the file and close the editor, your journal will be updated. -Of course, if you are using multiple journals, you can also edit e.g. the entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor. +Of course, if you are using multiple journals, you can also edit e.g. the latest entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor. You can also use this feature for deleting entries from your journal:: jrnl @girlfriend -until 'june 2012' --edit Just select all text, press delete, and everything is gone... + +Editing DayOne Journals +~~~~~~~~~~~~~~~~~~~~~~~ + +DayOne journals can be edited exactly the same way, however the output looks a little bit different because of the way DayOne stores its entries: + +.. code-block:: output + + # af8dbd0d43fb55458f11aad586ea2abf + 2013-05-02 15:30 I told everyone I built my @robot wife for sex. + But late at night when we're alone we mostly play Battleship. + + # 2391048fe24111e1983ed49a20be6f9e + 2013-08-10 03:22 I had all kinds of plans in case of a @zombie attack. + I just figured I'd be on the other side. + +The long strings starting with hash symbol are the so-called UUIDs, unique identifiers for each entry. Don't touch them. If you do, then the old entry would get deleted and a new one written, which means that you could DayOne loose data that jrnl can't handle (such as as the entry's geolocation). + diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 86def80c..8da83dd3 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -375,7 +375,6 @@ class DayOne(Journal): } plistlib.writePlist(entry_plist, filename) for entry in self._deleted_entries: - print "DELETING", entry.uuid, entry.title filename = os.path.join(self.config['journal'], "entries", entry.uuid+".doentry") os.remove(filename) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 032cecb2..c167789a 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.7.0' +__version__ = '1.7.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From ddae49e05352f6a4f4300faf7d7ba6ca6a4106b0 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 22 Dec 2013 18:26:57 +0100 Subject: [PATCH 13/13] Smal doc fixes (fixes #115) --- docs/export.rst | 2 ++ docs/overview.rst | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/export.rst b/docs/export.rst index 4a040b09..e017951d 100644 --- a/docs/export.rst +++ b/docs/export.rst @@ -15,6 +15,8 @@ you'll get a list of all tags you used in your journal, sorted by most frequent. List of all entries ------------------- +:: + jrnl --short Will only display the date and title of each entry. diff --git a/docs/overview.rst b/docs/overview.rst index 532fde36..726fbcf1 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -15,5 +15,9 @@ Optionally, your journal can be encrypted using the `256-bit AES