From a5f08e6081e1435f28369c94e1e6f99de070baff Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 07:25:22 +1000 Subject: [PATCH 01/17] Introduce legacy classes --- features/encryption.feature | 1 + jrnl/EncryptedJournal.py | 38 ++++++++++++++++++++++++++++++-- jrnl/Journal.py | 31 +++++++++++++++++++++++--- jrnl/cli.py | 14 +++--------- jrnl/install.py | 2 +- jrnl/upgrade.py | 43 +++++++------------------------------ 6 files changed, 77 insertions(+), 52 deletions(-) diff --git a/features/encryption.feature b/features/encryption.feature index 2212fc33..ebb3cc02 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -36,6 +36,7 @@ """ Y bad doggie no biscuit + bad doggie no biscuit """ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index 16821191..dbe3695a 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -1,6 +1,8 @@ from . import Journal, util from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes, padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +import hashlib from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.backends import default_backend import base64 @@ -38,7 +40,9 @@ class EncryptedJournal(Journal.Journal): def validate_password(password): key = make_key(password) try: - return Fernet(key).decrypt(journal_encrypted).decode('utf-8') + plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8') + self.config['password'] = password + return plain except (InvalidToken, IndexError): return None if password: @@ -57,3 +61,33 @@ class EncryptedJournal(Journal.Journal): dummy = Fernet(key).encrypt("") with open(filename, 'w') as f: f.write(dummy) + + +class LegacyEncryptedJournal(Journal.Journal): + def __init__(self, name='default', **kwargs): + super(LegacyEncryptedJournal, self).__init__(name, **kwargs) + self.config['encrypt'] = True + + def _load(self, filename, password=None): + with open(filename) as f: + journal_encrypted = f.read() + iv, cipher = journal_encrypted[:16], journal_encrypted[16:] + + def validate_password(password): + decryption_key = hashlib.sha256(password.encode('utf-8')).digest() + decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor() + try: + plain_padded = decryptor.update(cipher) + decryptor.finalize() + self.config['password'] = password + if plain_padded[-1] == " ": + # Ancient versions of jrnl. Do not judge me. + plain = plain_padded.rstrip(" ") + else: + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plain = unpadder.update(plain_padded) + unpadder.finalize() + return plain.decode('utf-8') + except ValueError: + return None + if password: + return validate_password(password) + return util.get_password(keychain=self.name, validator=validate_password) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 7371703c..7e3a36c6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -10,6 +10,9 @@ import sys import codecs import re from datetime import datetime +import logging + +log = logging.getLogger("jrnl") class Journal(object): @@ -33,6 +36,15 @@ class Journal(object): """Returns the number of entries""" return len(self.entries) + @classmethod + def from_journal(cls, other): + """Creates a new journal by copying configuration and entries from + another journal object""" + new_journal = cls(other.name, **other.config) + new_journal.entries = other.entries + log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__) + return new_journal + def import_(self, other_journal_txt): self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt))) self.sort() @@ -44,6 +56,7 @@ class Journal(object): text = self._load(filename) self.entries = self._parse(text) self.sort() + log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) return self def write(self, filename=None): @@ -72,7 +85,6 @@ class Journal(object): # Initialise our current entry entries = [] current_entry = None - for line in journal_txt.splitlines(): line = line.rstrip() try: @@ -94,7 +106,7 @@ class Journal(object): # Happens when we can't parse the start of the line as an date. # In this case, just append line to our body. if current_entry: - current_entry.body += line + "\n" + current_entry.body += line + u"\n" # Append last entry if current_entry: @@ -243,10 +255,21 @@ class PlainJournal(Journal): f.write(text) -def open_journal(name, config): +def open_journal(name, config, legacy=False): """ Creates a normal, encrypted or DayOne journal based on the passed config. + If legacy is True, it will open Journals with legacy classes build for + backwards compatibility with jrnl 1.x """ + config = config.copy() + journal_conf = config['journals'].get(name) + if type(journal_conf) is dict: # We can override the default config on a by-journal basis + log.debug('Updating configuration with specific journal overrides %s', journal_conf) + config.update(journal_conf) + else: # But also just give them a string to point to the journal file + config['journal'] = journal_conf + config['journal'] = os.path.expanduser(os.path.expandvars(config['journal'])) + if os.path.isdir(config['journal']): if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']): from . import DayOneJournal @@ -259,4 +282,6 @@ def open_journal(name, config): return PlainJournal(name, **config).open() else: from . import EncryptedJournal + if legacy: + return EncryptedJournal.LegacyEncryptedJournal(name, **config).open() return EncryptedJournal.EncryptedJournal(name, **config).open() diff --git a/jrnl/cli.py b/jrnl/cli.py index fbbdefec..e0663e8e 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -18,7 +18,7 @@ import argparse import sys import logging -log = logging.getLogger(__name__) +log = logging.getLogger("jrnl") def parse_args(args=None): @@ -147,7 +147,6 @@ def run(manual_args=None): sys.exit(0) config = install.load_or_install_jrnl() - if args.ls: util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH)) ml = min(max(len(k) for k in config['journals']), 20) @@ -163,6 +162,7 @@ def run(manual_args=None): journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' if journal_name is not 'default': args.text = args.text[1:] + # If the first remaining argument looks like e.g. '-3', interpret that as a limiter if not args.limit and args.text and args.text[0].startswith("-"): try: @@ -172,16 +172,7 @@ def run(manual_args=None): pass log.debug('Using journal "%s"', journal_name) - journal_conf = config['journals'].get(journal_name) - if type(journal_conf) is dict: # We can override the default config on a by-journal basis - log.debug('Updating configuration with specific jourlnal overrides %s', journal_conf) - config.update(journal_conf) - else: # But also just give them a string to point to the journal file - config['journal'] = journal_conf - config['journal'] = os.path.expanduser(os.path.expandvars(config['journal'])) - touch_journal(config['journal']) mode_compose, mode_export, mode_import = guess_mode(args, config) - log.debug('Using journal path %(journal)s', config) # How to quit writing? if "win32" in sys.platform: @@ -206,6 +197,7 @@ def run(manual_args=None): else: mode_compose = False + # This is where we finally open the journal! journal = Journal.open_journal(journal_name, config) # Import mode diff --git a/jrnl/install.py b/jrnl/install.py index 550d9935..bee35924 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -65,7 +65,7 @@ def upgrade_config(config): for key in missing_keys: config[key] = default_config[key] save_config(config) - print("[.jrnl_conf updated to newest version at {}]".format(CONFIG_FILE_PATH)) + print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH)) def save_config(config): diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index b8941c5b..85ad9365 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -1,37 +1,8 @@ -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -import hashlib import util from . import __version__ -from . import EncryptedJournal +from . import Journal import sys -from cryptography.fernet import Fernet - - -def upgrade_encrypted_journal(filename, key_plain): - """Decrypts a journal in memory using the jrnl 1.x encryption scheme - and returns it in plain text.""" - with open(filename) as f: - iv_cipher = f.read() - iv, cipher = iv_cipher[:16], iv_cipher[16:] - decryption_key = hashlib.sha256(key_plain.encode('utf-8')).digest() - decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor() - try: - plain_padded = decryptor.update(cipher) + decryptor.finalize() - if plain_padded[-1] == " ": - # Ancient versions of jrnl. Do not judge me. - plain = plain_padded.rstrip(" ") - else: - unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() - plain = unpadder.update(plain_padded) + unpadder.finalize() - except ValueError: - return None - key = EncryptedJournal.make_key(key_plain) - journal = Fernet(key).encrypt(plain) - with open(filename, 'w') as f: - f.write(journal) - return plain +from .EncryptedJournal import EncryptedJournal def upgrade_jrnl_if_necessary(config_path): @@ -57,7 +28,6 @@ Please note that jrnl 1.x is NOT forward compatible with this version of jrnl. If you choose to proceed, you will not be able to use your journals with older versions of jrnl anymore. """.format(__version__)) - encrypted_journals = {} plain_journals = {} for journal, journal_conf in config['journals'].items(): @@ -86,9 +56,12 @@ older versions of jrnl anymore. util.prompt("jrnl NOT upgraded, exiting.") sys.exit(1) - for journal, path in encrypted_journals.items(): - util.prompt("Enter password for {} journal (stored in {}).".format(journal, path)) - util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd)) + for journal_name, path in encrypted_journals.items(): + util.prompt("Upgrading {} journal (stored in {}).".format(journal_name, path)) + old_journal = Journal.open_journal(journal_name, config, legacy=True) + new_journal = EncryptedJournal.from_journal(old_journal) + new_journal.write() + # util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd)) with open(config_path + ".backup", 'w') as config_backup: config_backup.write(config_file) From 57007a8266d43e7ccfefe773fc9704d0a1028fa8 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 02:19:03 +0400 Subject: [PATCH 02/17] Cuddle timestamp in brackets to fix #318 --- jrnl/Entry.py | 25 +++++++++++++------------ jrnl/Journal.py | 49 +++++++++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 34503cd9..4504982b 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -20,19 +20,19 @@ class Entry: @staticmethod def tag_regex(tagsymbols): pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=tagsymbols) - return re.compile( pattern, re.UNICODE ) + return re.compile(pattern, re.UNICODE) def parse_tags(self): - fulltext = " " + " ".join([self.title, self.body]).lower() + fulltext = " " + " ".join([self.title, self.body]).lower() tagsymbols = self.journal.config['tagsymbols'] - tags = re.findall( Entry.tag_regex(tagsymbols), fulltext ) + tags = re.findall(Entry.tag_regex(tagsymbols), fulltext) self.tags = tags return set(tags) def __unicode__(self): """Returns a string representation of the entry to be written into a journal file.""" date_str = self.date.strftime(self.journal.config['timeformat']) - title = date_str + " " + self.title.rstrip("\n ") + title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) if self.starred: title += " *" return "{title}{sep}{body}\n".format( @@ -48,13 +48,14 @@ class Entry: if not short and self.journal.config['linewrap']: title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap']) body = "\n".join([ - textwrap.fill((line + " ") if (len(line) == 0) else line, - self.journal.config['linewrap'], - initial_indent="| ", - subsequent_indent="| ", - drop_whitespace=False) - for line in self.body.rstrip(" \n").splitlines() - ]) + textwrap.fill( + (line + " ") if (len(line) == 0) else line, + self.journal.config['linewrap'], + initial_indent="| ", + subsequent_indent="| ", + drop_whitespace=False) + for line in self.body.rstrip(" \n").splitlines() + ]) else: title = date_str + " " + self.title.rstrip("\n ") body = self.body.rstrip("\n ") @@ -83,7 +84,7 @@ class Entry: or self.body.rstrip() != other.body.rstrip() \ or self.date != other.date \ or self.starred != other.starred: - return False + return False return True def __ne__(self, other): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 7e3a36c6..ef42f2cd 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -77,36 +77,36 @@ class Journal(object): 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 - # long the date will be by constructing one - date_length = len(datetime.today().strftime(self.config['timeformat'])) - # Initialise our current entry entries = [] current_entry = None + date_blob_re = re.compile("^\[.+\] ") for line in journal_txt.splitlines(): line = line.rstrip() - try: - # try to parse line as date => new entry begins - new_date = datetime.strptime(line[:date_length], self.config['timeformat']) + date_blob = date_blob_re.findall(line) + if date_blob: + date_blob = date_blob[0] + new_date = time.parse(date_blob.strip(" []")) + if new_date: + # Found a date at the start of the line: This is a new entry. + if current_entry: + entries.append(current_entry) - # parsing successful => save old entry and create new one - if new_date and current_entry: - entries.append(current_entry) + if line.endswith("*"): + starred = True + line = line[:-1] + else: + starred = False - if line.endswith("*"): - starred = True - line = line[:-1] - else: - starred = False - - current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred) - except ValueError: - # Happens when we can't parse the start of the line as an date. - # In this case, just append line to our body. - if current_entry: - current_entry.body += line + u"\n" + current_entry = Entry.Entry( + self, + date=new_date, + title=line[len(date_blob) + 1:], + starred=starred + ) + elif current_entry: + # Didn't find a date - keep on feeding to current entry. + current_entry.body += line + "\n" # Append last entry if current_entry: @@ -238,9 +238,6 @@ class Journal(object): class PlainJournal(Journal): - def __init__(self, name='default', **kwargs): - super(PlainJournal, self).__init__(name, **kwargs) - @classmethod def _create(cls, filename): with codecs.open(filename, "a", "utf-8"): From 5ffd0d5d0b9bb8d0972bf034c209778be577cca9 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 02:19:13 +0400 Subject: [PATCH 03/17] Legacy Journal with old timestamps --- jrnl/EncryptedJournal.py | 4 +++- jrnl/Journal.py | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index dbe3695a..c5154a96 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -63,7 +63,9 @@ class EncryptedJournal(Journal.Journal): f.write(dummy) -class LegacyEncryptedJournal(Journal.Journal): +class LegacyEncryptedJournal(Journal.LegacyJournal): + """Legacy class to support opening journals encrypted with the jrnl 1.x + standard. You'll not be able to save these journals anymore.""" def __init__(self, name='default', **kwargs): super(LegacyEncryptedJournal, self).__init__(name, **kwargs) self.config['encrypt'] = True diff --git a/jrnl/Journal.py b/jrnl/Journal.py index ef42f2cd..49623171 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -252,6 +252,55 @@ class PlainJournal(Journal): f.write(text) +class LegacyJournal(Journal): + """Legacy class to support opening journals formatted with the jrnl 1.x + standard. Main difference here is that in 1.x, timestamps were not cuddled + by square brackets, and the line break between the title and the rest of + the entry was not enforced. You'll not be able to save these journals anymore.""" + def _load(self, filename): + with codecs.open(filename, "r", "utf-8") as f: + return f.read() + + 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 + # long the date will be by constructing one + date_length = len(datetime.today().strftime(self.config['timeformat'])) + + # Initialise our current entry + entries = [] + current_entry = None + for line in journal_txt.splitlines(): + line = line.rstrip() + try: + # try to parse line as date => new entry begins + new_date = datetime.strptime(line[:date_length], self.config['timeformat']) + + # parsing successful => save old entry and create new one + if new_date and current_entry: + entries.append(current_entry) + + if line.endswith("*"): + starred = True + line = line[:-1] + else: + starred = False + + current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred) + except ValueError: + # Happens when we can't parse the start of the line as an date. + # In this case, just append line to our body. + if current_entry: + current_entry.body += line + u"\n" + + # Append last entry + if current_entry: + entries.append(current_entry) + for entry in entries: + entry.parse_tags() + return entries + + def open_journal(name, config, legacy=False): """ Creates a normal, encrypted or DayOne journal based on the passed config. @@ -276,6 +325,8 @@ def open_journal(name, config, legacy=False): sys.exit(1) if not config['encrypt']: + if legacy: + return LegacyJournal(name, **config).open() return PlainJournal(name, **config).open() else: from . import EncryptedJournal From b37cefbea1251855ea61535ca859e3aad9acbf53 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 02:19:19 +0400 Subject: [PATCH 04/17] Upgrade old time stamps to fix #317 --- jrnl/upgrade.py | 75 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index 85ad9365..96f4869d 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -1,8 +1,19 @@ -import util +from __future__ import absolute_import, unicode_literals + from . import __version__ from . import Journal -import sys +from . import util from .EncryptedJournal import EncryptedJournal +import sys +import os + + +def backup(filename, binary=False): + util.prompt(" Created a backup at {}.backup".format(filename)) + with open(filename, 'rb' if binary else 'r') as original: + contents = original.read() + with open(filename + ".backup", 'wb' if binary else 'w') as backup: + backup.write(contents) def upgrade_jrnl_if_necessary(config_path): @@ -30,41 +41,61 @@ older versions of jrnl anymore. """.format(__version__)) encrypted_journals = {} plain_journals = {} - for journal, journal_conf in config['journals'].items(): + other_journals = {} + + for journal_name, journal_conf in config['journals'].items(): if isinstance(journal_conf, dict): - if journal_conf.get("encrypt"): - encrypted_journals[journal] = journal_conf.get("journal") - else: - plain_journals[journal] = journal_conf.get("journal") + path = journal_conf.get("journal") + encrypt = journal_conf.get("encrypt") else: - if config.get('encrypt'): - encrypted_journals[journal] = journal_conf - else: - plain_journals[journal] = journal_conf + encrypt = config.get('encrypt') + path = journal_conf + + if encrypt: + encrypted_journals[journal_name] = path + elif os.path.isdir(path): + other_journals[journal_name] = path + else: + plain_journals[journal_name] = path + if encrypted_journals: longest_journal_name = max([len(journal) for journal in config['journals']]) util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__)) for journal, path in encrypted_journals.items(): util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) - if plain_journals: - util.prompt("\nFollowing plain text journals will be not be touched:") - for journal, path in plain_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) - cont = util.yesno("Continue upgrading jrnl?", default=False) + if plain_journals: + util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__)) + for journal, path in plain_journals.items(): + util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + + if other_journals: + util.prompt("\nFollowing journals will be not be touched:") + for journal, path in other_journals.items(): + util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + + cont = util.yesno("\nContinue upgrading jrnl?", default=False) if not cont: util.prompt("jrnl NOT upgraded, exiting.") sys.exit(1) for journal_name, path in encrypted_journals.items(): - util.prompt("Upgrading {} journal (stored in {}).".format(journal_name, path)) + util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path)) + backup(path, binary=True) old_journal = Journal.open_journal(journal_name, config, legacy=True) new_journal = EncryptedJournal.from_journal(old_journal) new_journal.write() - # util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd)) + util.prompt(" Done.") - with open(config_path + ".backup", 'w') as config_backup: - config_backup.write(config_file) + for journal_name, path in plain_journals.items(): + util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path)) + backup(path) + old_journal = Journal.open_journal(journal_name, config, legacy=True) + new_journal = Journal.PlainJournal.from_journal(old_journal) + new_journal.write() + util.prompt(" Done.") - util.prompt("""\n\nYour old config has been backed up to {}.backup. -We're all done here and you can start enjoying jrnl 2.""".format(config_path)) + util.prompt("\nUpgrading config...") + backup(config_path) + + util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path)) From 18d46417f1f9360a849021fb7ec713d17afc8009 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 02:27:47 +0400 Subject: [PATCH 05/17] Fix conflicting requirements Fixes #296 --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 23ca9ed0..1134744d 100644 --- a/setup.py +++ b/setup.py @@ -89,7 +89,6 @@ setup( "PyYAML>=3.11", "keyring>=3.3", "passlib>=1.6.2", - "python-dateutil>=2.2" ] + [p for p, cond in conditional_dependencies.items() if cond], long_description=__doc__, entry_points={ From b8ef40445340dddd4da41093180cd52b0735aa65 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 02:28:04 +0400 Subject: [PATCH 06/17] Cleanup and more logging --- jrnl/Journal.py | 2 +- jrnl/cli.py | 2 +- setup.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 49623171..12e638ec 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -12,7 +12,7 @@ import re from datetime import datetime import logging -log = logging.getLogger("jrnl") +log = logging.getLogger(__name__) class Journal(object): diff --git a/jrnl/cli.py b/jrnl/cli.py index e0663e8e..0fd91371 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -18,7 +18,7 @@ import argparse import sys import logging -log = logging.getLogger("jrnl") +log = logging.getLogger(__name__) def parse_args(args=None): diff --git a/setup.py b/setup.py index 1134744d..a4a0488b 100644 --- a/setup.py +++ b/setup.py @@ -75,11 +75,11 @@ conditional_dependencies = { setup( - name = "jrnl", - version = get_version(), - description = "A command line journal application that stores your journal in a plain text file", - packages = ['jrnl'], - install_requires = [ + name="jrnl", + version=get_version(), + description="A command line journal application that stores your journal in a plain text file", + packages=['jrnl'], + install_requires=[ "pyxdg>=0.19", "parsedatetime>=1.2", "pytz>=2013b", @@ -112,9 +112,9 @@ setup( 'Topic :: Text Processing' ], # metadata for upload to PyPI - author = "Manuel Ebert", - author_email = "manuel@1450.me", + author="Manuel Ebert", + author_email="manuel@1450.me", license="LICENSE", - keywords = "journal todo todo.txt jrnl".split(), - url = "http://www.jrnl.sh", + keywords="journal todo todo.txt jrnl".split(), + url="http://www.jrnl.sh", ) From 54f838245060d5c2b51cffac95e0d8036f6a4d0d Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 03:25:17 +0400 Subject: [PATCH 07/17] Fix creating non-existent journals --- jrnl/Journal.py | 7 ++++++- jrnl/cli.py | 7 ------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 12e638ec..74c82470 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -53,6 +53,11 @@ class Journal(object): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config['journal'] + + if not os.path.exists(filename): + util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) + self._create(filename) + text = self._load(filename) self.entries = self._parse(text) self.sort() @@ -101,7 +106,7 @@ class Journal(object): current_entry = Entry.Entry( self, date=new_date, - title=line[len(date_blob) + 1:], + title=line[len(date_blob):], starred=starred ) elif current_entry: diff --git a/jrnl/cli.py b/jrnl/cli.py index 0fd91371..6492e85f 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -102,13 +102,6 @@ def decrypt(journal, filename=None): util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal'])) -def touch_journal(filename): - """If filename does not exist, touch the file""" - if not os.path.exists(filename): - util.prompt("[Journal created at {0}]".format(filename)) - Journal.PlainJournal._create(filename) - - def list_journals(config): """List the journals specified in the configuration file""" sep = "\n" From c508e0c57442fd3c11bf7bcd1b198e1492ea68c0 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 03:25:39 +0400 Subject: [PATCH 08/17] Update all tests to new time format --- features/core.feature | 2 +- features/data/journals/encrypted.journal | 2 +- .../journals/{simple.journal => simple_jrnl-1-9-5.journal} | 0 features/data/journals/tags-216.journal | 2 +- features/data/journals/tags-237.journal | 2 +- features/data/journals/tags.journal | 4 ++-- features/regression.feature | 4 ++-- features/starring.feature | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) rename features/data/journals/{simple.journal => simple_jrnl-1-9-5.journal} (100%) diff --git a/features/core.feature b/features/core.feature index 85b13d90..08f7aea8 100644 --- a/features/core.feature +++ b/features/core.feature @@ -39,7 +39,7 @@ Feature: Basic reading and writing to a journal Given we use the config "basic.yaml" When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive." Then we should get no error - and the journal should contain "2013-07-25 09:00 I saw Elvis." + and the journal should contain "[2013-07-25 09:00] I saw Elvis." and the journal should contain "He's alive." Scenario: Displaying the version number diff --git a/features/data/journals/encrypted.journal b/features/data/journals/encrypted.journal index f628c25c..d2a5fcbe 100644 --- a/features/data/journals/encrypted.journal +++ b/features/data/journals/encrypted.journal @@ -1 +1 @@ -gAAAAABVH4F009PRK-vz0bGa2elPRuNWvQOFjDt_TQtTbgHDBCiWgEzsTF7c4Vy-iqm-MYOh2UUrh_kUX7vTzsj3R-OJsKEYRy060yUaOH3cfBB1QHmMBhefV2XSJ-A5u_PryN137rf7kbV5Xk0jSDi2GbRuIRT6yRER1y-MAn4RDs0jfpxfeskZ65ykaB9-5Rm-lA_1ygHM9Uwrcu3HyrMJei1C6kl23w== +gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA== diff --git a/features/data/journals/simple.journal b/features/data/journals/simple_jrnl-1-9-5.journal similarity index 100% rename from features/data/journals/simple.journal rename to features/data/journals/simple_jrnl-1-9-5.journal diff --git a/features/data/journals/tags-216.journal b/features/data/journals/tags-216.journal index 4bd942b9..08b6d630 100644 --- a/features/data/journals/tags-216.journal +++ b/features/data/journals/tags-216.journal @@ -1,2 +1,2 @@ -2013-06-10 15:40 I programmed for @OS/2. +[2013-06-10 15:40] I programmed for @OS/2. Almost makes me want to go back to @C++, though. (Still better than @C#). diff --git a/features/data/journals/tags-237.journal b/features/data/journals/tags-237.journal index a1b5f5d4..be050652 100644 --- a/features/data/journals/tags-237.journal +++ b/features/data/journals/tags-237.journal @@ -1,3 +1,3 @@ -2014-07-22 11:11 This entry has an email. +[2014-07-22 11:11] This entry has an email. @Newline tag should show as a tag. Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org. diff --git a/features/data/journals/tags.journal b/features/data/journals/tags.journal index b4186a53..a28f3159 100644 --- a/features/data/journals/tags.journal +++ b/features/data/journals/tags.journal @@ -1,8 +1,8 @@ -2013-04-09 15:39 I have an @idea: +[2013-04-09 15:39] I have an @idea: (1) write a command line @journal software (2) ??? (3) PROFIT! -2013-06-10 15:40 I met with @dan. +[2013-06-10 15:40] I met with @dan. As alway's he shared his latest @idea on how to rule the world with me. inst diff --git a/features/regression.feature b/features/regression.feature index 9ae04713..3018ea10 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -19,14 +19,14 @@ Feature: Zapped bugs should stay dead. Given we use the config "basic.yaml" 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." + and the journal should contain "[2013-11-30 15:42] Project Started." Scenario: Date in the future should be parsed correctly # https://github.com/maebert/jrnl/issues/185 Given we use the config "basic.yaml" When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019." Then we should see the message "Entry added" - and the journal should contain "2019-06-26 09:00 Planet?" + and the journal should contain "[2019-06-26 09:00] Planet?" Scenario: Loading entry with ambiguous time stamp #https://github.com/maebert/jrnl/issues/153 diff --git a/features/starring.feature b/features/starring.feature index 35154045..18113dc3 100644 --- a/features/starring.feature +++ b/features/starring.feature @@ -4,7 +4,7 @@ Feature: Starring entries Given we use the config "basic.yaml" When we run "jrnl 20 july 2013 *: Best day of my life!" Then we should see the message "Entry added" - and the journal should contain "2013-07-20 09:00 Best day of my life! *" + and the journal should contain "[2013-07-20 09:00] Best day of my life! *" Scenario: Filtering by starred entries Given we use the config "basic.yaml" From aae0fc21b62648f0d8131b9acbbdf8f5f1aeb625 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 03:26:52 +0400 Subject: [PATCH 09/17] Export DayOne UUID in json Fixes #333 --- jrnl/plugins/json_exporter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py index 374e4e51..5abaf916 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -14,13 +14,16 @@ class JSONExporter(TextExporter): @classmethod def entry_to_dict(cls, entry): - return { + entry_dict = { 'title': entry.title, 'body': entry.body, 'date': entry.date.strftime("%Y-%m-%d"), 'time': entry.date.strftime("%H:%M"), 'starred': entry.starred } + if hasattr(entry, "uuid"): + entry_dict['uuid'] = entry.uuid + return entry_dict @classmethod def export_entry(cls, entry): From 6e52b5eb705047e75ecf70b21048984260c36d09 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 03:27:09 +0400 Subject: [PATCH 10/17] Tests for #333 --- features/data/journals/simple.journal | 5 +++++ features/exporting.feature | 6 ++++++ features/steps/core.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 features/data/journals/simple.journal diff --git a/features/data/journals/simple.journal b/features/data/journals/simple.journal new file mode 100644 index 00000000..8336068e --- /dev/null +++ b/features/data/journals/simple.journal @@ -0,0 +1,5 @@ +[2013-06-09 15:39] My first entry. +Everything is alright + +[2013-06-10 15:40] Life is good. +But I'm better. diff --git a/features/exporting.feature b/features/exporting.feature index 003801a3..3d6c2607 100644 --- a/features/exporting.feature +++ b/features/exporting.feature @@ -20,3 +20,9 @@ Feature: Exporting a Journal and "tags" in the json output should contain "@journal" and "tags" in the json output should not contain "@dan" + Scenario: Exporting dayone to json + Given we use the config "dayone.yaml" + When we run "jrnl --export json" + Then we should get no error + and the output should be parsable as json + and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1" diff --git a/features/steps/core.py b/features/steps/core.py index e7711e9a..7997f8e7 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -126,6 +126,22 @@ def check_output_field_key(context, field, key): assert key in out_json[field] +@then('the json output should contain {path} = "{value}"') +def check_json_output_path(context, path, value): + """ E.g. + the json output should contain entries.0.title = "hello" + """ + out = context.stdout_capture.getvalue() + struct = json.loads(out) + + for node in path.split('.'): + try: + struct = struct[int(node)] + except ValueError: + struct = struct[node] + assert struct == value, struct + + @then('the output should be') @then('the output should be "{text}"') def check_output(context, text=None): From bad5582632f992afa6a2b678f5d45f83e2b3a21c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 05:50:30 +0400 Subject: [PATCH 11/17] Bilingual meta classes --- jrnl/plugins/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 301ad76d..6f802342 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -32,15 +32,10 @@ class PluginMeta(type): else: return ', '.join(plugin_names[:-1]) + ", or " + plugin_names[-1] - -class BaseExporter(object): - __metaclass__ = PluginMeta - names = [] - - -class BaseImporter(object): - __metaclass__ = PluginMeta - names = [] +# This looks a bit arcane, but is basically bilingual speak for defining a +# class with meta class 'PluginMeta' for both Python 2 and 3. +BaseExporter = PluginMeta(str('BaseExporter'), (), {'names': []}) +BaseImporter = PluginMeta(str('BaseImporter'), (), {'names': []}) for module in glob.glob(os.path.dirname(__file__) + "/*.py"): From 76c9006ed39ddabb4fa6a854eabc6af3e9cd1235 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 06:05:46 +0400 Subject: [PATCH 12/17] Python 3 fixes :) --- jrnl/EncryptedJournal.py | 7 +++---- jrnl/cli.py | 1 - jrnl/util.py | 9 ++++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index c5154a96..3fbffa69 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -9,13 +9,12 @@ import base64 def make_key(password): - if type(password) is unicode: - password = password.encode('utf-8') + password = util.bytes(password) kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, # Salt is hard-coded - salt='\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8', + salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8', iterations=100000, backend=default_backend() ) @@ -34,7 +33,7 @@ class EncryptedJournal(Journal.Journal): and otherwise ask the user to enter a password up to three times. If the password is provided but wrong (or corrupt), this will simply return None.""" - with open(filename) as f: + with open(filename, 'rb') as f: journal_encrypted = f.read() def validate_password(password): diff --git a/jrnl/cli.py b/jrnl/cli.py index 6492e85f..389afc0a 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -13,7 +13,6 @@ from . import util from . import install from . import plugins import jrnl -import os import argparse import sys import logging diff --git a/jrnl/util.py b/jrnl/util.py index bf3bb3c8..6d5ba528 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -75,10 +75,17 @@ def u(s): def py2encode(s): - """Encode in Python 2, but not in python 3.""" + """Encodes to UTF-8 in Python 2 but not r.""" return s.encode("utf-8") if PY2 and type(s) is unicode else s +def bytes(s): + """Returns bytes, no matter what.""" + if PY3: + return s.encode("utf-8") if type(s) is not bytes else s + return s.encode("utf-8") if type(s) is unicode else s + + def prnt(s): """Encode and print a string""" STDOUT.write(u(s + "\n")) From 4a7a8cb7a481e915ec87c24daf73b6503d677abc Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 06:14:18 +0400 Subject: [PATCH 13/17] Encryption reads and writes in binary mode --- jrnl/EncryptedJournal.py | 4 ++-- jrnl/util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index 3fbffa69..17cb880d 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -51,7 +51,7 @@ class EncryptedJournal(Journal.Journal): def _store(self, filename, text): key = make_key(self.config['password']) journal = Fernet(key).encrypt(text.encode('utf-8')) - with open(filename, 'w') as f: + with open(filename, 'wb') as f: f.write(journal) @classmethod @@ -70,7 +70,7 @@ class LegacyEncryptedJournal(Journal.LegacyJournal): self.config['encrypt'] = True def _load(self, filename, password=None): - with open(filename) as f: + with open(filename, 'rb') as f: journal_encrypted = f.read() iv, cipher = journal_encrypted[:16], journal_encrypted[16:] diff --git a/jrnl/util.py b/jrnl/util.py index 6d5ba528..8aeb1781 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -99,7 +99,7 @@ def prompt(msg): def py23_input(msg=""): - STDERR.write(u(msg)) + prompt(msg) return STDIN.readline().strip() From 6c18b6f3b44e319948ec0709a4d2a2218d701e39 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 06:38:38 +0400 Subject: [PATCH 14/17] More encoding madness --- features/steps/core.py | 2 +- jrnl/EncryptedJournal.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 7997f8e7..fadaefc2 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -167,7 +167,7 @@ def check_output_inline(context, text): out = context.stdout_capture.getvalue() if isinstance(out, bytes): out = out.decode('utf-8') - assert text in out + assert text in out, text @then('the output should not contain "{text}"') diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index 17cb880d..ae37769d 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -80,13 +80,13 @@ class LegacyEncryptedJournal(Journal.LegacyJournal): try: plain_padded = decryptor.update(cipher) + decryptor.finalize() self.config['password'] = password - if plain_padded[-1] == " ": + if plain_padded[-1] in (" ", 32): # Ancient versions of jrnl. Do not judge me. - plain = plain_padded.rstrip(" ") + return plain_padded.decode('utf-8').rstrip(" ") else: unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() plain = unpadder.update(plain_padded) + unpadder.finalize() - return plain.decode('utf-8') + return plain.decode('utf-8') except ValueError: return None if password: From b4e578b63a303aa8ddebdcce5e9051e6e61bb759 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 18:38:30 +0200 Subject: [PATCH 15/17] Fix parsing issue --- jrnl/Journal.py | 2 +- jrnl/upgrade.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 74c82470..af9e6b9e 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -85,7 +85,7 @@ class Journal(object): # Initialise our current entry entries = [] current_entry = None - date_blob_re = re.compile("^\[.+\] ") + date_blob_re = re.compile("^\[[^\\]]+\] ") for line in journal_txt.splitlines(): line = line.rstrip() date_blob = date_blob_re.findall(line) diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index 96f4869d..c73fa086 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -58,8 +58,8 @@ older versions of jrnl anymore. else: plain_journals[journal_name] = path + longest_journal_name = max([len(journal) for journal in config['journals']]) if encrypted_journals: - longest_journal_name = max([len(journal) for journal in config['journals']]) util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__)) for journal, path in encrypted_journals.items(): util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) From 539a88ed14f6a50dd67963b88d76e1a5fbd1769b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 18:38:37 +0200 Subject: [PATCH 16/17] Tests for parsing issue fix --- features/data/configs/upgrade_from_195.json | 11 +++++++ .../data/journals/simple_jrnl-1-9-5.journal | 6 ++-- features/regression.feature | 30 ++++++++++++------- features/steps/core.py | 4 ++- 4 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 features/data/configs/upgrade_from_195.json diff --git a/features/data/configs/upgrade_from_195.json b/features/data/configs/upgrade_from_195.json new file mode 100644 index 00000000..ec380372 --- /dev/null +++ b/features/data/configs/upgrade_from_195.json @@ -0,0 +1,11 @@ +{ +"default_hour": 9, +"timeformat": "%Y-%m-%d %H:%M", +"linewrap": 80, +"encrypt": false, +"editor": "", +"default_minute": 0, +"highlight": true, +"journals": {"default": "features/journals/simple_jrnl-1-9-5.journal"}, +"tagsymbols": "@" +} diff --git a/features/data/journals/simple_jrnl-1-9-5.journal b/features/data/journals/simple_jrnl-1-9-5.journal index 66d8439c..f660305b 100644 --- a/features/data/journals/simple_jrnl-1-9-5.journal +++ b/features/data/journals/simple_jrnl-1-9-5.journal @@ -1,5 +1,3 @@ -2013-06-09 15:39 My first entry. -Everything is alright +2010-06-10 15:00 A life without chocolate is like a bad analogy. -2013-06-10 15:40 Life is good. -But I'm better. +2013-06-10 15:40 He said "[this] is the best time to be alive". diff --git a/features/regression.feature b/features/regression.feature index 3018ea10..6013b804 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -38,16 +38,26 @@ Feature: Zapped bugs should stay dead. 2013-10-27 03:27 Some text. """ - Scenario: Title with an embedded period. - Given we use the config "basic.yaml" - When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic." - Then we should see the message "Entry added" - When we run "jrnl -1" - Then the output should be - """ - 2014-04-24 09:00 Created a new website - empty.com. - | Hope to get a lot of traffic. - """ + Scenario: Title with an embedded period. + Given we use the config "basic.yaml" + When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic." + Then we should see the message "Entry added" + When we run "jrnl -1" + Then the output should be + """ + 2014-04-24 09:00 Created a new website - empty.com. + | Hope to get a lot of traffic. + """ + + Scenario: Upgrade and parse journals with square brackets + Given we use the config "upgrade_from_195.json" + When we run "jrnl -2" and enter "Y" + Then the output should contain + """ + 2010-06-10 15:00 A life without chocolate is like a bad analogy. + + 2013-06-10 15:40 He said "[this] is the best time to be alive". + """ Scenario: Title with an embedded period on DayOne journal Given we use the config "dayone.yaml" diff --git a/features/steps/core.py b/features/steps/core.py index fadaefc2..3e0b1de8 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -162,8 +162,10 @@ def check_output_time_inline(context, text): assert local_date in out, local_date +@then('the output should contain') @then('the output should contain "{text}"') -def check_output_inline(context, text): +def check_output_inline(context, text=None): + text = text or context.text out = context.stdout_capture.getvalue() if isinstance(out, bytes): out = out.decode('utf-8') From bf7ece82080a65463d501f99c13ff00c77a61ebd Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 5 Apr 2015 23:55:04 +0200 Subject: [PATCH 17/17] TXT extension for temp files --- jrnl/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/util.py b/jrnl/util.py index 8aeb1781..c6ea4659 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -121,7 +121,7 @@ def load_config(config_path): def get_text_from_editor(config, template=""): - tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl")) + tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl", suffix=".txt")) with codecs.open(tmpfile, 'w', "utf-8") as f: if template: f.write(template)