mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
(#770) run black formatter on codebase for standardization
This commit is contained in:
parent
9664924096
commit
46c4c88231
24 changed files with 850 additions and 427 deletions
|
@ -5,8 +5,11 @@ from jrnl import cli, install, Journal, util, plugins
|
|||
from jrnl import __version__
|
||||
from dateutil import parser as date_parser
|
||||
from collections import defaultdict
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
|
@ -17,7 +20,7 @@ import shlex
|
|||
import sys
|
||||
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
|
@ -44,23 +47,25 @@ keyring.set_keyring(TestKeyring())
|
|||
def ushlex(command):
|
||||
if sys.version_info[0] == 3:
|
||||
return shlex.split(command)
|
||||
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
|
||||
return map(lambda s: s.decode("UTF8"), shlex.split(command.encode("utf8")))
|
||||
|
||||
|
||||
def read_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
with open(config['journals'][journal_name]) as journal_file:
|
||||
with open(config["journals"][journal_name]) as journal_file:
|
||||
journal = journal_file.read()
|
||||
return journal
|
||||
|
||||
|
||||
def open_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
journal_conf = config['journals'][journal_name]
|
||||
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
||||
journal_conf = config["journals"][journal_name]
|
||||
if (
|
||||
type(journal_conf) is dict
|
||||
): # We can override the default config on a by-journal basis
|
||||
config.update(journal_conf)
|
||||
else: # But also just give them a string to point to the journal file
|
||||
config['journal'] = journal_conf
|
||||
config["journal"] = journal_conf
|
||||
return Journal.open_journal(journal_name, config)
|
||||
|
||||
|
||||
|
@ -70,14 +75,15 @@ def set_config(context, config_file):
|
|||
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
|
||||
if config_file.endswith("yaml"):
|
||||
# Add jrnl version to file for 2.x journals
|
||||
with open(install.CONFIG_FILE_PATH, 'a') as cf:
|
||||
with open(install.CONFIG_FILE_PATH, "a") as cf:
|
||||
cf.write("version: {}".format(__version__))
|
||||
|
||||
|
||||
@when('we open the editor and enter ""')
|
||||
@when('we open the editor and enter "{text}"')
|
||||
def open_editor_and_enter(context, text=""):
|
||||
text = (text or context.text)
|
||||
text = text or context.text
|
||||
|
||||
def _mock_editor_function(command):
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, "w+") as f:
|
||||
|
@ -88,7 +94,7 @@ def open_editor_and_enter(context, text=""):
|
|||
|
||||
return tmpfile
|
||||
|
||||
with patch('subprocess.call', side_effect=_mock_editor_function):
|
||||
with patch("subprocess.call", side_effect=_mock_editor_function):
|
||||
run(context, "jrnl")
|
||||
|
||||
|
||||
|
@ -96,6 +102,7 @@ def _mock_getpass(inputs):
|
|||
def prompt_return(prompt="Password: "):
|
||||
print(prompt)
|
||||
return next(inputs)
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
|
@ -104,6 +111,7 @@ def _mock_input(inputs):
|
|||
val = next(inputs)
|
||||
print(prompt, val)
|
||||
return val
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
|
@ -119,24 +127,28 @@ def run_with_input(context, command, inputs=""):
|
|||
text = iter([inputs])
|
||||
|
||||
args = ushlex(command)[1:]
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input,\
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass,\
|
||||
|
||||
# fmt: off
|
||||
# see: https://github.com/psf/black/issues/557
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass, \
|
||||
patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
# all inputs were used
|
||||
try:
|
||||
next(text)
|
||||
assert False, "Not all inputs were consumed"
|
||||
except StopIteration:
|
||||
pass
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
# all inputs were used
|
||||
try:
|
||||
next(text)
|
||||
assert False, "Not all inputs were consumed"
|
||||
except StopIteration:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
@when('we run "{command}"')
|
||||
|
@ -158,20 +170,20 @@ def load_template(context, filename):
|
|||
|
||||
@when('we set the keychain password of "{journal}" to "{password}"')
|
||||
def set_keychain(context, journal, password):
|
||||
keyring.set_password('jrnl', journal, password)
|
||||
keyring.set_password("jrnl", journal, password)
|
||||
|
||||
|
||||
@then('we should get an error')
|
||||
@then("we should get an error")
|
||||
def has_error(context):
|
||||
assert context.exit_status != 0, context.exit_status
|
||||
|
||||
|
||||
@then('we should get no error')
|
||||
@then("we should get no error")
|
||||
def no_error(context):
|
||||
assert context.exit_status == 0, context.exit_status
|
||||
|
||||
|
||||
@then('the output should be parsable as json')
|
||||
@then("the output should be parsable as json")
|
||||
def check_output_json(context):
|
||||
out = context.stdout_capture.getvalue()
|
||||
assert json.loads(out), out
|
||||
|
@ -210,7 +222,7 @@ def check_json_output_path(context, path, value):
|
|||
out = context.stdout_capture.getvalue()
|
||||
struct = json.loads(out)
|
||||
|
||||
for node in path.split('.'):
|
||||
for node in path.split("."):
|
||||
try:
|
||||
struct = struct[int(node)]
|
||||
except ValueError:
|
||||
|
@ -218,14 +230,19 @@ def check_json_output_path(context, path, value):
|
|||
assert struct == value, struct
|
||||
|
||||
|
||||
@then('the output should be')
|
||||
@then("the output should be")
|
||||
@then('the output should be "{text}"')
|
||||
def check_output(context, text=None):
|
||||
text = (text or context.text).strip().splitlines()
|
||||
out = context.stdout_capture.getvalue().strip().splitlines()
|
||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
|
||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(
|
||||
len(out), len(text)
|
||||
)
|
||||
for line_text, line_out in zip(text, out):
|
||||
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
|
||||
assert line_text.strip() == line_out.strip(), [
|
||||
line_text.strip(),
|
||||
line_out.strip(),
|
||||
]
|
||||
|
||||
|
||||
@then('the output should contain "{text}" in the local time')
|
||||
|
@ -233,11 +250,11 @@ def check_output_time_inline(context, text):
|
|||
out = context.stdout_capture.getvalue()
|
||||
local_tz = tzlocal.get_localzone()
|
||||
date, flag = CALENDAR.parse(text)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M",date)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M", date)
|
||||
assert output_date in out, output_date
|
||||
|
||||
|
||||
@then('the output should contain')
|
||||
@then("the output should contain")
|
||||
@then('the output should contain "{text}"')
|
||||
def check_output_inline(context, text=None):
|
||||
text = text or context.text
|
||||
|
@ -274,7 +291,7 @@ def check_journal_content(context, text, journal_name="default"):
|
|||
def journal_doesnt_exist(context, journal_name="default"):
|
||||
with open(install.CONFIG_FILE_PATH) as config_file:
|
||||
config = yaml.load(config_file, Loader=yaml.FullLoader)
|
||||
journal_path = config['journals'][journal_name]
|
||||
journal_path = config["journals"][journal_name]
|
||||
assert not os.path.exists(journal_path)
|
||||
|
||||
|
||||
|
@ -282,11 +299,7 @@ def journal_doesnt_exist(context, journal_name="default"):
|
|||
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
|
||||
def config_var(context, key, value, journal=None):
|
||||
t, value = value.split(":")
|
||||
value = {
|
||||
"bool": lambda v: v.lower() == "true",
|
||||
"int": int,
|
||||
"str": str
|
||||
}[t](value)
|
||||
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
if journal:
|
||||
config = config["journals"][journal]
|
||||
|
@ -294,8 +307,8 @@ def config_var(context, key, value, journal=None):
|
|||
assert config[key] == value
|
||||
|
||||
|
||||
@then('the journal should have {number:d} entries')
|
||||
@then('the journal should have {number:d} entry')
|
||||
@then("the journal should have {number:d} entries")
|
||||
@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_entries(context, number, journal_name="default"):
|
||||
|
@ -303,6 +316,6 @@ def check_journal_entries(context, number, journal_name="default"):
|
|||
assert len(journal.entries) == number
|
||||
|
||||
|
||||
@then('fail')
|
||||
@then("fail")
|
||||
def debug_fail(context):
|
||||
assert False
|
||||
|
|
|
@ -19,7 +19,11 @@ class DayOne(Journal.Journal):
|
|||
"""A special Journal handling DayOne files"""
|
||||
|
||||
# InvalidFileException was added to plistlib in Python3.4
|
||||
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
|
||||
PLIST_EXCEPTIONS = (
|
||||
(ExpatError, plistlib.InvalidFileException)
|
||||
if hasattr(plistlib, "InvalidFileException")
|
||||
else ExpatError
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
|
@ -27,28 +31,39 @@ class DayOne(Journal.Journal):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def open(self):
|
||||
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
||||
filenames = [
|
||||
os.path.join(self.config["journal"], "entries", f)
|
||||
for f in os.listdir(os.path.join(self.config["journal"], "entries"))
|
||||
]
|
||||
filenames = []
|
||||
for root, dirnames, f in os.walk(self.config['journal']):
|
||||
for filename in fnmatch.filter(f, '*.doentry'):
|
||||
for root, dirnames, f in os.walk(self.config["journal"]):
|
||||
for filename in fnmatch.filter(f, "*.doentry"):
|
||||
filenames.append(os.path.join(root, filename))
|
||||
self.entries = []
|
||||
for filename in filenames:
|
||||
with open(filename, 'rb') as plist_entry:
|
||||
with open(filename, "rb") as plist_entry:
|
||||
try:
|
||||
dict_entry = plistlib.readPlist(plist_entry)
|
||||
except self.PLIST_EXCEPTIONS:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
timezone = pytz.timezone(dict_entry['Time Zone'])
|
||||
timezone = pytz.timezone(dict_entry["Time Zone"])
|
||||
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
||||
timezone = tzlocal.get_localzone()
|
||||
date = dict_entry['Creation Date']
|
||||
date = dict_entry["Creation Date"]
|
||||
date = date + timezone.utcoffset(date, is_dst=False)
|
||||
entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"])
|
||||
entry = Entry.Entry(
|
||||
self,
|
||||
date,
|
||||
text=dict_entry["Entry Text"],
|
||||
starred=dict_entry["Starred"],
|
||||
)
|
||||
entry.uuid = dict_entry["UUID"]
|
||||
entry._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])]
|
||||
entry._tags = [
|
||||
self.config["tagsymbols"][0] + tag.lower()
|
||||
for tag in dict_entry.get("Tags", [])
|
||||
]
|
||||
|
||||
self.entries.append(entry)
|
||||
self.sort()
|
||||
|
@ -58,24 +73,33 @@ class DayOne(Journal.Journal):
|
|||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
if entry.modified:
|
||||
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
||||
utc_time = datetime.utcfromtimestamp(
|
||||
time.mktime(entry.date.timetuple())
|
||||
)
|
||||
|
||||
if not hasattr(entry, "uuid"):
|
||||
entry.uuid = uuid.uuid1().hex
|
||||
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
|
||||
filename = os.path.join(
|
||||
self.config["journal"], "entries", entry.uuid.upper() + ".doentry"
|
||||
)
|
||||
|
||||
entry_plist = {
|
||||
'Creation Date': utc_time,
|
||||
'Starred': entry.starred if hasattr(entry, 'starred') else False,
|
||||
'Entry Text': entry.title + "\n" + entry.body,
|
||||
'Time Zone': str(tzlocal.get_localzone()),
|
||||
'UUID': entry.uuid.upper(),
|
||||
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
|
||||
"Creation Date": utc_time,
|
||||
"Starred": entry.starred if hasattr(entry, "starred") else False,
|
||||
"Entry Text": entry.title + "\n" + entry.body,
|
||||
"Time Zone": str(tzlocal.get_localzone()),
|
||||
"UUID": entry.uuid.upper(),
|
||||
"Tags": [
|
||||
tag.strip(self.config["tagsymbols"]).replace("_", " ")
|
||||
for tag in entry.tags
|
||||
],
|
||||
}
|
||||
plistlib.writePlist(entry_plist, filename)
|
||||
for entry in self._deleted_entries:
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
|
||||
filename = os.path.join(
|
||||
self.config["journal"], "entries", entry.uuid + ".doentry"
|
||||
)
|
||||
os.remove(filename)
|
||||
|
||||
def editable_str(self):
|
||||
|
@ -113,7 +137,7 @@ class DayOne(Journal.Journal):
|
|||
if line.endswith("*"):
|
||||
current_entry.starred = True
|
||||
line = line[:-1]
|
||||
current_entry.title = line[len(date_blob) - 1:]
|
||||
current_entry.title = line[len(date_blob) - 1 :]
|
||||
current_entry.date = new_date
|
||||
elif current_entry:
|
||||
current_entry.body += line + "\n"
|
||||
|
|
|
@ -22,29 +22,32 @@ def make_key(password):
|
|||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
# Salt is hard-coded
|
||||
salt=b'\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()
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = kdf.derive(password)
|
||||
return base64.urlsafe_b64encode(key)
|
||||
|
||||
|
||||
class EncryptedJournal(Journal):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
def open(self, filename=None):
|
||||
"""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']
|
||||
filename = filename or self.config["journal"]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
self.create_file(filename)
|
||||
self.password = util.create_password(self.name)
|
||||
print(f"Encrypted journal '{self.name}' created at {filename}", file=sys.stderr)
|
||||
print(
|
||||
f"Encrypted journal '{self.name}' created at {filename}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
|
@ -58,13 +61,13 @@ class EncryptedJournal(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, 'rb') as f:
|
||||
with open(filename, "rb") as f:
|
||||
journal_encrypted = f.read()
|
||||
|
||||
def decrypt_journal(password):
|
||||
key = make_key(password)
|
||||
try:
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8")
|
||||
self.password = password
|
||||
return plain
|
||||
except (InvalidToken, IndexError):
|
||||
|
@ -77,45 +80,53 @@ class EncryptedJournal(Journal):
|
|||
|
||||
def _store(self, filename, text):
|
||||
key = make_key(self.password)
|
||||
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
||||
with open(filename, 'wb') as f:
|
||||
journal = Fernet(key).encrypt(text.encode("utf-8"))
|
||||
with open(filename, "wb") as f:
|
||||
f.write(journal)
|
||||
|
||||
@classmethod
|
||||
def from_journal(cls, other: Journal):
|
||||
new_journal = super().from_journal(other)
|
||||
new_journal.password = other.password if hasattr(other, "password") else util.create_password(other.name)
|
||||
new_journal.password = (
|
||||
other.password
|
||||
if hasattr(other, "password")
|
||||
else util.create_password(other.name)
|
||||
)
|
||||
return new_journal
|
||||
|
||||
|
||||
class LegacyEncryptedJournal(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):
|
||||
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, 'rb') as f:
|
||||
with open(filename, "rb") as f:
|
||||
journal_encrypted = f.read()
|
||||
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
||||
|
||||
def decrypt_journal(password):
|
||||
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
|
||||
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
|
||||
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.password = password
|
||||
if plain_padded[-1] in (" ", 32):
|
||||
# Ancient versions of jrnl. Do not judge me.
|
||||
return plain_padded.decode('utf-8').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 self.password:
|
||||
return decrypt_journal(self.password)
|
||||
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
|
|
@ -49,50 +49,60 @@ class Entry:
|
|||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)'
|
||||
pattern = fr"(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)"
|
||||
return re.compile(pattern)
|
||||
|
||||
def _parse_tags(self):
|
||||
tagsymbols = self.journal.config['tagsymbols']
|
||||
return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)}
|
||||
tagsymbols = self.journal.config["tagsymbols"]
|
||||
return {
|
||||
tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""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 = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||
if self.starred:
|
||||
title += " *"
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if self.body.rstrip("\n ") else "",
|
||||
body=self.body.rstrip("\n ")
|
||||
body=self.body.rstrip("\n "),
|
||||
)
|
||||
|
||||
def pprint(self, short=False):
|
||||
"""Returns a pretty-printed version of the entry.
|
||||
If short is true, only print the title."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
if self.journal.config['indent_character']:
|
||||
indent = self.journal.config['indent_character'].rstrip() + " "
|
||||
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||
if self.journal.config["indent_character"]:
|
||||
indent = self.journal.config["indent_character"].rstrip() + " "
|
||||
else:
|
||||
indent = ""
|
||||
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,
|
||||
self.journal.config['linewrap'],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True) or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
])
|
||||
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,
|
||||
self.journal.config["linewrap"],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True,
|
||||
)
|
||||
or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
]
|
||||
)
|
||||
else:
|
||||
title = date_str + " " + self.title.rstrip("\n ")
|
||||
body = self.body.rstrip("\n ")
|
||||
|
||||
# Suppress bodies that are just blanks and new lines.
|
||||
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
|
||||
has_body = len(self.body) > 20 or not all(
|
||||
char in (" ", "\n") for char in self.body
|
||||
)
|
||||
|
||||
if short:
|
||||
return title
|
||||
|
@ -104,17 +114,21 @@ class Entry:
|
|||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Entry '{}' on {}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
return "<Entry '{}' on {}>".format(
|
||||
self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Entry) \
|
||||
or self.title.strip() != other.title.strip() \
|
||||
or self.body.rstrip() != other.body.rstrip() \
|
||||
or self.date != other.date \
|
||||
or self.starred != other.starred:
|
||||
if (
|
||||
not isinstance(other, Entry)
|
||||
or self.title.strip() != other.title.strip()
|
||||
or self.body.rstrip() != other.body.rstrip()
|
||||
or self.date != other.date
|
||||
or self.starred != other.starred
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
115
jrnl/Journal.py
115
jrnl/Journal.py
|
@ -25,17 +25,17 @@ class Tag:
|
|||
|
||||
|
||||
class Journal:
|
||||
def __init__(self, name='default', **kwargs):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
self.config = {
|
||||
'journal': "journal.txt",
|
||||
'encrypt': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 80,
|
||||
'indent_character': '|',
|
||||
"journal": "journal.txt",
|
||||
"encrypt": False,
|
||||
"default_hour": 9,
|
||||
"default_minute": 0,
|
||||
"timeformat": "%Y-%m-%d %H:%M",
|
||||
"tagsymbols": "@",
|
||||
"highlight": True,
|
||||
"linewrap": 80,
|
||||
"indent_character": "|",
|
||||
}
|
||||
self.config.update(kwargs)
|
||||
# Set up date parser
|
||||
|
@ -57,17 +57,24 @@ class Journal:
|
|||
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__)
|
||||
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.entries = list(
|
||||
frozenset(self.entries) | frozenset(self._parse(other_journal_txt))
|
||||
)
|
||||
self.sort()
|
||||
|
||||
def open(self, filename=None):
|
||||
"""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']
|
||||
filename = filename or self.config["journal"]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
self.create_file(filename)
|
||||
|
@ -81,7 +88,7 @@ class Journal:
|
|||
|
||||
def write(self, filename=None):
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
filename = filename or self.config['journal']
|
||||
filename = filename or self.config["journal"]
|
||||
text = self._to_text()
|
||||
self._store(filename, text)
|
||||
|
||||
|
@ -129,7 +136,7 @@ class Journal:
|
|||
|
||||
if new_date:
|
||||
if entries:
|
||||
entries[-1].text = journal_txt[last_entry_pos:match.start()]
|
||||
entries[-1].text = journal_txt[last_entry_pos : match.start()]
|
||||
last_entry_pos = match.end()
|
||||
entries.append(Entry.Entry(self, date=new_date))
|
||||
|
||||
|
@ -148,18 +155,16 @@ class Journal:
|
|||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
||||
if self.config['highlight']: # highlight tags
|
||||
if self.config["highlight"]: # highlight tags
|
||||
if self.search_tags:
|
||||
for tag in self.search_tags:
|
||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre,
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp)
|
||||
pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), pp)
|
||||
else:
|
||||
pp = re.sub(
|
||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||
Entry.Entry.tag_regex(self.config["tagsymbols"]),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp
|
||||
pp,
|
||||
)
|
||||
return pp
|
||||
|
||||
|
@ -183,14 +188,22 @@ class Journal:
|
|||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
for entry in self.entries
|
||||
for tag in set(entry.tags)]
|
||||
tags = [tag for entry in self.entries for tag in set(entry.tags)]
|
||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
||||
|
||||
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, contains=None, exclude=[]):
|
||||
def filter(
|
||||
self,
|
||||
tags=[],
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
starred=False,
|
||||
strict=False,
|
||||
short=False,
|
||||
contains=None,
|
||||
exclude=[],
|
||||
):
|
||||
"""Removes all entries from the journal that don't match the filter.
|
||||
|
||||
tags is a list of tags, each being a string that starts with one of the
|
||||
|
@ -216,13 +229,20 @@ class Journal:
|
|||
contains_lower = contains.casefold()
|
||||
|
||||
result = [
|
||||
entry for entry in self.entries
|
||||
entry
|
||||
for entry in self.entries
|
||||
if (not tags or tagged(entry.tags))
|
||||
and (not starred or entry.starred)
|
||||
and (not start_date or entry.date >= start_date)
|
||||
and (not end_date or entry.date <= end_date)
|
||||
and (not exclude or not excluded(entry.tags))
|
||||
and (not contains or (contains_lower in entry.title.casefold() or contains_lower in entry.body.casefold()))
|
||||
and (
|
||||
not contains
|
||||
or (
|
||||
contains_lower in entry.title.casefold()
|
||||
or contains_lower in entry.body.casefold()
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
self.entries = result
|
||||
|
@ -231,11 +251,11 @@ class Journal:
|
|||
"""Constructs a new entry from some raw text input.
|
||||
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
|
||||
|
||||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||
raw = raw.replace("\\n ", "\n").replace("\\n", "\n")
|
||||
starred = False
|
||||
# Split raw text into title and body
|
||||
sep = re.search(r"\n|[?!.]+ +\n?", raw)
|
||||
first_line = raw[:sep.end()].strip() if sep else raw
|
||||
first_line = raw[: sep.end()].strip() if sep else raw
|
||||
starred = False
|
||||
|
||||
if not date:
|
||||
|
@ -243,12 +263,12 @@ class Journal:
|
|||
if colon_pos > 0:
|
||||
date = time.parse(
|
||||
raw[:colon_pos],
|
||||
default_hour=self.config['default_hour'],
|
||||
default_minute=self.config['default_minute']
|
||||
default_hour=self.config["default_hour"],
|
||||
default_minute=self.config["default_minute"],
|
||||
)
|
||||
if date: # Parsed successfully, strip that from the raw text
|
||||
starred = raw[:colon_pos].strip().endswith("*")
|
||||
raw = raw[colon_pos + 1:].strip()
|
||||
raw = raw[colon_pos + 1 :].strip()
|
||||
starred = starred or first_line.startswith("*") or first_line.endswith("*")
|
||||
if not date: # Still nothing? Meh, just live in the moment.
|
||||
date = time.parse("now")
|
||||
|
@ -281,7 +301,7 @@ class PlainJournal(Journal):
|
|||
return f.read()
|
||||
|
||||
def _store(self, filename, text):
|
||||
with open(filename, 'w', encoding="utf-8") as f:
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
|
@ -289,6 +309,7 @@ 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. You'll not be able to save these journals anymore."""
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
@ -297,17 +318,19 @@ class LegacyJournal(Journal):
|
|||
"""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']))
|
||||
date_length = len(datetime.today().strftime(self.config["timeformat"]))
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
current_entry = None
|
||||
new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)')
|
||||
new_date_format_regex = re.compile(r"(^\[[^\]]+\].*?$)")
|
||||
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'])
|
||||
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:
|
||||
|
@ -319,12 +342,14 @@ class LegacyJournal(Journal):
|
|||
else:
|
||||
starred = False
|
||||
|
||||
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
|
||||
current_entry = Entry.Entry(
|
||||
self, date=new_date, text=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 (after some
|
||||
# escaping for the new format).
|
||||
line = new_date_format_regex.sub(r' \1', line)
|
||||
line = new_date_format_regex.sub(r" \1", line)
|
||||
if current_entry:
|
||||
current_entry.text += line + "\n"
|
||||
|
||||
|
@ -343,26 +368,30 @@ def open_journal(name, config, legacy=False):
|
|||
backwards compatibility with jrnl 1.x
|
||||
"""
|
||||
config = config.copy()
|
||||
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
|
||||
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']):
|
||||
if os.path.isdir(config["journal"]):
|
||||
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
||||
config["journal"]
|
||||
):
|
||||
from . import DayOneJournal
|
||||
|
||||
return DayOneJournal.DayOne(**config).open()
|
||||
else:
|
||||
print(
|
||||
f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.",
|
||||
file=sys.stderr
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
if not config['encrypt']:
|
||||
if not config["encrypt"]:
|
||||
if legacy:
|
||||
return LegacyJournal(name, **config).open()
|
||||
return PlainJournal(name, **config).open()
|
||||
else:
|
||||
from . import EncryptedJournal
|
||||
|
||||
if legacy:
|
||||
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
||||
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
from .__version__ import __version__
|
||||
except ImportError:
|
||||
|
|
296
jrnl/cli.py
296
jrnl/cli.py
|
@ -23,33 +23,155 @@ logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
|||
|
||||
def parse_args(args=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
|
||||
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
|
||||
parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
dest="version",
|
||||
action="store_true",
|
||||
help="prints version information and exits",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-ls", dest="ls", action="store_true", help="displays accessible journals"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--debug", dest="debug", action="store_true", help="execute in debug mode"
|
||||
)
|
||||
|
||||
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
|
||||
composing.add_argument('text', metavar='', nargs="*")
|
||||
composing = parser.add_argument_group(
|
||||
"Composing",
|
||||
'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."',
|
||||
)
|
||||
composing.add_argument("text", metavar="", nargs="*")
|
||||
|
||||
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
|
||||
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
|
||||
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
|
||||
reading.add_argument('-contains', dest='contains', help='View entries containing a specific string')
|
||||
reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date')
|
||||
reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)')
|
||||
reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries')
|
||||
reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
|
||||
reading.add_argument('-not', dest='excluded', nargs='+', default=[], metavar="E", help="Exclude entries with these tags")
|
||||
reading = parser.add_argument_group(
|
||||
"Reading",
|
||||
"Specifying either of these parameters will display posts of your journal",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-from", dest="start_date", metavar="DATE", help="View entries after this date"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-until",
|
||||
"-to",
|
||||
dest="end_date",
|
||||
metavar="DATE",
|
||||
help="View entries before this date",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-contains", dest="contains", help="View entries containing a specific string"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-on", dest="on_date", metavar="DATE", help="View entries on this date"
|
||||
)
|
||||
reading.add_argument(
|
||||
"-and",
|
||||
dest="strict",
|
||||
action="store_true",
|
||||
help="Filter by tags using AND (default: OR)",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-starred",
|
||||
dest="starred",
|
||||
action="store_true",
|
||||
help="Show only starred entries",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-n",
|
||||
dest="limit",
|
||||
default=None,
|
||||
metavar="N",
|
||||
help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.",
|
||||
nargs="?",
|
||||
type=int,
|
||||
)
|
||||
reading.add_argument(
|
||||
"-not",
|
||||
dest="excluded",
|
||||
nargs="+",
|
||||
default=[],
|
||||
metavar="E",
|
||||
help="Exclude entries with these tags",
|
||||
)
|
||||
|
||||
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
|
||||
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
|
||||
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
|
||||
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
|
||||
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
|
||||
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
|
||||
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None)
|
||||
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None)
|
||||
exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None)
|
||||
exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
|
||||
exporting = parser.add_argument_group(
|
||||
"Export / Import", "Options for transmogrifying your journal"
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-s",
|
||||
"--short",
|
||||
dest="short",
|
||||
action="store_true",
|
||||
help="Show only titles or line containing the search tags",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--tags",
|
||||
dest="tags",
|
||||
action="store_true",
|
||||
help="Returns a list of all tags and number of occurences",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--export",
|
||||
metavar="TYPE",
|
||||
dest="export",
|
||||
choices=plugins.EXPORT_FORMATS,
|
||||
help="Export your journal. TYPE can be {}.".format(
|
||||
plugins.util.oxford_list(plugins.EXPORT_FORMATS)
|
||||
),
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-o",
|
||||
metavar="OUTPUT",
|
||||
dest="output",
|
||||
help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--import",
|
||||
metavar="TYPE",
|
||||
dest="import_",
|
||||
choices=plugins.IMPORT_FORMATS,
|
||||
help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format(
|
||||
plugins.util.oxford_list(plugins.IMPORT_FORMATS)
|
||||
),
|
||||
default=False,
|
||||
const="jrnl",
|
||||
nargs="?",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"-i",
|
||||
metavar="INPUT",
|
||||
dest="input",
|
||||
help="Optionally specifies input file when using --import.",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--encrypt",
|
||||
metavar="FILENAME",
|
||||
dest="encrypt",
|
||||
help="Encrypts your existing journal with a new password",
|
||||
nargs="?",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--decrypt",
|
||||
metavar="FILENAME",
|
||||
dest="decrypt",
|
||||
help="Decrypts your journal and stores it in plain text",
|
||||
nargs="?",
|
||||
default=False,
|
||||
const=None,
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--edit",
|
||||
dest="edit",
|
||||
help="Opens your editor to edit the selected entries.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
@ -63,13 +185,30 @@ def guess_mode(args, config):
|
|||
compose = False
|
||||
export = False
|
||||
import_ = True
|
||||
elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)):
|
||||
elif (
|
||||
args.decrypt is not False
|
||||
or args.encrypt is not False
|
||||
or args.export is not False
|
||||
or any((args.short, args.tags, args.edit))
|
||||
):
|
||||
compose = False
|
||||
export = True
|
||||
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred, args.contains)):
|
||||
elif any(
|
||||
(
|
||||
args.start_date,
|
||||
args.end_date,
|
||||
args.on_date,
|
||||
args.limit,
|
||||
args.strict,
|
||||
args.starred,
|
||||
args.contains,
|
||||
)
|
||||
):
|
||||
# Any sign of displaying stuff?
|
||||
compose = False
|
||||
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
|
||||
elif args.text and all(
|
||||
word[0] in config["tagsymbols"] for word in " ".join(args.text).split()
|
||||
):
|
||||
# No date and only tags?
|
||||
compose = False
|
||||
|
||||
|
@ -78,29 +217,37 @@ def guess_mode(args, config):
|
|||
|
||||
def encrypt(journal, filename=None):
|
||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
journal.config['encrypt'] = True
|
||||
journal.config["encrypt"] = True
|
||||
|
||||
new_journal = EncryptedJournal.from_journal(journal)
|
||||
new_journal.write(filename)
|
||||
|
||||
print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
print(
|
||||
"Journal encrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def decrypt(journal, filename=None):
|
||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
journal.config['encrypt'] = False
|
||||
journal.config["encrypt"] = False
|
||||
|
||||
new_journal = PlainJournal.from_journal(journal)
|
||||
new_journal.write(filename)
|
||||
print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
print(
|
||||
"Journal decrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
"""List the journals specified in the configuration file"""
|
||||
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
|
||||
ml = min(max(len(k) for k in config['journals']), 20)
|
||||
for journal, cfg in config['journals'].items():
|
||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
||||
ml = min(max(len(k) for k in config["journals"]), 20)
|
||||
for journal, cfg in config["journals"].items():
|
||||
result += " * {:{}} -> {}\n".format(
|
||||
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -108,11 +255,11 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
"""Updates a config dict with new values - either global if scope is None
|
||||
or config['journals'][scope] is just a string pointing to a journal file,
|
||||
or within the scope"""
|
||||
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
||||
config['journals'][scope].update(new_config)
|
||||
if scope and type(config["journals"][scope]) is dict: # Update to journal specific
|
||||
config["journals"][scope].update(new_config)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config['journals'][scope] = {"journal": config['journals'][scope]}
|
||||
config['journals'][scope].update(new_config)
|
||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||
config["journals"][scope].update(new_config)
|
||||
else:
|
||||
config.update(new_config)
|
||||
|
||||
|
@ -120,9 +267,11 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
def configure_logger(debug=False):
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
format='%(levelname)-8s %(name)-12s %(message)s'
|
||||
format="%(levelname)-8s %(name)-12s %(message)s",
|
||||
)
|
||||
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
|
||||
logging.getLogger("parsedatetime").setLevel(
|
||||
logging.INFO
|
||||
) # disable parsedatetime debug logging
|
||||
|
||||
|
||||
def run(manual_args=None):
|
||||
|
@ -150,10 +299,10 @@ def run(manual_args=None):
|
|||
# use this!
|
||||
|
||||
journal_name = install.DEFAULT_JOURNAL_KEY
|
||||
if args.text and args.text[0] in config['journals']:
|
||||
if args.text and args.text[0] in config["journals"]:
|
||||
journal_name = args.text[0]
|
||||
args.text = args.text[1:]
|
||||
elif install.DEFAULT_JOURNAL_KEY not in config['journals']:
|
||||
elif install.DEFAULT_JOURNAL_KEY not in config["journals"]:
|
||||
print("No default journal configured.", file=sys.stderr)
|
||||
print(list_journals(config), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
@ -181,18 +330,24 @@ def run(manual_args=None):
|
|||
if not sys.stdin.isatty():
|
||||
# Piping data into jrnl
|
||||
raw = sys.stdin.read()
|
||||
elif config['editor']:
|
||||
elif config["editor"]:
|
||||
template = ""
|
||||
if config['template']:
|
||||
if config["template"]:
|
||||
try:
|
||||
template = open(config['template']).read()
|
||||
template = open(config["template"]).read()
|
||||
except OSError:
|
||||
print(f"[Could not read template at '{config['template']}']", file=sys.stderr)
|
||||
print(
|
||||
f"[Could not read template at '{config['template']}']",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
raw = util.get_text_from_editor(config, template)
|
||||
else:
|
||||
try:
|
||||
print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr)
|
||||
print(
|
||||
"[Compose Entry; " + _exit_multiline_code + " to finish writing]\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raw = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
print("[Entry NOT saved to journal.]", file=sys.stderr)
|
||||
|
@ -225,13 +380,16 @@ def run(manual_args=None):
|
|||
old_entries = journal.entries
|
||||
if args.on_date:
|
||||
args.start_date = args.end_date = args.on_date
|
||||
journal.filter(tags=args.text,
|
||||
start_date=args.start_date, end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
short=args.short,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded,
|
||||
contains=args.contains)
|
||||
journal.filter(
|
||||
tags=args.text,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
short=args.short,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded,
|
||||
contains=args.contains,
|
||||
)
|
||||
journal.limit(args.limit)
|
||||
|
||||
# Reading mode
|
||||
|
@ -253,20 +411,28 @@ def run(manual_args=None):
|
|||
encrypt(journal, filename=args.encrypt)
|
||||
# Not encrypting to a separate file: update config!
|
||||
if not args.encrypt:
|
||||
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
|
||||
update_config(
|
||||
original_config, {"encrypt": True}, journal_name, force_local=True
|
||||
)
|
||||
install.save_config(original_config)
|
||||
|
||||
elif args.decrypt is not False:
|
||||
decrypt(journal, filename=args.decrypt)
|
||||
# Not decrypting to a separate file: update config!
|
||||
if not args.decrypt:
|
||||
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
||||
update_config(
|
||||
original_config, {"encrypt": False}, journal_name, force_local=True
|
||||
)
|
||||
install.save_config(original_config)
|
||||
|
||||
elif args.edit:
|
||||
if not config['editor']:
|
||||
print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]"
|
||||
.format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||
if not config["editor"]:
|
||||
print(
|
||||
"[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(
|
||||
install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
# Edit
|
||||
|
@ -277,9 +443,17 @@ def run(manual_args=None):
|
|||
num_edited = len([e for e in journal.entries if e.modified])
|
||||
prompts = []
|
||||
if num_deleted:
|
||||
prompts.append("{} {} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
||||
prompts.append(
|
||||
"{} {} deleted".format(
|
||||
num_deleted, "entry" if num_deleted == 1 else "entries"
|
||||
)
|
||||
)
|
||||
if num_edited:
|
||||
prompts.append("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
||||
prompts.append(
|
||||
"{} {} modified".format(
|
||||
num_edited, "entry" if num_deleted == 1 else "entries"
|
||||
)
|
||||
)
|
||||
if prompts:
|
||||
print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
|
||||
journal.entries += other_entries
|
||||
|
|
|
@ -8,6 +8,7 @@ import os
|
|||
|
||||
class Exporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
def __init__(self, format):
|
||||
with open("jrnl/templates/" + format + ".template") as f:
|
||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
||||
|
@ -18,11 +19,7 @@ class Exporter:
|
|||
return str(entry)
|
||||
|
||||
def _get_vars(self, journal):
|
||||
return {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
'tags': journal.tags
|
||||
}
|
||||
return {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
||||
|
||||
def export_journal(self, journal):
|
||||
"""Returns a string representation of an entire journal."""
|
||||
|
@ -38,7 +35,9 @@ class Exporter:
|
|||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
|
||||
def make_filename(self, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension))
|
||||
return entry.date.strftime(
|
||||
"%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension)
|
||||
)
|
||||
|
||||
def write_files(self, journal, path):
|
||||
"""Exports a journal into individual files for each entry."""
|
||||
|
@ -57,7 +56,7 @@ class Exporter:
|
|||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return self.write_files(journal, output)
|
||||
elif output: # single file
|
||||
elif output: # single file
|
||||
return self.write_file(journal, output)
|
||||
else:
|
||||
return self.export_journal(journal)
|
||||
|
|
|
@ -13,16 +13,17 @@ from .util import UserAbort
|
|||
import yaml
|
||||
import logging
|
||||
import sys
|
||||
|
||||
if "win32" not in sys.platform:
|
||||
# readline is not included in Windows Active Python
|
||||
import readline
|
||||
|
||||
DEFAULT_CONFIG_NAME = 'jrnl.yaml'
|
||||
DEFAULT_JOURNAL_NAME = 'journal.txt'
|
||||
DEFAULT_JOURNAL_KEY = 'default'
|
||||
XDG_RESOURCE = 'jrnl'
|
||||
DEFAULT_CONFIG_NAME = "jrnl.yaml"
|
||||
DEFAULT_JOURNAL_NAME = "journal.txt"
|
||||
DEFAULT_JOURNAL_KEY = "default"
|
||||
XDG_RESOURCE = "jrnl"
|
||||
|
||||
USER_HOME = os.path.expanduser('~')
|
||||
USER_HOME = os.path.expanduser("~")
|
||||
|
||||
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
|
||||
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
|
||||
|
@ -43,21 +44,20 @@ def module_exists(module_name):
|
|||
else:
|
||||
return True
|
||||
|
||||
|
||||
default_config = {
|
||||
'version': __version__,
|
||||
'journals': {
|
||||
DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH
|
||||
},
|
||||
'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "",
|
||||
'encrypt': False,
|
||||
'template': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 79,
|
||||
'indent_character': '|',
|
||||
"version": __version__,
|
||||
"journals": {DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH},
|
||||
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
|
||||
"encrypt": False,
|
||||
"template": False,
|
||||
"default_hour": 9,
|
||||
"default_minute": 0,
|
||||
"timeformat": "%Y-%m-%d %H:%M",
|
||||
"tagsymbols": "@",
|
||||
"highlight": True,
|
||||
"linewrap": 79,
|
||||
"indent_character": "|",
|
||||
}
|
||||
|
||||
|
||||
|
@ -70,13 +70,18 @@ def upgrade_config(config):
|
|||
for key in missing_keys:
|
||||
config[key] = default_config[key]
|
||||
save_config(config)
|
||||
print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr)
|
||||
print(
|
||||
f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def save_config(config):
|
||||
config['version'] = __version__
|
||||
with open(CONFIG_FILE_PATH, 'w') as f:
|
||||
yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False)
|
||||
config["version"] = __version__
|
||||
with open(CONFIG_FILE_PATH, "w") as f:
|
||||
yaml.safe_dump(
|
||||
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
|
||||
)
|
||||
|
||||
|
||||
def load_or_install_jrnl():
|
||||
|
@ -84,17 +89,27 @@ def load_or_install_jrnl():
|
|||
If jrnl is already installed, loads and returns a config object.
|
||||
Else, perform various prompts to install jrnl.
|
||||
"""
|
||||
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
|
||||
config_path = (
|
||||
CONFIG_FILE_PATH
|
||||
if os.path.exists(CONFIG_FILE_PATH)
|
||||
else CONFIG_FILE_PATH_FALLBACK
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
log.debug('Reading configuration from file %s', config_path)
|
||||
log.debug("Reading configuration from file %s", config_path)
|
||||
config = util.load_config(config_path)
|
||||
|
||||
try:
|
||||
upgrade.upgrade_jrnl_if_necessary(config_path)
|
||||
except upgrade.UpgradeValidationException:
|
||||
print("Aborting upgrade.", file=sys.stderr)
|
||||
print("Please tell us about this problem at the following URL:", file=sys.stderr)
|
||||
print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
|
||||
print(
|
||||
"Please tell us about this problem at the following URL:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Exiting.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
@ -102,7 +117,7 @@ def load_or_install_jrnl():
|
|||
|
||||
return config
|
||||
else:
|
||||
log.debug('Configuration file not found, installing jrnl...')
|
||||
log.debug("Configuration file not found, installing jrnl...")
|
||||
try:
|
||||
config = install()
|
||||
except KeyboardInterrupt:
|
||||
|
@ -112,25 +127,32 @@ def load_or_install_jrnl():
|
|||
|
||||
def install():
|
||||
if "win32" not in sys.platform:
|
||||
readline.set_completer_delims(' \t\n;')
|
||||
readline.set_completer_delims(" \t\n;")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(autocomplete)
|
||||
|
||||
# Where to create the journal?
|
||||
path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): '
|
||||
path_query = f"Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): "
|
||||
journal_path = input(path_query).strip() or JOURNAL_FILE_PATH
|
||||
default_config['journals'][DEFAULT_JOURNAL_KEY] = os.path.expanduser(os.path.expandvars(journal_path))
|
||||
default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser(
|
||||
os.path.expandvars(journal_path)
|
||||
)
|
||||
|
||||
path = os.path.split(default_config['journals'][DEFAULT_JOURNAL_KEY])[0] # If the folder doesn't exist, create it
|
||||
path = os.path.split(default_config["journals"][DEFAULT_JOURNAL_KEY])[
|
||||
0
|
||||
] # If the folder doesn't exist, create it
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Encrypt it?
|
||||
encrypt = util.yesno("Do you want to encrypt your journal? You can always change this later", default=False)
|
||||
encrypt = util.yesno(
|
||||
"Do you want to encrypt your journal? You can always change this later",
|
||||
default=False,
|
||||
)
|
||||
if encrypt:
|
||||
default_config['encrypt'] = True
|
||||
default_config["encrypt"] = True
|
||||
print("Journal will be encrypted.", file=sys.stderr)
|
||||
|
||||
save_config(default_config)
|
||||
|
@ -138,7 +160,7 @@ def install():
|
|||
|
||||
|
||||
def autocomplete(text, state):
|
||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*')
|
||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + "*")
|
||||
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
|
||||
expansions.append(None)
|
||||
return expansions[state]
|
||||
|
|
|
@ -11,8 +11,16 @@ from .yaml_exporter import YAMLExporter
|
|||
from .template_exporter import __all__ as template_exporters
|
||||
from .fancy_exporter import FancyExporter
|
||||
|
||||
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter, FancyExporter] + template_exporters
|
||||
__importers =[JRNLImporter]
|
||||
__exporters = [
|
||||
JSONExporter,
|
||||
MarkdownExporter,
|
||||
TagExporter,
|
||||
TextExporter,
|
||||
XMLExporter,
|
||||
YAMLExporter,
|
||||
FancyExporter,
|
||||
] + template_exporters
|
||||
__importers = [JRNLImporter]
|
||||
|
||||
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||
|
@ -20,6 +28,7 @@ __importer_types = {name: plugin for plugin in __importers for name in plugin.na
|
|||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
|
||||
|
||||
def get_exporter(format):
|
||||
for exporter in __exporters:
|
||||
if hasattr(exporter, "names") and format in exporter.names:
|
||||
|
|
|
@ -8,46 +8,64 @@ from textwrap import TextWrapper
|
|||
|
||||
class FancyExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
||||
|
||||
names = ["fancy", "boxed"]
|
||||
extension = "txt"
|
||||
|
||||
border_a="┎"
|
||||
border_b="─"
|
||||
border_c="╮"
|
||||
border_d="╘"
|
||||
border_e="═"
|
||||
border_f="╕"
|
||||
border_g="┃"
|
||||
border_h="│"
|
||||
border_i="┠"
|
||||
border_j="╌"
|
||||
border_k="┤"
|
||||
border_l="┖"
|
||||
border_m="┘"
|
||||
border_a = "┎"
|
||||
border_b = "─"
|
||||
border_c = "╮"
|
||||
border_d = "╘"
|
||||
border_e = "═"
|
||||
border_f = "╕"
|
||||
border_g = "┃"
|
||||
border_h = "│"
|
||||
border_i = "┠"
|
||||
border_j = "╌"
|
||||
border_k = "┤"
|
||||
border_l = "┖"
|
||||
border_m = "┘"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a fancy unicode representation of a single entry."""
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
linewrap = entry.journal.config['linewrap'] or 78
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
linewrap = entry.journal.config["linewrap"] or 78
|
||||
initial_linewrap = linewrap - len(date_str) - 2
|
||||
body_linewrap = linewrap - 2
|
||||
card = [cls.border_a + cls.border_b*(initial_linewrap) + cls.border_c + date_str]
|
||||
w = TextWrapper(width=initial_linewrap, initial_indent=cls.border_g+' ', subsequent_indent=cls.border_g+' ')
|
||||
card = [
|
||||
cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str
|
||||
]
|
||||
w = TextWrapper(
|
||||
width=initial_linewrap,
|
||||
initial_indent=cls.border_g + " ",
|
||||
subsequent_indent=cls.border_g + " ",
|
||||
)
|
||||
title_lines = w.wrap(entry.title)
|
||||
card.append(title_lines[0].ljust(initial_linewrap+1) + cls.border_d + cls.border_e*(len(date_str)-1) + cls.border_f)
|
||||
card.append(
|
||||
title_lines[0].ljust(initial_linewrap + 1)
|
||||
+ cls.border_d
|
||||
+ cls.border_e * (len(date_str) - 1)
|
||||
+ cls.border_f
|
||||
)
|
||||
w.width = body_linewrap
|
||||
if len(title_lines) > 1:
|
||||
for line in w.wrap(' '.join([title_line[len(w.subsequent_indent):]
|
||||
for title_line in title_lines[1:]])):
|
||||
card.append(line.ljust(body_linewrap+1) + cls.border_h)
|
||||
for line in w.wrap(
|
||||
" ".join(
|
||||
[
|
||||
title_line[len(w.subsequent_indent) :]
|
||||
for title_line in title_lines[1:]
|
||||
]
|
||||
)
|
||||
):
|
||||
card.append(line.ljust(body_linewrap + 1) + cls.border_h)
|
||||
if entry.body:
|
||||
card.append(cls.border_i + cls.border_j*body_linewrap + cls.border_k)
|
||||
card.append(cls.border_i + cls.border_j * body_linewrap + cls.border_k)
|
||||
for line in entry.body.splitlines():
|
||||
body_lines = w.wrap(line) or [cls.border_g]
|
||||
for body_line in body_lines:
|
||||
card.append(body_line.ljust(body_linewrap+1) + cls.border_h)
|
||||
card.append(cls.border_l + cls.border_b*body_linewrap + cls.border_m)
|
||||
card.append(body_line.ljust(body_linewrap + 1) + cls.border_h)
|
||||
card.append(cls.border_l + cls.border_b * body_linewrap + cls.border_m)
|
||||
return "\n".join(card)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
import sys
|
||||
from .. import util
|
||||
|
||||
|
||||
class JRNLImporter:
|
||||
"""This plugin imports entries from other jrnl files."""
|
||||
|
||||
names = ["jrnl"]
|
||||
|
||||
@staticmethod
|
||||
|
@ -25,5 +27,8 @@ class JRNLImporter:
|
|||
sys.exit(0)
|
||||
journal.import_(other_journal_txt)
|
||||
new_cnt = len(journal.entries)
|
||||
print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
|
||||
print(
|
||||
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
journal.write()
|
||||
|
|
|
@ -8,20 +8,21 @@ from .util import get_tags_count
|
|||
|
||||
class JSONExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into json."""
|
||||
|
||||
names = ["json"]
|
||||
extension = "json"
|
||||
|
||||
@classmethod
|
||||
def entry_to_dict(cls, entry):
|
||||
entry_dict = {
|
||||
'title': entry.title,
|
||||
'body': entry.body,
|
||||
'date': entry.date.strftime("%Y-%m-%d"),
|
||||
'time': entry.date.strftime("%H:%M"),
|
||||
'starred': entry.starred
|
||||
"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
|
||||
entry_dict["uuid"] = entry.uuid
|
||||
return entry_dict
|
||||
|
||||
@classmethod
|
||||
|
@ -35,6 +36,6 @@ class JSONExporter(TextExporter):
|
|||
tags = get_tags_count(journal)
|
||||
result = {
|
||||
"tags": {tag: count for count, tag in tags},
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries],
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
||||
|
|
|
@ -10,24 +10,25 @@ from ..util import WARNING_COLOR, RESET_COLOR
|
|||
|
||||
class MarkdownExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown."""
|
||||
|
||||
names = ["md", "markdown"]
|
||||
extension = "md"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry."""
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
if to_multifile is True:
|
||||
heading = '#'
|
||||
heading = "#"
|
||||
else:
|
||||
heading = '###'
|
||||
heading = "###"
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
previous_line = ''
|
||||
"""Increase heading levels in body text"""
|
||||
newbody = ""
|
||||
previous_line = ""
|
||||
warn_on_heading_level = False
|
||||
for line in body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
|
@ -35,24 +36,30 @@ class MarkdownExporter(TextExporter):
|
|||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
line = ""
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr)
|
||||
print(
|
||||
f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from .util import get_tags_count
|
|||
|
||||
class TagExporter(TextExporter):
|
||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||
|
||||
names = ["tags"]
|
||||
extension = "tags"
|
||||
|
||||
|
@ -21,9 +22,11 @@ class TagExporter(TextExporter):
|
|||
tag_counts = get_tags_count(journal)
|
||||
result = ""
|
||||
if not tag_counts:
|
||||
return '[No tags found in journal.]'
|
||||
return "[No tags found in journal.]"
|
||||
elif min(tag_counts)[0] == 0:
|
||||
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
||||
result += '[Removed tags that appear only once.]\n'
|
||||
result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
||||
result += "[Removed tags that appear only once.]\n"
|
||||
result += "\n".join(
|
||||
"{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -6,7 +6,9 @@ EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
|
|||
PRINT_RE = r"{{ *(.+?) *}}"
|
||||
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
||||
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(
|
||||
varname=VAR_RE, expression=EXPRESSION_RE
|
||||
)
|
||||
IF_RE = r"{% *if +(.+?) *%}"
|
||||
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||
|
@ -39,9 +41,10 @@ class Template:
|
|||
|
||||
def _eval_context(self, vars):
|
||||
import asteval
|
||||
|
||||
e = asteval.Interpreter(use_numpy=False, writer=None)
|
||||
e.symtable.update(vars)
|
||||
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
|
||||
e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
|
||||
return e
|
||||
|
||||
def _get_blocks(self):
|
||||
|
@ -49,12 +52,19 @@ class Template:
|
|||
name, contents = match.groups()
|
||||
self.blocks[name] = self._strip_single_nl(contents)
|
||||
return ""
|
||||
|
||||
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
||||
|
||||
def _expand(self, template, **vars):
|
||||
stack = sorted(
|
||||
[(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] +
|
||||
[(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)]
|
||||
[
|
||||
(m.start(), 1, m.groups()[0])
|
||||
for m in re.finditer(START_BLOCK_RE, template)
|
||||
]
|
||||
+ [
|
||||
(m.end(), -1, m.groups()[0])
|
||||
for m in re.finditer(END_BLOCK_RE, template)
|
||||
]
|
||||
)
|
||||
|
||||
last_nesting, nesting = 0, 0
|
||||
|
@ -80,19 +90,23 @@ class Template:
|
|||
start = pos
|
||||
last_nesting = nesting
|
||||
|
||||
result += self._expand_vars(template[stack[-1][0]:], **vars)
|
||||
result += self._expand_vars(template[stack[-1][0] :], **vars)
|
||||
return result
|
||||
|
||||
def _expand_vars(self, template, **vars):
|
||||
safe_eval = self._eval_context(vars)
|
||||
expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template)
|
||||
expanded = re.sub(
|
||||
INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
|
||||
)
|
||||
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
|
||||
|
||||
def _expand_cond(self, template, **vars):
|
||||
start_block = re.search(IF_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
expression = start_block.groups()[0]
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()]
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
if safe_eval(expression):
|
||||
|
@ -110,15 +124,17 @@ class Template:
|
|||
start_block = re.search(FOR_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
var_name, iterator = start_block.groups()
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()], strip_r=False
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
|
||||
result = ''
|
||||
result = ""
|
||||
items = safe_eval(iterator)
|
||||
for idx, var in enumerate(items):
|
||||
vars[var_name] = var
|
||||
vars['__last_iteration'] = idx == len(items) - 1
|
||||
vars["__last_iteration"] = idx == len(items) - 1
|
||||
result += self._expand(sub_template, **vars)
|
||||
del vars[var_name]
|
||||
return self._strip_single_nl(result)
|
||||
|
|
|
@ -13,20 +13,13 @@ class GenericTemplateExporter(TextExporter):
|
|||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
vars = {
|
||||
'entry': entry,
|
||||
'tags': entry.tags
|
||||
}
|
||||
vars = {"entry": entry, "tags": entry.tags}
|
||||
return cls.template.render_block("entry", **vars)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a string representation of an entire journal."""
|
||||
vars = {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
'tags': journal.tags
|
||||
}
|
||||
vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
||||
return cls.template.render_block("journal", **vars)
|
||||
|
||||
|
||||
|
@ -34,11 +27,12 @@ def __exporter_from_file(template_file):
|
|||
"""Create a template class from a file"""
|
||||
name = os.path.basename(template_file).replace(".template", "")
|
||||
template = Template.from_file(template_file)
|
||||
return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), {
|
||||
"names": [name],
|
||||
"extension": template.extension,
|
||||
"template": template
|
||||
})
|
||||
return type(
|
||||
str(f"{name.title()}Exporter"),
|
||||
(GenericTemplateExporter,),
|
||||
{"names": [name], "extension": template.extension, "template": template},
|
||||
)
|
||||
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from ..util import ERROR_COLOR, RESET_COLOR
|
|||
|
||||
class TextExporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
names = ["text", "txt"]
|
||||
extension = "txt"
|
||||
|
||||
|
@ -33,7 +34,9 @@ class TextExporter:
|
|||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension))
|
||||
return entry.date.strftime(
|
||||
"%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def write_files(cls, journal, path):
|
||||
|
@ -44,7 +47,9 @@ class TextExporter:
|
|||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_entry(entry))
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||
)
|
||||
return "[Journal exported to {}]".format(path)
|
||||
|
||||
@classmethod
|
||||
|
@ -54,7 +59,7 @@ class TextExporter:
|
|||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return cls.write_files(journal, output)
|
||||
elif output: # single file
|
||||
elif output: # single file
|
||||
return cls.write_file(journal, output)
|
||||
else:
|
||||
return cls.export_journal(journal)
|
||||
|
|
|
@ -6,9 +6,7 @@ def get_tags_count(journal):
|
|||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
for entry in journal.entries
|
||||
for tag in set(entry.tags)]
|
||||
tags = [tag for entry in journal.entries for tag in set(entry.tags)]
|
||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
return tag_counts
|
||||
|
@ -24,4 +22,4 @@ def oxford_list(lst):
|
|||
elif len(lst) == 2:
|
||||
return lst[0] + " or " + lst[1]
|
||||
else:
|
||||
return ', '.join(lst[:-1]) + ", or " + lst[-1]
|
||||
return ", ".join(lst[:-1]) + ", or " + lst[-1]
|
||||
|
|
|
@ -8,6 +8,7 @@ from xml.dom import minidom
|
|||
|
||||
class XMLExporter(JSONExporter):
|
||||
"""This Exporter can convert entries and journals into XML."""
|
||||
|
||||
names = ["xml"]
|
||||
extension = "xml"
|
||||
|
||||
|
@ -15,7 +16,7 @@ class XMLExporter(JSONExporter):
|
|||
def export_entry(cls, entry, doc=None):
|
||||
"""Returns an XML representation of a single entry."""
|
||||
doc_el = doc or minidom.Document()
|
||||
entry_el = doc_el.createElement('entry')
|
||||
entry_el = doc_el.createElement("entry")
|
||||
for key, value in cls.entry_to_dict(entry).items():
|
||||
elem = doc_el.createElement(key)
|
||||
elem.appendChild(doc_el.createTextNode(value))
|
||||
|
@ -28,11 +29,11 @@ class XMLExporter(JSONExporter):
|
|||
|
||||
@classmethod
|
||||
def entry_to_xml(cls, entry, doc):
|
||||
entry_el = doc.createElement('entry')
|
||||
entry_el.setAttribute('date', entry.date.isoformat())
|
||||
entry_el = doc.createElement("entry")
|
||||
entry_el.setAttribute("date", entry.date.isoformat())
|
||||
if hasattr(entry, "uuid"):
|
||||
entry_el.setAttribute('uuid', entry.uuid)
|
||||
entry_el.setAttribute('starred', entry.starred)
|
||||
entry_el.setAttribute("uuid", entry.uuid)
|
||||
entry_el.setAttribute("starred", entry.starred)
|
||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||
return entry_el
|
||||
|
||||
|
@ -41,12 +42,12 @@ class XMLExporter(JSONExporter):
|
|||
"""Returns an XML representation of an entire journal."""
|
||||
tags = get_tags_count(journal)
|
||||
doc = minidom.Document()
|
||||
xml = doc.createElement('journal')
|
||||
tags_el = doc.createElement('tags')
|
||||
entries_el = doc.createElement('entries')
|
||||
xml = doc.createElement("journal")
|
||||
tags_el = doc.createElement("tags")
|
||||
entries_el = doc.createElement("entries")
|
||||
for count, tag in tags:
|
||||
tag_el = doc.createElement('tag')
|
||||
tag_el.setAttribute('name', tag)
|
||||
tag_el = doc.createElement("tag")
|
||||
tag_el.setAttribute("name", tag)
|
||||
count_node = doc.createTextNode(str(count))
|
||||
tag_el.appendChild(count_node)
|
||||
tags_el.appendChild(tag_el)
|
||||
|
|
|
@ -10,6 +10,7 @@ from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
|
|||
|
||||
class YAMLExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||
|
||||
names = ["yaml"]
|
||||
extension = "md"
|
||||
|
||||
|
@ -17,22 +18,29 @@ class YAMLExporter(TextExporter):
|
|||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||
if to_multifile is False:
|
||||
print("{}ERROR{}: YAML export must be to individual files. "
|
||||
"Please specify a directory to export to.".format("\033[31m", "\033[0m"), file=sys.stderr)
|
||||
print(
|
||||
"{}ERROR{}: YAML export must be to individual files. "
|
||||
"Please specify a directory to export to.".format(
|
||||
"\033[31m", "\033[0m"
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
tagsymbols = entry.journal.config['tagsymbols']
|
||||
tagsymbols = entry.journal.config["tagsymbols"]
|
||||
# see also Entry.Entry.rag_regex
|
||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols))
|
||||
multi_tag_regex = re.compile(
|
||||
r"(?u)^\s*([{tags}][-+*#/\w]+\s*)+$".format(tags=tagsymbols)
|
||||
)
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
heading = '#'
|
||||
previous_line = ''
|
||||
"""Increase heading levels in body text"""
|
||||
newbody = ""
|
||||
heading = "#"
|
||||
previous_line = ""
|
||||
warn_on_heading_level = False
|
||||
for line in entry.body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
|
@ -40,45 +48,59 @@ class YAMLExporter(TextExporter):
|
|||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
line = ""
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||
r"^$", previous_line.strip()
|
||||
):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
line = ""
|
||||
elif multi_tag_regex.match(line):
|
||||
"""Tag only lines"""
|
||||
line = ''
|
||||
line = ""
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
|
||||
print(
|
||||
"{}WARNING{}: Headings increased past H6 on export - {} {}".format(
|
||||
WARNING_COLOR, RESET_COLOR, date_str, entry.title
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
dayone_attributes = ''
|
||||
dayone_attributes = ""
|
||||
if hasattr(entry, "uuid"):
|
||||
dayone_attributes += 'uuid: ' + entry.uuid + '\n'
|
||||
dayone_attributes += "uuid: " + entry.uuid + "\n"
|
||||
# TODO: copy over pictures, if present
|
||||
# source directory is entry.journal.config['journal']
|
||||
# output directory is...?
|
||||
|
||||
return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone} {body} {space}".format(
|
||||
date = date_str,
|
||||
title = entry.title,
|
||||
stared = entry.starred,
|
||||
tags = ', '.join([tag[1:] for tag in entry.tags]),
|
||||
dayone = dayone_attributes,
|
||||
body = newbody,
|
||||
space=""
|
||||
date=date_str,
|
||||
title=entry.title,
|
||||
stared=entry.starred,
|
||||
tags=", ".join([tag[1:] for tag in entry.tags]),
|
||||
dayone=dayone_attributes,
|
||||
body=newbody,
|
||||
space="",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns an error, as YAML export requires a directory as a target."""
|
||||
print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||
print(
|
||||
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
||||
ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
|
23
jrnl/time.py
23
jrnl/time.py
|
@ -1,7 +1,10 @@
|
|||
from datetime import datetime
|
||||
from dateutil.parser import parse as dateparse
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
|
||||
FAKE_YEAR = 9999
|
||||
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
|
||||
|
@ -12,7 +15,9 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
|||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False):
|
||||
def parse(
|
||||
date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False
|
||||
):
|
||||
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
|
||||
if not date_str:
|
||||
return None
|
||||
|
@ -37,7 +42,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra
|
|||
flag = 1 if date.hour == date.minute == 0 else 2
|
||||
date = date.timetuple()
|
||||
except Exception as e:
|
||||
if e.args[0] == 'day is out of range for month':
|
||||
if e.args[0] == "day is out of range for month":
|
||||
y, m, d, H, M, S = default_date.timetuple()[:6]
|
||||
default_date = datetime(y, m, d - 1, H, M, S)
|
||||
else:
|
||||
|
@ -53,10 +58,12 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra
|
|||
return None
|
||||
|
||||
if flag is 1: # Date found, but no time. Use the default time.
|
||||
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)
|
||||
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])
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import os
|
|||
def backup(filename, binary=False):
|
||||
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||
with open(filename, 'rb' if binary else 'r') as original:
|
||||
with open(filename, "rb" if binary else "r") as original:
|
||||
contents = original.read()
|
||||
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
|
||||
with open(filename + ".backup", "wb" if binary else "w") as backup:
|
||||
backup.write(contents)
|
||||
|
||||
|
||||
|
@ -25,7 +25,8 @@ def upgrade_jrnl_if_necessary(config_path):
|
|||
|
||||
config = util.load_config(config_path)
|
||||
|
||||
print("""Welcome to jrnl {}.
|
||||
print(
|
||||
"""Welcome to jrnl {}.
|
||||
|
||||
It looks like you've been using an older version of jrnl until now. That's
|
||||
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
||||
|
@ -39,18 +40,21 @@ you can enjoy all of the great new features that come with jrnl 2:
|
|||
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__))
|
||||
""".format(
|
||||
__version__
|
||||
)
|
||||
)
|
||||
encrypted_journals = {}
|
||||
plain_journals = {}
|
||||
other_journals = {}
|
||||
all_journals = []
|
||||
|
||||
for journal_name, journal_conf in config['journals'].items():
|
||||
for journal_name, journal_conf in config["journals"].items():
|
||||
if isinstance(journal_conf, dict):
|
||||
path = journal_conf.get("journal")
|
||||
encrypt = journal_conf.get("encrypt")
|
||||
else:
|
||||
encrypt = config.get('encrypt')
|
||||
encrypt = config.get("encrypt")
|
||||
path = journal_conf
|
||||
|
||||
path = os.path.expanduser(path)
|
||||
|
@ -62,21 +66,36 @@ older versions of jrnl anymore.
|
|||
else:
|
||||
plain_journals[journal_name] = path
|
||||
|
||||
longest_journal_name = max([len(journal) for journal in config['journals']])
|
||||
longest_journal_name = max([len(journal) for journal in config["journals"]])
|
||||
if encrypted_journals:
|
||||
print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
print(
|
||||
f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in encrypted_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if plain_journals:
|
||||
print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
print(
|
||||
f"\nFollowing plain text journals will upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in plain_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if other_journals:
|
||||
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||
for journal, path in other_journals.items():
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
try:
|
||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||
|
@ -86,24 +105,37 @@ older versions of jrnl anymore.
|
|||
raise UserAbort("jrnl NOT upgraded, exiting.")
|
||||
|
||||
for journal_name, path in encrypted_journals.items():
|
||||
print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
print(
|
||||
f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
backup(path, binary=True)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||
|
||||
for journal_name, path in plain_journals.items():
|
||||
print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
print(
|
||||
f"\nUpgrading plain text '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
backup(path)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||
|
||||
# loop through lists to validate
|
||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||
|
||||
if len(failed_journals) > 0:
|
||||
print("\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)),
|
||||
file=sys.stderr
|
||||
print(
|
||||
"\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
"s" if len(failed_journals) > 1 else "",
|
||||
"\n".join(j.name for j in failed_journals),
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
raise UpgradeValidationException
|
||||
|
@ -120,4 +152,5 @@ older versions of jrnl anymore.
|
|||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
|
||||
pass
|
||||
|
|
59
jrnl/util.py
59
jrnl/util.py
|
@ -25,7 +25,8 @@ RESET_COLOR = "\033[0m"
|
|||
|
||||
# Based on Segtok by Florian Leitner
|
||||
# https://github.com/fnl/segtok
|
||||
SENTENCE_SPLITTER = re.compile(r"""
|
||||
SENTENCE_SPLITTER = re.compile(
|
||||
r"""
|
||||
( # A sentence ends at one of two sequences:
|
||||
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
|
||||
[\'\u2019\"\u201D]? # an optional right quote,
|
||||
|
@ -33,14 +34,18 @@ SENTENCE_SPLITTER = re.compile(r"""
|
|||
\s+ # a sequence of required spaces.
|
||||
| # Otherwise,
|
||||
\n # a sentence also terminates newlines.
|
||||
)""", re.VERBOSE)
|
||||
)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_password(journal_name: str, prompt: str = "Enter password for new journal: ") -> str:
|
||||
def create_password(
|
||||
journal_name: str, prompt: str = "Enter password for new journal: "
|
||||
) -> str:
|
||||
while True:
|
||||
pw = gp.getpass(prompt)
|
||||
if not pw:
|
||||
|
@ -59,7 +64,11 @@ def create_password(journal_name: str, prompt: str = "Enter password for new jou
|
|||
return pw
|
||||
|
||||
|
||||
def decrypt_content(decrypt_func: Callable[[str], Optional[str]], keychain: str = None, max_attempts: int = 3) -> str:
|
||||
def decrypt_content(
|
||||
decrypt_func: Callable[[str], Optional[str]],
|
||||
keychain: str = None,
|
||||
max_attempts: int = 3,
|
||||
) -> str:
|
||||
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||
password = pwd_from_keychain or gp.getpass()
|
||||
result = decrypt_func(password)
|
||||
|
@ -81,21 +90,23 @@ def decrypt_content(decrypt_func: Callable[[str], Optional[str]], keychain: str
|
|||
|
||||
def get_keychain(journal_name):
|
||||
import keyring
|
||||
|
||||
try:
|
||||
return keyring.get_password('jrnl', journal_name)
|
||||
return keyring.get_password("jrnl", journal_name)
|
||||
except RuntimeError:
|
||||
return ""
|
||||
|
||||
|
||||
def set_keychain(journal_name, password):
|
||||
import keyring
|
||||
|
||||
if password is None:
|
||||
try:
|
||||
keyring.delete_password('jrnl', journal_name)
|
||||
keyring.delete_password("jrnl", journal_name)
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
keyring.set_password('jrnl', journal_name, password)
|
||||
keyring.set_password("jrnl", journal_name, password)
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
|
@ -112,34 +123,40 @@ def load_config(config_path):
|
|||
|
||||
|
||||
def scope_config(config, journal_name):
|
||||
if journal_name not in config['journals']:
|
||||
if journal_name not in config["journals"]:
|
||||
return config
|
||||
config = config.copy()
|
||||
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 journal overrides %s', journal_conf)
|
||||
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 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.pop('journals')
|
||||
config["journal"] = journal_conf
|
||||
config.pop("journals")
|
||||
return config
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
with open(tmpfile, 'w', encoding="utf-8") as f:
|
||||
with open(tmpfile, "w", encoding="utf-8") as f:
|
||||
if template:
|
||||
f.write(template)
|
||||
try:
|
||||
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
||||
subprocess.call(
|
||||
shlex.split(config["editor"], posix="win" not in sys.platform) + [tmpfile]
|
||||
)
|
||||
except AttributeError:
|
||||
subprocess.call(config['editor'] + [tmpfile])
|
||||
subprocess.call(config["editor"] + [tmpfile])
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.close(filehandle)
|
||||
os.remove(tmpfile)
|
||||
if not raw:
|
||||
print('[Nothing saved to file]', file=sys.stderr)
|
||||
print("[Nothing saved to file]", file=sys.stderr)
|
||||
return raw
|
||||
|
||||
|
||||
|
@ -152,9 +169,9 @@ def slugify(string):
|
|||
"""Slugifies a string.
|
||||
Based on public domain code from https://github.com/zacharyvoase/slugify
|
||||
"""
|
||||
normalized_string = str(unicodedata.normalize('NFKD', string))
|
||||
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
|
||||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
||||
normalized_string = str(unicodedata.normalize("NFKD", string))
|
||||
no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower()
|
||||
slug = re.sub(r"[-\s]+", "-", no_punctuation)
|
||||
return slug
|
||||
|
||||
|
||||
|
@ -163,4 +180,4 @@ def split_title(text):
|
|||
punkt = SENTENCE_SPLITTER.search(text)
|
||||
if not punkt:
|
||||
return text, ""
|
||||
return text[:punkt.end()].strip(), text[punkt.end():].strip()
|
||||
return text[: punkt.end()].strip(), text[punkt.end() :].strip()
|
||||
|
|
Loading…
Add table
Reference in a new issue