Merge branch 'pr/211' into 2.0-rc1

This commit is contained in:
Manuel Ebert 2014-06-27 14:51:03 +02:00
commit 800a373462
7 changed files with 138 additions and 56 deletions

View file

@ -4,6 +4,10 @@ Changelog
### 1.8 (May 22, 2014) ### 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.8.0__ Official support for python 3.4
### 1.7 (December 22, 2013) ### 1.7 (December 22, 2013)

View file

@ -48,6 +48,15 @@ Text export
Pretty-prints your entire journal. 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 Export to files
--------------- ---------------

View file

@ -79,26 +79,3 @@ class Entry:
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(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
)

View file

@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line.
from __future__ import absolute_import from __future__ import absolute_import
__title__ = 'jrnl' __title__ = 'jrnl'
__version__ = '1.8.1' __version__ = '1.8.4'
__author__ = 'Manuel Ebert' __author__ = 'Manuel Ebert'
__license__ = 'MIT License' __license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -153,17 +153,6 @@ def run(manual_args=None):
touch_journal(config['journal']) touch_journal(config['journal'])
mode_compose, mode_export = guess_mode(args, config) 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? # How to quit writing?
if "win32" in sys.platform: if "win32" in sys.platform:
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
@ -183,6 +172,17 @@ def run(manual_args=None):
else: else:
mode_compose = False 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 # Writing mode
if mode_compose: if mode_compose:
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()

View file

@ -6,6 +6,7 @@ import os
import json import json
from .util import u, slugify from .util import u, slugify
import codecs import codecs
from xml.dom import minidom
def get_tags_count(journal): def get_tags_count(journal):
@ -13,13 +14,13 @@ def get_tags_count(journal):
# Astute reader: should the following line leave you as puzzled as me the first time # 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. # I came across this construction, worry not and embrace the ensuing moment of enlightment.
tags = [tag tags = [tag
for entry in journal.entries for entry in journal.entries
for tag in set(entry.tags) for tag in set(entry.tags)]
]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] # 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]) tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts return tag_counts
def to_tag_list(journal): def to_tag_list(journal):
"""Prints a list of all tags and the number of occurrences.""" """Prints a list of all tags and the number of occurrences."""
tag_counts = get_tags_count(journal) tag_counts = get_tags_count(journal)
@ -32,15 +33,82 @@ def to_tag_list(journal):
result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result 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): def to_json(journal):
"""Returns a JSON representation of the Journal.""" """Returns a JSON representation of the Journal."""
tags = get_tags_count(journal) tags = get_tags_count(journal)
result = { result = {
"tags": dict((tag, count) for count, tag in tags), "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) 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')
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): def to_md(journal):
"""Returns a markdown representation of the Journal""" """Returns a markdown representation of the Journal"""
out = [] out = []
@ -58,25 +126,30 @@ def to_md(journal):
result = "\n".join(out) result = "\n".join(out)
return result return result
def to_txt(journal): def to_txt(journal):
"""Returns the complete text of the Journal.""" """Returns the complete text of the Journal."""
return journal.pprint() return journal.pprint()
def export(journal, format, output=None): def export(journal, format, output=None):
"""Exports the journal to various formats. """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 None, returns a unicode representation of the output.
If output is a directory, exports entries into individual files. If output is a directory, exports entries into individual files.
Otherwise, exports to the given output file. Otherwise, exports to the given output file.
""" """
maps = { maps = {
"json": to_json, "json": to_json,
"xml": to_xml,
"txt": to_txt, "txt": to_txt,
"text": to_txt, "text": to_txt,
"md": to_md, "md": to_md,
"markdown": 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', 'xml', and 'json']".format(format)
if output and os.path.isdir(output): # multiple files
return write_files(journal, output, format) return write_files(journal, output, format)
else: else:
content = maps[format](journal) content = maps[format](journal)
@ -84,24 +157,27 @@ def export(journal, format, output=None):
try: try:
with codecs.open(output, "w", "utf-8") as f: with codecs.open(output, "w", "utf-8") as f:
f.write(content) f.write(content)
return "[Journal exported to {0}]".format(output) return u"[Journal exported to {0}]".format(output)
except IOError as e: except IOError as e:
return "[ERROR: {0} {1}]".format(e.filename, e.strerror) return u"[ERROR: {0} {1}]".format(e.filename, e.strerror)
else: else:
return content return content
def write_files(journal, path, format): def write_files(journal, path, format):
"""Turns your journal into separate files for each entry. """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)) make_filename = lambda entry: e.date.strftime("%C-%m-%d_{0}.{1}".format(slugify(u(e.title)), format))
for e in journal.entries: for e in journal.entries:
full_path = os.path.join(path, make_filename(e)) full_path = os.path.join(path, make_filename(e))
if format == 'json': if format == 'json':
content = json.dumps(e.to_dict(), indent=2) + "\n" content = json.dumps(entry_to_dict(e), indent=2) + "\n"
elif format == 'md': elif format in ('md', 'markdown'):
content = e.to_md() content = entry_to_md(e)
elif format == 'txt': elif format in 'xml':
content = u(e) content = entry_to_xml(e)
elif format in ('txt', 'text'):
content = e.__unicode__()
with codecs.open(full_path, "w", "utf-8") as f: with codecs.open(full_path, "w", "utf-8") as f:
f.write(content) f.write(content)
return "[Journal exported individual files in {0}]".format(path) return u"[Journal exported individual files in {0}]".format(path)

View file

@ -2,10 +2,8 @@
# encoding: utf-8 # encoding: utf-8
import sys import sys
import os import os
from tzlocal import get_localzone
import getpass as gp import getpass as gp
import keyring import keyring
import pytz
import json import json
if "win32" in sys.platform: if "win32" in sys.platform:
import colorama import colorama
@ -24,12 +22,14 @@ STDOUT = sys.stdout
TEST = False TEST = False
__cached_tz = None __cached_tz = None
def getpass(prompt="Password: "): def getpass(prompt="Password: "):
if not TEST: if not TEST:
return gp.getpass(prompt) return gp.getpass(prompt)
else: else:
return py23_input(prompt) return py23_input(prompt)
def get_password(validator, keychain=None, max_attempts=3): def get_password(validator, keychain=None, max_attempts=3):
pwd_from_keychain = keychain and get_keychain(keychain) pwd_from_keychain = keychain and get_keychain(keychain)
password = pwd_from_keychain or getpass() password = pwd_from_keychain or getpass()
@ -49,9 +49,11 @@ def get_password(validator, keychain=None, max_attempts=3):
prompt("Extremely wrong password.") prompt("Extremely wrong password.")
sys.exit(1) sys.exit(1)
def get_keychain(journal_name): def get_keychain(journal_name):
return keyring.get_password('jrnl', journal_name) return keyring.get_password('jrnl', journal_name)
def set_keychain(journal_name, password): def set_keychain(journal_name, password):
if password is None: if password is None:
try: try:
@ -61,40 +63,51 @@ def set_keychain(journal_name, password):
elif not TEST: elif not TEST:
keyring.set_password('jrnl', journal_name, password) keyring.set_password('jrnl', journal_name, password)
def u(s): def u(s):
"""Mock unicode function for python 2 and 3 compatibility.""" """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): def py2encode(s):
"""Encode in Python 2, but not in python 3.""" """Encode in Python 2, but not in python 3."""
return s.encode("utf-8") if PY2 and type(s) is unicode else s return s.encode("utf-8") if PY2 and type(s) is unicode else s
def prompt(msg): def prompt(msg):
"""Prints a message to the std err stream defined in util.""" """Prints a message to the std err stream defined in util."""
if not msg.endswith("\n"): if not msg.endswith("\n"):
msg += "\n" msg += "\n"
STDERR.write(u(msg)) STDERR.write(u(msg))
def py23_input(msg=""): def py23_input(msg=""):
STDERR.write(u(msg)) STDERR.write(u(msg))
return STDIN.readline().strip() return STDIN.readline().strip()
def py23_read(msg=""): def py23_read(msg=""):
STDERR.write(u(msg)) STDERR.write(u(msg))
return STDIN.read() return STDIN.read()
def yesno(prompt, default=True): def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
raw = py23_input(prompt) raw = py23_input(prompt)
return {'y': True, 'n': False}.get(raw.lower(), default) return {'y': True, 'n': False}.get(raw.lower(), default)
def load_and_fix_json(json_path): def load_and_fix_json(json_path):
"""Tries to load a json object from a file. """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). If that fails, tries to fix common errors (no or extra , at end of the line).
""" """
with open(json_path) as f: with open(json_path) as f:
json_str = f.read() json_str = f.read()
config = fixed = None config = None
try: try:
return json.loads(json_str) return json.loads(json_str)
except ValueError as e: except ValueError as e:
@ -113,8 +126,9 @@ def load_and_fix_json(json_path):
prompt("[Entry was NOT added to your journal]") prompt("[Entry was NOT added to your journal]")
sys.exit(1) sys.exit(1)
def get_text_from_editor(config, template=""): 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: with codecs.open(tmpfile, 'w', "utf-8") as f:
if template: if template:
f.write(template) f.write(template)
@ -126,10 +140,12 @@ def get_text_from_editor(config, template=""):
prompt('[Nothing saved to file]') prompt('[Nothing saved to file]')
return raw return raw
def colorize(string): def colorize(string):
"""Returns the string wrapped in cyan ANSI escape""" """Returns the string wrapped in cyan ANSI escape"""
return u"\033[36m{}\033[39m".format(string) return u"\033[36m{}\033[39m".format(string)
def slugify(string): def slugify(string):
"""Slugifies a string. """Slugifies a string.
Based on public domain code from https://github.com/zacharyvoase/slugify 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) slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(slug) return u(slug)
def int2byte(i): def int2byte(i):
"""Converts an integer to a byte. """Converts an integer to a byte.
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3.""" 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. """Converts a byte to an integer.
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3.""" This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
return ord(b)if PY2 else b return ord(b)if PY2 else b