From 9276a92a0b347776d8ebd3f27fa616672d5ba6b5 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Thu, 26 Jun 2014 08:23:37 -0400 Subject: [PATCH 1/9] Allow parallel edits * Wait to open the journal until after getting text * Use mktemp so that editors like vim get separate files for editing --- jrnl/cli.py | 22 +++++++++++----------- jrnl/util.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/jrnl/cli.py b/jrnl/cli.py index b73fd675..46cecb09 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -159,17 +159,6 @@ def run(manual_args=None): touch_journal(config['journal']) mode_compose, mode_export = guess_mode(args, config) - # open journal file or folder - if os.path.isdir(config['journal']): - if config['journal'].strip("/").endswith(".dayone") or \ - "entries" in os.listdir(config['journal']): - journal = DayOneJournal.DayOne(**config) - else: - util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) - sys.exit(1) - else: - journal = Journal.Journal(journal_name, **config) - # How to quit writing? if "win32" in sys.platform: _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" @@ -189,6 +178,17 @@ def run(manual_args=None): else: mode_compose = False + # open journal file or folder + if os.path.isdir(config['journal']): + if config['journal'].strip("/").endswith(".dayone") or \ + "entries" in os.listdir(config['journal']): + journal = DayOneJournal.DayOne(**config) + else: + util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) + sys.exit(1) + else: + journal = Journal.Journal(journal_name, **config) + # Writing mode if mode_compose: raw = " ".join(args.text).strip() diff --git a/jrnl/util.py b/jrnl/util.py index 315666b3..9c8aaa40 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -114,7 +114,7 @@ def load_and_fix_json(json_path): sys.exit(1) def get_text_from_editor(config, template=""): - tmpfile = os.path.join(tempfile.gettempdir(), "jrnl") + tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl")) with codecs.open(tmpfile, 'w', "utf-8") as f: if template: f.write(template) From 872cab6cb4e9f8e60caecb197136e92c62a4a51a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 26 Jun 2014 15:00:38 +0200 Subject: [PATCH 2/9] Allow 'text' and 'markdown' aliases in export to dir Fixes #201 --- jrnl/exporters.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 83499f87..85cb5554 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -13,13 +13,13 @@ def get_tags_count(journal): # 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) - ] + 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 = set([(tags.count(tag), tag) for tag in tags]) return tag_counts + def to_tag_list(journal): """Prints a list of all tags and the number of occurrences.""" tag_counts = get_tags_count(journal) @@ -32,6 +32,7 @@ def to_tag_list(journal): result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) return result + def to_json(journal): """Returns a JSON representation of the Journal.""" tags = get_tags_count(journal) @@ -41,6 +42,7 @@ def to_json(journal): } return json.dumps(result, indent=2) + def to_md(journal): """Returns a markdown representation of the Journal""" out = [] @@ -58,10 +60,12 @@ def to_md(journal): result = "\n".join(out) return result + def to_txt(journal): """Returns the complete text of the Journal.""" return journal.pprint() + def export(journal, format, output=None): """Exports the journal to various formats. format should be one of json, txt, text, md, markdown. @@ -76,7 +80,9 @@ def export(journal, format, output=None): "md": to_md, "markdown": to_md } - if output and os.path.isdir(output): # multiple files + if format not in maps: + return u"[ERROR: can't export to {0}. Valid options are 'md', 'txt', and 'json']".format(format) + if output and os.path.isdir(output): # multiple files return write_files(journal, output, format) else: content = maps[format](journal) @@ -84,12 +90,13 @@ def export(journal, format, output=None): try: with codecs.open(output, "w", "utf-8") as f: f.write(content) - return "[Journal exported to {0}]".format(output) + return u"[Journal exported to {0}]".format(output) except IOError as e: - return "[ERROR: {0} {1}]".format(e.filename, e.strerror) + return u"[ERROR: {0} {1}]".format(e.filename, e.strerror) else: return content + def write_files(journal, path, format): """Turns your journal into separate files for each entry. Format should be either json, md or txt.""" @@ -98,10 +105,10 @@ def write_files(journal, path, format): full_path = os.path.join(path, make_filename(e)) if format == 'json': content = json.dumps(e.to_dict(), indent=2) + "\n" - elif format == 'md': + elif format in ('md', 'markdown'): content = e.to_md() - elif format == 'txt': + elif format in ('txt', 'text'): content = u(e) with codecs.open(full_path, "w", "utf-8") as f: f.write(content) - return "[Journal exported individual files in {0}]".format(path) + return u"[Journal exported individual files in {0}]".format(path) From 828ea4d427480119693c0d157cfc77378bddea78 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 26 Jun 2014 15:03:27 +0200 Subject: [PATCH 3/9] Fixes error when exporting txt files Fixes #202 --- jrnl/exporters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 85cb5554..bdab3987 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -81,7 +81,7 @@ def export(journal, format, output=None): "markdown": to_md } if format not in maps: - return u"[ERROR: can't export to {0}. Valid options are 'md', 'txt', and 'json']".format(format) + return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format) if output and os.path.isdir(output): # multiple files return write_files(journal, output, format) else: @@ -108,7 +108,7 @@ def write_files(journal, path, format): elif format in ('md', 'markdown'): content = e.to_md() elif format in ('txt', 'text'): - content = u(e) + content = e.__unicode__() with codecs.open(full_path, "w", "utf-8") as f: f.write(content) return u"[Journal exported individual files in {0}]".format(path) From 8eeba1481daa23e5b6bd47166cd03f3cba9a07bc Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 26 Jun 2014 15:30:39 +0200 Subject: [PATCH 4/9] Version bump --- CHANGELOG.md | 4 ++++ jrnl/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb89a41a..7f39fbf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Changelog ### 1.8 (May 22, 2014) +* __1.8.4__ Improved: using external editors (thanks to @chrissexton) +* __1.8.3__ Fixed: export to text files and improves help (thanks to @igniteflow and @mpe) +* __1.8.2__ Better integration with environment variables (thanks to @ajaam and @matze) +* __1.8.1__ Minor bug fixes * __1.8.0__ Official support for python 3.4 ### 1.7 (December 22, 2013) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 2812d8d2..260d7f10 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line. from __future__ import absolute_import __title__ = 'jrnl' -__version__ = '1.8.1' +__version__ = '1.8.4' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' From ee944bc3795fb78ed3250545e8b1c30146b06a1e Mon Sep 17 00:00:00 2001 From: B Krishna Chaitanya Date: Fri, 27 Jun 2014 01:34:36 +0530 Subject: [PATCH 5/9] Export to xml --- jrnl/exporters.py | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index bdab3987..ab15096e 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -6,6 +6,7 @@ import os import json from .util import u, slugify import codecs +from xml.dom import minidom def get_tags_count(journal): @@ -43,6 +44,33 @@ def to_json(journal): return json.dumps(result, indent=2) +def to_xml(journal): + """Returns a XML representation of the Journal.""" + tags = get_tags_count(journal) + doc = minidom.Document() + xml = doc.createElement('journal') + tagsxml = doc.createElement('tags') + entries = doc.createElement('entries') + for t in tags: + tag = doc.createElement('tag') + tag.setAttribute('name', t[1]) + countNode = doc.createTextNode(str(t[0])) + tag.appendChild(countNode) + tagsxml.appendChild(tag) + for e in journal.entries: + entry = doc.createElement('entry') + ed = e.to_dict() + for en in ed: + elem = doc.createElement(en) + elem.appendChild(doc.createTextNode(str(ed[en]))) + entry.appendChild(elem) + entries.appendChild(entry) + xml.appendChild(entries) + xml.appendChild(tagsxml) + doc.appendChild(xml) + return doc.toprettyxml() + + def to_md(journal): """Returns a markdown representation of the Journal""" out = [] @@ -68,20 +96,21 @@ def to_txt(journal): def export(journal, format, output=None): """Exports the journal to various formats. - format should be one of json, txt, text, md, markdown. + format should be one of json, xml, txt, text, md, markdown. If output is None, returns a unicode representation of the output. If output is a directory, exports entries into individual files. Otherwise, exports to the given output file. """ maps = { "json": to_json, + "xml": to_xml, "txt": to_txt, "text": to_txt, "md": to_md, "markdown": to_md } if format not in maps: - return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format) + return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', 'xml', and 'json']".format(format) if output and os.path.isdir(output): # multiple files return write_files(journal, output, format) else: @@ -99,7 +128,7 @@ def export(journal, format, output=None): def write_files(journal, path, format): """Turns your journal into separate files for each entry. - Format should be either json, md or txt.""" + Format should be either json, xml, md or txt.""" make_filename = lambda entry: e.date.strftime("%C-%m-%d_{0}.{1}".format(slugify(u(e.title)), format)) for e in journal.entries: full_path = os.path.join(path, make_filename(e)) @@ -107,6 +136,8 @@ def write_files(journal, path, format): content = json.dumps(e.to_dict(), indent=2) + "\n" elif format in ('md', 'markdown'): content = e.to_md() + elif format in 'xml': + content = e.to_xml() elif format in ('txt', 'text'): content = e.__unicode__() with codecs.open(full_path, "w", "utf-8") as f: From 19572625ac55ef430f6e09ce3b3932a51097a4e5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 27 Jun 2014 14:49:45 +0200 Subject: [PATCH 6/9] Better unicode mock support --- jrnl/util.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/jrnl/util.py b/jrnl/util.py index 9c8aaa40..ecddddce 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -2,10 +2,8 @@ # encoding: utf-8 import sys import os -from tzlocal import get_localzone import getpass as gp import keyring -import pytz import json if "win32" in sys.platform: import colorama @@ -24,12 +22,14 @@ STDOUT = sys.stdout TEST = False __cached_tz = None + def getpass(prompt="Password: "): if not TEST: return gp.getpass(prompt) else: return py23_input(prompt) + def get_password(validator, keychain=None, max_attempts=3): pwd_from_keychain = keychain and get_keychain(keychain) password = pwd_from_keychain or getpass() @@ -49,9 +49,11 @@ def get_password(validator, keychain=None, max_attempts=3): prompt("Extremely wrong password.") sys.exit(1) + def get_keychain(journal_name): return keyring.get_password('jrnl', journal_name) + def set_keychain(journal_name, password): if password is None: try: @@ -61,40 +63,51 @@ def set_keychain(journal_name, password): elif not TEST: keyring.set_password('jrnl', journal_name, password) + def u(s): """Mock unicode function for python 2 and 3 compatibility.""" - return s if PY3 or type(s) is unicode else unicode(s.encode('string-escape'), "unicode_escape") + if PY3: + return str(s) + elif isinstance(s, basestring) and type(s) is not unicode: + return unicode(s.encode('string-escape'), "unicode_escape") + return unicode(s) + def py2encode(s): """Encode in Python 2, but not in python 3.""" return s.encode("utf-8") if PY2 and type(s) is unicode else s + def prompt(msg): """Prints a message to the std err stream defined in util.""" if not msg.endswith("\n"): msg += "\n" STDERR.write(u(msg)) + def py23_input(msg=""): STDERR.write(u(msg)) return STDIN.readline().strip() + def py23_read(msg=""): STDERR.write(u(msg)) return STDIN.read() + def yesno(prompt, default=True): prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") raw = py23_input(prompt) return {'y': True, 'n': False}.get(raw.lower(), default) + def load_and_fix_json(json_path): """Tries to load a json object from a file. If that fails, tries to fix common errors (no or extra , at end of the line). """ with open(json_path) as f: json_str = f.read() - config = fixed = None + config = None try: return json.loads(json_str) except ValueError as e: @@ -113,6 +126,7 @@ 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.mktemp(prefix="jrnl")) with codecs.open(tmpfile, 'w', "utf-8") as f: @@ -126,10 +140,12 @@ def get_text_from_editor(config, template=""): prompt('[Nothing saved to file]') return raw + def colorize(string): """Returns the string wrapped in cyan ANSI escape""" return u"\033[36m{}\033[39m".format(string) + def slugify(string): """Slugifies a string. Based on public domain code from https://github.com/zacharyvoase/slugify @@ -141,6 +157,7 @@ def slugify(string): slug = re.sub(r'[-\s]+', '-', no_punctuation) return u(slug) + def int2byte(i): """Converts an integer to a byte. This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3.""" @@ -151,4 +168,3 @@ def byte2int(b): """Converts a byte to an integer. This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3.""" return ord(b)if PY2 else b - From 7934dea48593b7a4fc10d98fddc65cd144b86250 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 27 Jun 2014 14:50:06 +0200 Subject: [PATCH 7/9] Fixes unicode errors in XML export --- jrnl/exporters.py | 82 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index ab15096e..c54d2260 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -34,43 +34,81 @@ def to_tag_list(journal): return result +def entry_to_dict(entry): + return { + 'title': entry.title, + 'body': entry.body, + 'date': entry.date.strftime("%Y-%m-%d"), + 'time': entry.date.strftime("%H:%M"), + 'starred': entry.starred + } + + def to_json(journal): """Returns a JSON representation of the Journal.""" tags = get_tags_count(journal) result = { "tags": dict((tag, count) for count, tag in tags), - "entries": [e.to_dict() for e in journal.entries] + "entries": [entry_to_dict(e) for e in journal.entries] } return json.dumps(result, indent=2) +def entry_to_xml(entry, doc=None): + """Turns an entry into an XML representation. + If doc is not given, it will return a full XML document. + Otherwise, it will only return a new 'entry' elemtent for + a given doc.""" + doc_el = doc or minidom.Document() + entry_el = doc_el.createElement('entry') + for key, value in entry_to_dict(entry).items(): + elem = doc_el.createElement(key) + elem.appendChild(doc_el.createTextNode(u(value))) + entry_el.appendChild(elem) + if not doc: + doc_el.appendChild(entry_el) + return doc_el.toprettyxml() + else: + return entry_el + + def to_xml(journal): """Returns a XML representation of the Journal.""" tags = get_tags_count(journal) doc = minidom.Document() xml = doc.createElement('journal') - tagsxml = doc.createElement('tags') - entries = doc.createElement('entries') - for t in tags: - tag = doc.createElement('tag') - tag.setAttribute('name', t[1]) - countNode = doc.createTextNode(str(t[0])) - tag.appendChild(countNode) - tagsxml.appendChild(tag) - for e in journal.entries: - entry = doc.createElement('entry') - ed = e.to_dict() - for en in ed: - elem = doc.createElement(en) - elem.appendChild(doc.createTextNode(str(ed[en]))) - entry.appendChild(elem) - entries.appendChild(entry) - xml.appendChild(entries) - xml.appendChild(tagsxml) + tags_el = doc.createElement('tags') + entries_el = doc.createElement('entries') + for tag in tags: + tag_el = doc.createElement('tag') + tag_el.setAttribute('name', tag[1]) + count_node = doc.createTextNode(u(tag[0])) + tag.appendChild(count_node) + tags_el.appendChild(tag) + for entry in journal.entries: + entries_el.appendChild(entry_to_xml(entry, doc)) + xml.appendChild(entries_el) + xml.appendChild(tags_el) doc.appendChild(xml) return doc.toprettyxml() +def entry_to_md(entry): + date_str = entry.date.strftime(entry.journal.config['timeformat']) + body_wrapper = "\n\n" if entry.body else "" + body = body_wrapper + entry.body + space = "\n" + md_head = "###" + + return u"{md} {date}, {title} {body} {space}".format( + md=md_head, + date=date_str, + title=entry.title, + body=body, + space=space + ) + + def to_md(journal): """Returns a markdown representation of the Journal""" out = [] @@ -133,11 +171,11 @@ def write_files(journal, path, format): for e in journal.entries: full_path = os.path.join(path, make_filename(e)) if format == 'json': - content = json.dumps(e.to_dict(), indent=2) + "\n" + content = json.dumps(entry_to_dict(e), indent=2) + "\n" elif format in ('md', 'markdown'): - content = e.to_md() + content = entry_to_md(e) elif format in 'xml': - content = e.to_xml() + content = entry_to_xml(e) elif format in ('txt', 'text'): content = e.__unicode__() with codecs.open(full_path, "w", "utf-8") as f: From a4bd1ee37983d8a69cb20f88743ef1f13717d912 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 27 Jun 2014 14:50:21 +0200 Subject: [PATCH 8/9] Cleans up entry --- jrnl/Entry.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 461014fe..841c3525 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -79,26 +79,3 @@ class Entry: def __ne__(self, other): return not self.__eq__(other) - def to_dict(self): - return { - 'title': self.title, - 'body': self.body, - 'date': self.date.strftime("%Y-%m-%d"), - 'time': self.date.strftime("%H:%M"), - 'starred': self.starred - } - - def to_md(self): - date_str = self.date.strftime(self.journal.config['timeformat']) - body_wrapper = "\n\n" if self.body else "" - body = body_wrapper + self.body - space = "\n" - md_head = "###" - - return u"{md} {date}, {title} {body} {space}".format( - md=md_head, - date=date_str, - title=self.title, - body=body, - space=space - ) From 13071edb3a9406cc9ab8a1bd6f29752110b65757 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 27 Jun 2014 14:50:50 +0200 Subject: [PATCH 9/9] Docs --- docs/export.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/export.rst b/docs/export.rst index e017951d..8b16348e 100644 --- a/docs/export.rst +++ b/docs/export.rst @@ -48,6 +48,15 @@ Text export Pretty-prints your entire journal. +XML export +----------- + +:: + + jrnl --export xml + +Why anyone would want to export stuff to XML is beyond me, but here you go. + Export to files ---------------