diff --git a/CHANGELOG.md b/CHANGELOG.md index 402ca773..0f0379f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changelog ### 1.9 (July 21, 2014) +* __1.9.7__ Fixes writing non-ascii entries on the prompt +* __1.9.6__ Fuzzy time parsing improvements (thanks to @pcarranza) * __1.9.5__ Multi-word tags for DayOne Journals * __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing * __1.9.3__ Fixed: Tags at the beginning of lines diff --git a/docs/encryption.rst b/docs/encryption.rst index 37d341a8..8ca1faba 100644 --- a/docs/encryption.rst +++ b/docs/encryption.rst @@ -30,7 +30,12 @@ A note on security While jrnl follows best practises, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell history -- it's meant to keep journals secure in transit, for example when storing it on an `untrusted `_ services such as Dropbox. If you're concerned about security, disable history logging for journal in your ``.bashrc`` :: - HISTINGNORE="jrnl *" + HISTIGNORE="jrnl *" + +If you are using zsh instead of bash, you can get the same behaviour adding this to your ``zshrc`` :: + + setopt HIST_IGNORE_SPACE + alias jrnl=" jrnl" Manual decryption ----------------- diff --git a/docs/installation.rst b/docs/installation.rst index 46df9b3a..9cadd548 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -6,7 +6,11 @@ Getting started Installation ------------ -Install *jrnl* using pip :: +On OS X, the easiest way to install *jrnl* is using `Homebrew `_ :: + + brew install jrnl + +On other platforms, install *jrnl* using pip :: pip install jrnl diff --git a/features/data/configs/multiple_without_default.json b/features/data/configs/multiple_without_default.json new file mode 100644 index 00000000..042e843a --- /dev/null +++ b/features/data/configs/multiple_without_default.json @@ -0,0 +1,16 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "simple": "features/journals/simple.journal", + "work": "features/journals/work.journal", + "ideas": "features/journals/nothing.journal" + }, + "tagsymbols": "@" +} diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index 0510209b..7c77ff72 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -34,3 +34,8 @@ Feature: Multiple journals Then journal "ideas" should not exist When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money" Then journal "ideas" should have 1 entry + + Scenario: Gracefully handle a config without a default journal + Given we use the config "multiple_without_default.json" + When we run "jrnl fork this repo and fix something" + Then we should see the message "You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line." diff --git a/features/regression.feature b/features/regression.feature index 1672afb4..f975a4b1 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -59,3 +59,10 @@ Feature: Zapped bugs should stay dead. 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. | I'm feeling sore because I forgot to stretch. """ + + Scenario: Writing an entry at the prompt with non-ascii characters + # https://github.com/maebert/jrnl/issues/295 + Given we use the config "basic.json" + When we run "jrnl" and enter "Crème brûlée & Mötorhead" + Then we should get no error + and the journal should contain "Crème brûlée & Mötorhead" diff --git a/features/steps/core.py b/features/steps/core.py index c4aa2f59..d0ea8460 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -2,9 +2,8 @@ from behave import * from jrnl import cli, Journal, util from dateutil import parser as date_parser import os -import sys +import codecs import json -import pytz import keyring keyring.set_keyring(keyring.backends.file.PlaintextKeyring()) try: @@ -30,7 +29,7 @@ def _parse_args(command): def read_journal(journal_name="default"): with open(cli.CONFIG_PATH) as config_file: config = json.load(config_file) - with open(config['journals'][journal_name]) as journal_file: + with codecs.open(config['journals'][journal_name], 'r', 'utf-8') as journal_file: journal = journal_file.read() return journal @@ -57,7 +56,7 @@ def run_with_input(context, command, inputs=None): buffer = StringIO(text.strip()) util.STDIN = buffer try: - cli.run(args or None) + cli.run(args) context.exit_status = 0 except SystemExit as e: context.exit_status = e.code @@ -66,7 +65,7 @@ def run_with_input(context, command, inputs=None): def run(context, command): args = _parse_args(command) try: - cli.run(args or None) + cli.run(args) context.exit_status = 0 except SystemExit as e: context.exit_status = e.code @@ -124,10 +123,8 @@ def check_output(context, text=None): def check_output_time_inline(context, text): out = context.stdout_capture.getvalue() local_tz = tzlocal.get_localzone() - utc_time = date_parser.parse(text) - date = utc_time + local_tz._utcoffset - local_date = date.strftime("%Y-%m-%d %H:%M") - assert local_date in out, local_date + local_time = date_parser.parse(text).astimezone(local_tz).strftime("%Y-%m-%d %H:%M") + assert local_time in out, local_time @then('the output should contain "{text}"') def check_output_inline(context, text): @@ -186,7 +183,7 @@ def config_var(context, key, value, journal=None): @then('the journal should have {number:d} entry') @then('journal "{journal_name}" should have {number:d} entries') @then('journal "{journal_name}" should have {number:d} entry') -def check_journal_content(context, number, journal_name="default"): +def check_num_entries(context, number, journal_name="default"): journal = open_journal(journal_name) assert len(journal.entries) == number diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 030a9d4b..29a9cef8 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.9.5' +__version__ = '1.9.7' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' diff --git a/jrnl/cli.py b/jrnl/cli.py index fe105e89..b7b980b4 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -156,10 +156,26 @@ def run(manual_args=None): config.update(journal_conf) else: # But also just give them a string to point to the journal file config['journal'] = journal_conf + + if config['journal'] is None: + util.prompt("You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line.") + sys.exit(1) + config['journal'] = os.path.expanduser(os.path.expandvars(config['journal'])) 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("[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" @@ -183,17 +199,6 @@ 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("[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() @@ -259,7 +264,7 @@ def run(manual_args=None): if num_deleted: prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries")) if num_edited: - prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) + prompts.append("{0} {1} modified".format(num_edited, "entry" if num_edited == 1 else "entries")) if prompts: util.prompt("[{0}]".format(", ".join(prompts).capitalize())) journal.entries += other_entries diff --git a/jrnl/time.py b/jrnl/time.py index 531293de..378d4c92 100644 --- a/jrnl/time.py +++ b/jrnl/time.py @@ -48,7 +48,10 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None): return None if flag is 1: # Date found, but no time. Use the default time. - date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0) + date = datetime(*date[:3], + hour=23 if inclusive else default_hour or 0, + minute=59 if inclusive else default_minute or 0, + second=59 if inclusive else 0) else: date = datetime(*date[:6]) diff --git a/jrnl/util.py b/jrnl/util.py index b06113c2..db8b0af7 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -71,16 +71,19 @@ def py2encode(s): def prompt(msg): """Prints a message to the std err stream defined in util.""" + if not msg: + return if not msg.endswith("\n"): msg += "\n" STDERR.write(u(msg)) def py23_input(msg=""): - STDERR.write(u(msg)) - return STDIN.readline().strip() + prompt(msg) + return u(STDIN.readline()).strip() def py23_read(msg=""): - return STDIN.read() + prompt(msg) + return u(STDIN.read()) def yesno(prompt, default=True): prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") @@ -93,7 +96,7 @@ def load_and_fix_json(json_path): """ 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,7 +116,7 @@ def load_and_fix_json(json_path): sys.exit(1) def get_text_from_editor(config, template=""): - tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl")) + _, tmpfile = os.path.join(tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")) with codecs.open(tmpfile, 'w', "utf-8") as f: if template: f.write(template)