mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Merge remote-tracking branch 'maebert/2.0-rc1' into 2.0-rc1
Conflicts: setup.py
This commit is contained in:
commit
53e3bd334c
23 changed files with 320 additions and 164 deletions
|
@ -39,7 +39,7 @@ Feature: Basic reading and writing to a journal
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
||||||
Then we should get no error
|
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."
|
and the journal should contain "He's alive."
|
||||||
|
|
||||||
Scenario: Displaying the version number
|
Scenario: Displaying the version number
|
||||||
|
|
11
features/data/configs/upgrade_from_195.json
Normal file
11
features/data/configs/upgrade_from_195.json
Normal file
|
@ -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": "@"
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
gAAAAABVH4F009PRK-vz0bGa2elPRuNWvQOFjDt_TQtTbgHDBCiWgEzsTF7c4Vy-iqm-MYOh2UUrh_kUX7vTzsj3R-OJsKEYRy060yUaOH3cfBB1QHmMBhefV2XSJ-A5u_PryN137rf7kbV5Xk0jSDi2GbRuIRT6yRER1y-MAn4RDs0jfpxfeskZ65ykaB9-5Rm-lA_1ygHM9Uwrcu3HyrMJei1C6kl23w==
|
gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
2013-06-09 15:39 My first entry.
|
[2013-06-09 15:39] My first entry.
|
||||||
Everything is alright
|
Everything is alright
|
||||||
|
|
||||||
2013-06-10 15:40 Life is good.
|
[2013-06-10 15:40] Life is good.
|
||||||
But I'm better.
|
But I'm better.
|
||||||
|
|
3
features/data/journals/simple_jrnl-1-9-5.journal
Normal file
3
features/data/journals/simple_jrnl-1-9-5.journal
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
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".
|
|
@ -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#).
|
Almost makes me want to go back to @C++, though. (Still better than @C#).
|
||||||
|
|
|
@ -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.
|
@Newline tag should show as a tag.
|
||||||
Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.
|
Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.
|
||||||
|
|
|
@ -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
|
(1) write a command line @journal software
|
||||||
(2) ???
|
(2) ???
|
||||||
(3) PROFIT!
|
(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.
|
As alway's he shared his latest @idea on how to rule the world with me.
|
||||||
inst
|
inst
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"""
|
"""
|
||||||
Y
|
Y
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
|
bad doggie no biscuit
|
||||||
"""
|
"""
|
||||||
Then we should see the message "Password"
|
Then we should see the message "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
|
@ -20,3 +20,9 @@ Feature: Exporting a Journal
|
||||||
and "tags" in the json output should contain "@journal"
|
and "tags" in the json output should contain "@journal"
|
||||||
and "tags" in the json output should not contain "@dan"
|
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"
|
||||||
|
|
|
@ -19,14 +19,14 @@ Feature: Zapped bugs should stay dead.
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl 2013-11-30 15:42: Project Started."
|
When we run "jrnl 2013-11-30 15:42: Project Started."
|
||||||
Then we should see the message "Entry added"
|
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
|
Scenario: Date in the future should be parsed correctly
|
||||||
# https://github.com/maebert/jrnl/issues/185
|
# https://github.com/maebert/jrnl/issues/185
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
||||||
Then we should see the message "Entry added"
|
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
|
Scenario: Loading entry with ambiguous time stamp
|
||||||
#https://github.com/maebert/jrnl/issues/153
|
#https://github.com/maebert/jrnl/issues/153
|
||||||
|
@ -38,16 +38,26 @@ Feature: Zapped bugs should stay dead.
|
||||||
2013-10-27 03:27 Some text.
|
2013-10-27 03:27 Some text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: Title with an embedded period.
|
Scenario: Title with an embedded period.
|
||||||
Given we use the config "basic.yaml"
|
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."
|
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"
|
Then we should see the message "Entry added"
|
||||||
When we run "jrnl -1"
|
When we run "jrnl -1"
|
||||||
Then the output should be
|
Then the output should be
|
||||||
"""
|
"""
|
||||||
2014-04-24 09:00 Created a new website - empty.com.
|
2014-04-24 09:00 Created a new website - empty.com.
|
||||||
| Hope to get a lot of traffic.
|
| 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
|
Scenario: Title with an embedded period on DayOne journal
|
||||||
Given we use the config "dayone.yaml"
|
Given we use the config "dayone.yaml"
|
||||||
|
|
|
@ -4,7 +4,7 @@ Feature: Starring entries
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl 20 july 2013 *: Best day of my life!"
|
When we run "jrnl 20 july 2013 *: Best day of my life!"
|
||||||
Then we should see the message "Entry added"
|
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
|
Scenario: Filtering by starred entries
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
|
|
|
@ -126,6 +126,22 @@ def check_output_field_key(context, field, key):
|
||||||
assert key in out_json[field]
|
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')
|
||||||
@then('the output should be "{text}"')
|
@then('the output should be "{text}"')
|
||||||
def check_output(context, text=None):
|
def check_output(context, text=None):
|
||||||
|
@ -146,12 +162,14 @@ def check_output_time_inline(context, text):
|
||||||
assert local_date in out, local_date
|
assert local_date in out, local_date
|
||||||
|
|
||||||
|
|
||||||
|
@then('the output should contain')
|
||||||
@then('the output should contain "{text}"')
|
@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()
|
out = context.stdout_capture.getvalue()
|
||||||
if isinstance(out, bytes):
|
if isinstance(out, bytes):
|
||||||
out = out.decode('utf-8')
|
out = out.decode('utf-8')
|
||||||
assert text in out
|
assert text in out, text
|
||||||
|
|
||||||
|
|
||||||
@then('the output should not contain "{text}"')
|
@then('the output should not contain "{text}"')
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
from . import Journal, util
|
from . import Journal, util
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
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.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
def make_key(password):
|
def make_key(password):
|
||||||
if type(password) is unicode:
|
password = util.bytes(password)
|
||||||
password = password.encode('utf-8')
|
|
||||||
kdf = PBKDF2HMAC(
|
kdf = PBKDF2HMAC(
|
||||||
algorithm=hashes.SHA256(),
|
algorithm=hashes.SHA256(),
|
||||||
length=32,
|
length=32,
|
||||||
# Salt is hard-coded
|
# 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,
|
iterations=100000,
|
||||||
backend=default_backend()
|
backend=default_backend()
|
||||||
)
|
)
|
||||||
|
@ -32,13 +33,15 @@ class EncryptedJournal(Journal.Journal):
|
||||||
and otherwise ask the user to enter a password up to three times.
|
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
|
If the password is provided but wrong (or corrupt), this will simply
|
||||||
return None."""
|
return None."""
|
||||||
with open(filename) as f:
|
with open(filename, 'rb') as f:
|
||||||
journal_encrypted = f.read()
|
journal_encrypted = f.read()
|
||||||
|
|
||||||
def validate_password(password):
|
def validate_password(password):
|
||||||
key = make_key(password)
|
key = make_key(password)
|
||||||
try:
|
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):
|
except (InvalidToken, IndexError):
|
||||||
return None
|
return None
|
||||||
if password:
|
if password:
|
||||||
|
@ -48,7 +51,7 @@ class EncryptedJournal(Journal.Journal):
|
||||||
def _store(self, filename, text):
|
def _store(self, filename, text):
|
||||||
key = make_key(self.config['password'])
|
key = make_key(self.config['password'])
|
||||||
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'wb') as f:
|
||||||
f.write(journal)
|
f.write(journal)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -57,3 +60,35 @@ class EncryptedJournal(Journal.Journal):
|
||||||
dummy = Fernet(key).encrypt("")
|
dummy = Fernet(key).encrypt("")
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
f.write(dummy)
|
f.write(dummy)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def _load(self, filename, password=None):
|
||||||
|
with open(filename, 'rb') 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] in (" ", 32):
|
||||||
|
# Ancient versions of jrnl. Do not judge me.
|
||||||
|
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')
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if password:
|
||||||
|
return validate_password(password)
|
||||||
|
return util.get_password(keychain=self.name, validator=validate_password)
|
||||||
|
|
|
@ -20,19 +20,19 @@ class Entry:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tag_regex(tagsymbols):
|
def tag_regex(tagsymbols):
|
||||||
pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=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):
|
def parse_tags(self):
|
||||||
fulltext = " " + " ".join([self.title, self.body]).lower()
|
fulltext = " " + " ".join([self.title, self.body]).lower()
|
||||||
tagsymbols = self.journal.config['tagsymbols']
|
tagsymbols = self.journal.config['tagsymbols']
|
||||||
tags = re.findall( Entry.tag_regex(tagsymbols), fulltext )
|
tags = re.findall(Entry.tag_regex(tagsymbols), fulltext)
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
return set(tags)
|
return set(tags)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
"""Returns a string representation of the entry to be written into a journal file."""
|
"""Returns a string representation of the entry to be written into a journal file."""
|
||||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
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:
|
if self.starred:
|
||||||
title += " *"
|
title += " *"
|
||||||
return "{title}{sep}{body}\n".format(
|
return "{title}{sep}{body}\n".format(
|
||||||
|
@ -48,13 +48,14 @@ class Entry:
|
||||||
if not short and self.journal.config['linewrap']:
|
if not short and self.journal.config['linewrap']:
|
||||||
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
|
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
|
||||||
body = "\n".join([
|
body = "\n".join([
|
||||||
textwrap.fill((line + " ") if (len(line) == 0) else line,
|
textwrap.fill(
|
||||||
self.journal.config['linewrap'],
|
(line + " ") if (len(line) == 0) else line,
|
||||||
initial_indent="| ",
|
self.journal.config['linewrap'],
|
||||||
subsequent_indent="| ",
|
initial_indent="| ",
|
||||||
drop_whitespace=False)
|
subsequent_indent="| ",
|
||||||
for line in self.body.rstrip(" \n").splitlines()
|
drop_whitespace=False)
|
||||||
])
|
for line in self.body.rstrip(" \n").splitlines()
|
||||||
|
])
|
||||||
else:
|
else:
|
||||||
title = date_str + " " + self.title.rstrip("\n ")
|
title = date_str + " " + self.title.rstrip("\n ")
|
||||||
body = self.body.rstrip("\n ")
|
body = self.body.rstrip("\n ")
|
||||||
|
@ -83,7 +84,7 @@ class Entry:
|
||||||
or self.body.rstrip() != other.body.rstrip() \
|
or self.body.rstrip() != other.body.rstrip() \
|
||||||
or self.date != other.date \
|
or self.date != other.date \
|
||||||
or self.starred != other.starred:
|
or self.starred != other.starred:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
|
|
134
jrnl/Journal.py
134
jrnl/Journal.py
|
@ -10,6 +10,9 @@ import sys
|
||||||
import codecs
|
import codecs
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal(object):
|
||||||
|
@ -33,6 +36,15 @@ class Journal(object):
|
||||||
"""Returns the number of entries"""
|
"""Returns the number of entries"""
|
||||||
return len(self.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):
|
def import_(self, other_journal_txt):
|
||||||
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
|
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
|
||||||
self.sort()
|
self.sort()
|
||||||
|
@ -41,9 +53,15 @@ class Journal(object):
|
||||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config['journal']
|
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)
|
text = self._load(filename)
|
||||||
self.entries = self._parse(text)
|
self.entries = self._parse(text)
|
||||||
self.sort()
|
self.sort()
|
||||||
|
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def write(self, filename=None):
|
def write(self, filename=None):
|
||||||
|
@ -64,37 +82,36 @@ class Journal(object):
|
||||||
|
|
||||||
def _parse(self, journal_txt):
|
def _parse(self, journal_txt):
|
||||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
"""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
|
# Initialise our current entry
|
||||||
entries = []
|
entries = []
|
||||||
current_entry = None
|
current_entry = None
|
||||||
|
date_blob_re = re.compile("^\[[^\\]]+\] ")
|
||||||
for line in journal_txt.splitlines():
|
for line in journal_txt.splitlines():
|
||||||
line = line.rstrip()
|
line = line.rstrip()
|
||||||
try:
|
date_blob = date_blob_re.findall(line)
|
||||||
# try to parse line as date => new entry begins
|
if date_blob:
|
||||||
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
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 line.endswith("*"):
|
||||||
if new_date and current_entry:
|
starred = True
|
||||||
entries.append(current_entry)
|
line = line[:-1]
|
||||||
|
else:
|
||||||
|
starred = False
|
||||||
|
|
||||||
if line.endswith("*"):
|
current_entry = Entry.Entry(
|
||||||
starred = True
|
self,
|
||||||
line = line[:-1]
|
date=new_date,
|
||||||
else:
|
title=line[len(date_blob):],
|
||||||
starred = False
|
starred=starred
|
||||||
|
)
|
||||||
current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred)
|
elif current_entry:
|
||||||
except ValueError:
|
# Didn't find a date - keep on feeding to current entry.
|
||||||
# Happens when we can't parse the start of the line as an date.
|
current_entry.body += line + "\n"
|
||||||
# In this case, just append line to our body.
|
|
||||||
if current_entry:
|
|
||||||
current_entry.body += line + "\n"
|
|
||||||
|
|
||||||
# Append last entry
|
# Append last entry
|
||||||
if current_entry:
|
if current_entry:
|
||||||
|
@ -226,9 +243,6 @@ class Journal(object):
|
||||||
|
|
||||||
|
|
||||||
class PlainJournal(Journal):
|
class PlainJournal(Journal):
|
||||||
def __init__(self, name='default', **kwargs):
|
|
||||||
super(PlainJournal, self).__init__(name, **kwargs)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, filename):
|
def _create(cls, filename):
|
||||||
with codecs.open(filename, "a", "utf-8"):
|
with codecs.open(filename, "a", "utf-8"):
|
||||||
|
@ -243,10 +257,70 @@ class PlainJournal(Journal):
|
||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
def open_journal(name, config):
|
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.
|
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 os.path.isdir(config['journal']):
|
||||||
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
||||||
from . import DayOneJournal
|
from . import DayOneJournal
|
||||||
|
@ -256,7 +330,11 @@ def open_journal(name, config):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not config['encrypt']:
|
if not config['encrypt']:
|
||||||
|
if legacy:
|
||||||
|
return LegacyJournal(name, **config).open()
|
||||||
return PlainJournal(name, **config).open()
|
return PlainJournal(name, **config).open()
|
||||||
else:
|
else:
|
||||||
from . import EncryptedJournal
|
from . import EncryptedJournal
|
||||||
|
if legacy:
|
||||||
|
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
||||||
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
||||||
|
|
20
jrnl/cli.py
20
jrnl/cli.py
|
@ -13,7 +13,6 @@ from . import util
|
||||||
from . import install
|
from . import install
|
||||||
from . import plugins
|
from . import plugins
|
||||||
import jrnl
|
import jrnl
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
@ -102,13 +101,6 @@ def decrypt(journal, filename=None):
|
||||||
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
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):
|
def list_journals(config):
|
||||||
"""List the journals specified in the configuration file"""
|
"""List the journals specified in the configuration file"""
|
||||||
sep = "\n"
|
sep = "\n"
|
||||||
|
@ -147,7 +139,6 @@ def run(manual_args=None):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
config = install.load_or_install_jrnl()
|
config = install.load_or_install_jrnl()
|
||||||
|
|
||||||
if args.ls:
|
if args.ls:
|
||||||
util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
|
util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
|
||||||
ml = min(max(len(k) for k in config['journals']), 20)
|
ml = min(max(len(k) for k in config['journals']), 20)
|
||||||
|
@ -163,6 +154,7 @@ def run(manual_args=None):
|
||||||
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
||||||
if journal_name is not 'default':
|
if journal_name is not 'default':
|
||||||
args.text = args.text[1:]
|
args.text = args.text[1:]
|
||||||
|
|
||||||
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter
|
# 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("-"):
|
if not args.limit and args.text and args.text[0].startswith("-"):
|
||||||
try:
|
try:
|
||||||
|
@ -172,16 +164,7 @@ def run(manual_args=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
log.debug('Using journal "%s"', journal_name)
|
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)
|
mode_compose, mode_export, mode_import = guess_mode(args, config)
|
||||||
log.debug('Using journal path %(journal)s', config)
|
|
||||||
|
|
||||||
# How to quit writing?
|
# How to quit writing?
|
||||||
if "win32" in sys.platform:
|
if "win32" in sys.platform:
|
||||||
|
@ -206,6 +189,7 @@ def run(manual_args=None):
|
||||||
else:
|
else:
|
||||||
mode_compose = False
|
mode_compose = False
|
||||||
|
|
||||||
|
# This is where we finally open the journal!
|
||||||
journal = Journal.open_journal(journal_name, config)
|
journal = Journal.open_journal(journal_name, config)
|
||||||
|
|
||||||
# Import mode
|
# Import mode
|
||||||
|
|
|
@ -65,7 +65,7 @@ def upgrade_config(config):
|
||||||
for key in missing_keys:
|
for key in missing_keys:
|
||||||
config[key] = default_config[key]
|
config[key] = default_config[key]
|
||||||
save_config(config)
|
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):
|
def save_config(config):
|
||||||
|
|
|
@ -32,15 +32,10 @@ class PluginMeta(type):
|
||||||
else:
|
else:
|
||||||
return ', '.join(plugin_names[:-1]) + ", or " + plugin_names[-1]
|
return ', '.join(plugin_names[:-1]) + ", or " + plugin_names[-1]
|
||||||
|
|
||||||
|
# This looks a bit arcane, but is basically bilingual speak for defining a
|
||||||
class BaseExporter(object):
|
# class with meta class 'PluginMeta' for both Python 2 and 3.
|
||||||
__metaclass__ = PluginMeta
|
BaseExporter = PluginMeta(str('BaseExporter'), (), {'names': []})
|
||||||
names = []
|
BaseImporter = PluginMeta(str('BaseImporter'), (), {'names': []})
|
||||||
|
|
||||||
|
|
||||||
class BaseImporter(object):
|
|
||||||
__metaclass__ = PluginMeta
|
|
||||||
names = []
|
|
||||||
|
|
||||||
|
|
||||||
for module in glob.glob(os.path.dirname(__file__) + "/*.py"):
|
for module in glob.glob(os.path.dirname(__file__) + "/*.py"):
|
||||||
|
|
|
@ -14,13 +14,16 @@ class JSONExporter(TextExporter):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def entry_to_dict(cls, entry):
|
def entry_to_dict(cls, entry):
|
||||||
return {
|
entry_dict = {
|
||||||
'title': entry.title,
|
'title': entry.title,
|
||||||
'body': entry.body,
|
'body': entry.body,
|
||||||
'date': entry.date.strftime("%Y-%m-%d"),
|
'date': entry.date.strftime("%Y-%m-%d"),
|
||||||
'time': entry.date.strftime("%H:%M"),
|
'time': entry.date.strftime("%H:%M"),
|
||||||
'starred': entry.starred
|
'starred': entry.starred
|
||||||
}
|
}
|
||||||
|
if hasattr(entry, "uuid"):
|
||||||
|
entry_dict['uuid'] = entry.uuid
|
||||||
|
return entry_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
|
|
110
jrnl/upgrade.py
110
jrnl/upgrade.py
|
@ -1,37 +1,19 @@
|
||||||
from cryptography.hazmat.backends import default_backend
|
from __future__ import absolute_import, unicode_literals
|
||||||
from cryptography.hazmat.primitives import padding
|
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
||||||
import hashlib
|
|
||||||
import util
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import EncryptedJournal
|
from . import Journal
|
||||||
|
from . import util
|
||||||
|
from .EncryptedJournal import EncryptedJournal
|
||||||
import sys
|
import sys
|
||||||
from cryptography.fernet import Fernet
|
import os
|
||||||
|
|
||||||
|
|
||||||
def upgrade_encrypted_journal(filename, key_plain):
|
def backup(filename, binary=False):
|
||||||
"""Decrypts a journal in memory using the jrnl 1.x encryption scheme
|
util.prompt(" Created a backup at {}.backup".format(filename))
|
||||||
and returns it in plain text."""
|
with open(filename, 'rb' if binary else 'r') as original:
|
||||||
with open(filename) as f:
|
contents = original.read()
|
||||||
iv_cipher = f.read()
|
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
|
||||||
iv, cipher = iv_cipher[:16], iv_cipher[16:]
|
backup.write(contents)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade_jrnl_if_necessary(config_path):
|
def upgrade_jrnl_if_necessary(config_path):
|
||||||
|
@ -57,41 +39,63 @@ 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
|
If you choose to proceed, you will not be able to use your journals with
|
||||||
older versions of jrnl anymore.
|
older versions of jrnl anymore.
|
||||||
""".format(__version__))
|
""".format(__version__))
|
||||||
|
|
||||||
encrypted_journals = {}
|
encrypted_journals = {}
|
||||||
plain_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 isinstance(journal_conf, dict):
|
||||||
if journal_conf.get("encrypt"):
|
path = journal_conf.get("journal")
|
||||||
encrypted_journals[journal] = journal_conf.get("journal")
|
encrypt = journal_conf.get("encrypt")
|
||||||
else:
|
|
||||||
plain_journals[journal] = journal_conf.get("journal")
|
|
||||||
else:
|
else:
|
||||||
if config.get('encrypt'):
|
encrypt = config.get('encrypt')
|
||||||
encrypted_journals[journal] = journal_conf
|
path = journal_conf
|
||||||
else:
|
|
||||||
plain_journals[journal] = 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
|
||||||
|
|
||||||
|
longest_journal_name = max([len(journal) for journal in config['journals']])
|
||||||
if encrypted_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__))
|
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
|
||||||
for journal, path in encrypted_journals.items():
|
for journal, path in encrypted_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
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:
|
if not cont:
|
||||||
util.prompt("jrnl NOT upgraded, exiting.")
|
util.prompt("jrnl NOT upgraded, exiting.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
for journal, path in encrypted_journals.items():
|
for journal_name, path in encrypted_journals.items():
|
||||||
util.prompt("Enter password for {} journal (stored in {}).".format(journal, path))
|
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
|
||||||
util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd))
|
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.prompt(" Done.")
|
||||||
|
|
||||||
with open(config_path + ".backup", 'w') as config_backup:
|
for journal_name, path in plain_journals.items():
|
||||||
config_backup.write(config_file)
|
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.
|
util.prompt("\nUpgrading config...")
|
||||||
We're all done here and you can start enjoying jrnl 2.""".format(config_path))
|
backup(config_path)
|
||||||
|
|
||||||
|
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))
|
||||||
|
|
13
jrnl/util.py
13
jrnl/util.py
|
@ -75,10 +75,17 @@ def u(s):
|
||||||
|
|
||||||
|
|
||||||
def py2encode(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
|
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):
|
def prnt(s):
|
||||||
"""Encode and print a string"""
|
"""Encode and print a string"""
|
||||||
STDOUT.write(u(s + "\n"))
|
STDOUT.write(u(s + "\n"))
|
||||||
|
@ -92,7 +99,7 @@ def prompt(msg):
|
||||||
|
|
||||||
|
|
||||||
def py23_input(msg=""):
|
def py23_input(msg=""):
|
||||||
STDERR.write(u(msg))
|
prompt(msg)
|
||||||
return STDIN.readline().strip()
|
return STDIN.readline().strip()
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,7 +121,7 @@ def load_config(config_path):
|
||||||
|
|
||||||
|
|
||||||
def get_text_from_editor(config, template=""):
|
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:
|
with codecs.open(tmpfile, 'w', "utf-8") as f:
|
||||||
if template:
|
if template:
|
||||||
f.write(template)
|
f.write(template)
|
||||||
|
|
18
setup.py
18
setup.py
|
@ -75,11 +75,11 @@ conditional_dependencies = {
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = "jrnl",
|
name="jrnl",
|
||||||
version = get_version(),
|
version=get_version(),
|
||||||
description = "A command line journal application that stores your journal in a plain text file",
|
description="A command line journal application that stores your journal in a plain text file",
|
||||||
packages = ['jrnl'],
|
packages=['jrnl'],
|
||||||
install_requires = [
|
install_requires=[
|
||||||
"pyxdg>=0.19",
|
"pyxdg>=0.19",
|
||||||
"parsedatetime>=1.2",
|
"parsedatetime>=1.2",
|
||||||
"pytz>=2013b",
|
"pytz>=2013b",
|
||||||
|
@ -115,9 +115,9 @@ setup(
|
||||||
'Topic :: Text Processing'
|
'Topic :: Text Processing'
|
||||||
],
|
],
|
||||||
# metadata for upload to PyPI
|
# metadata for upload to PyPI
|
||||||
author = "Manuel Ebert",
|
author="Manuel Ebert",
|
||||||
author_email = "manuel@1450.me",
|
author_email="manuel@1450.me",
|
||||||
license="LICENSE",
|
license="LICENSE",
|
||||||
keywords = "journal todo todo.txt jrnl".split(),
|
keywords="journal todo todo.txt jrnl".split(),
|
||||||
url = "http://www.jrnl.sh",
|
url="http://www.jrnl.sh",
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue