mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-20 21:18:32 +02:00
reorganize code, move around lots of functions
This commit is contained in:
parent
38b78b1d1f
commit
73a348b033
23 changed files with 462 additions and 470 deletions
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from jrnl.os_compat import on_windows
|
||||
|
||||
|
||||
CWD = os.getcwd()
|
||||
|
||||
|
@ -19,7 +21,7 @@ def before_feature(context, feature):
|
|||
feature.skip("Marked with @skip")
|
||||
return
|
||||
|
||||
if "skip_win" in feature.tags and "win32" in sys.platform:
|
||||
if "skip_win" in feature.tags and on_windows:
|
||||
feature.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
@ -46,7 +48,7 @@ def before_scenario(context, scenario):
|
|||
scenario.skip("Marked with @skip")
|
||||
return
|
||||
|
||||
if "skip_win" in scenario.effective_tags and "win32" in sys.platform:
|
||||
if "skip_win" in scenario.effective_tags and on_windows:
|
||||
scenario.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import os
|
|||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
|
@ -13,7 +12,14 @@ import toml
|
|||
import yaml
|
||||
|
||||
from behave import given, then, when
|
||||
from jrnl import Journal, __version__, cli, install, plugins, util
|
||||
from jrnl import Journal
|
||||
from jrnl import __version__
|
||||
from jrnl import cli
|
||||
from jrnl import install
|
||||
from jrnl import plugins
|
||||
|
||||
from jrnl.config import load_config
|
||||
from jrnl.os_compat import on_windows
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
|
@ -62,18 +68,18 @@ keyring.set_keyring(TestKeyring())
|
|||
|
||||
|
||||
def ushlex(command):
|
||||
return shlex.split(command, posix="win32" not in sys.platform)
|
||||
return shlex.split(command, posix=on_windows)
|
||||
|
||||
|
||||
def read_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
config = load_config(install.CONFIG_FILE_PATH)
|
||||
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)
|
||||
config = load_config(install.CONFIG_FILE_PATH)
|
||||
journal_conf = config["journals"][journal_name]
|
||||
|
||||
# We can override the default config on a by-journal basis
|
||||
|
@ -364,7 +370,7 @@ def config_var(context, key, value, journal=None):
|
|||
# Handle value being a dictionary
|
||||
value = ast.literal_eval(value)
|
||||
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
config = load_config(install.CONFIG_FILE_PATH)
|
||||
if journal:
|
||||
config = config["journals"][journal]
|
||||
assert key in config
|
||||
|
|
|
@ -3,6 +3,8 @@ import hashlib
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable, Optional
|
||||
import getpass
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
@ -10,11 +12,9 @@ from cryptography.hazmat.primitives import hashes, padding
|
|||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from . import util
|
||||
from .prompt import create_password
|
||||
from .Journal import Journal, LegacyJournal
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def make_key(password):
|
||||
password = password.encode("utf-8")
|
||||
|
@ -30,6 +30,30 @@ def make_key(password):
|
|||
return base64.urlsafe_b64encode(key)
|
||||
|
||||
|
||||
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 getpass.getpass()
|
||||
result = decrypt_func(password)
|
||||
# Password is bad:
|
||||
if result is None and pwd_from_keychain:
|
||||
set_keychain(keychain, None)
|
||||
attempt = 1
|
||||
while result is None and attempt < max_attempts:
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = getpass.getpass()
|
||||
result = decrypt_func(password)
|
||||
attempt += 1
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
print("Extremely wrong password.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class EncryptedJournal(Journal):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
|
@ -46,7 +70,7 @@ class EncryptedJournal(Journal):
|
|||
os.makedirs(dirname)
|
||||
print(f"[Directory {dirname} created]", file=sys.stderr)
|
||||
self.create_file(filename)
|
||||
self.password = util.create_password(self.name)
|
||||
self.password = create_password(self.name)
|
||||
|
||||
print(
|
||||
f"Encrypted journal '{self.name}' created at {filename}",
|
||||
|
@ -56,7 +80,7 @@ class EncryptedJournal(Journal):
|
|||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
logging.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def _load(self, filename):
|
||||
|
@ -80,7 +104,7 @@ class EncryptedJournal(Journal):
|
|||
if self.password:
|
||||
return decrypt_journal(self.password)
|
||||
|
||||
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
return decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
||||
def _store(self, filename, text):
|
||||
key = make_key(self.password)
|
||||
|
@ -95,7 +119,7 @@ class EncryptedJournal(Journal):
|
|||
new_journal.password = (
|
||||
other.password
|
||||
if hasattr(other, "password")
|
||||
else util.create_password(other.name)
|
||||
else create_password(other.name)
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("[Interrupted while creating new journal]", file=sys.stderr)
|
||||
|
@ -138,4 +162,31 @@ class LegacyEncryptedJournal(LegacyJournal):
|
|||
|
||||
if self.password:
|
||||
return decrypt_journal(self.password)
|
||||
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
return decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
||||
|
||||
def get_keychain(journal_name):
|
||||
import keyring
|
||||
|
||||
try:
|
||||
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)
|
||||
except keyring.errors.PasswordDeleteError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
keyring.set_password("jrnl", journal_name, password)
|
||||
except keyring.errors.NoKeyringError:
|
||||
print(
|
||||
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
|
|
@ -5,7 +5,8 @@ import re
|
|||
|
||||
import ansiwrap
|
||||
|
||||
from .util import colorize, highlight_tags_with_background_color, split_title
|
||||
from .color import colorize
|
||||
from .color import highlight_tags_with_background_color
|
||||
|
||||
|
||||
class Entry:
|
||||
|
@ -194,3 +195,28 @@ class Entry:
|
|||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
# Based on Segtok by Florian Leitner
|
||||
# https://github.com/fnl/segtok
|
||||
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,
|
||||
[\]\)]* # optional closing brackets and
|
||||
\s+ # a sequence of required spaces.
|
||||
)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n")
|
||||
|
||||
|
||||
def split_title(text):
|
||||
"""Splits the first sentence off from a text."""
|
||||
sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip())
|
||||
if not sep:
|
||||
sep = SENTENCE_SPLITTER.search(text)
|
||||
if not sep:
|
||||
return text, ""
|
||||
return text[: sep.end()].strip(), text[sep.end() :].strip()
|
||||
|
|
|
@ -6,9 +6,9 @@ import os
|
|||
import re
|
||||
import sys
|
||||
|
||||
from jrnl import Entry, time, util
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from . import Entry
|
||||
from . import time
|
||||
from .prompt import yesno
|
||||
|
||||
|
||||
class Tag:
|
||||
|
@ -56,7 +56,7 @@ class Journal:
|
|||
another journal object"""
|
||||
new_journal = cls(other.name, **other.config)
|
||||
new_journal.entries = other.entries
|
||||
log.debug(
|
||||
logging.debug(
|
||||
"Imported %d entries from %s to %s",
|
||||
len(new_journal),
|
||||
other.__class__.__name__,
|
||||
|
@ -85,7 +85,7 @@ class Journal:
|
|||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
logging.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def write(self, filename=None):
|
||||
|
@ -248,9 +248,7 @@ class Journal:
|
|||
to_delete = []
|
||||
|
||||
def ask_delete(entry):
|
||||
return util.yesno(
|
||||
f"Delete entry '{entry.pprint(short=True)}'?", default=False,
|
||||
)
|
||||
return yesno(f"Delete entry '{entry.pprint(short=True)}'?", default=False,)
|
||||
|
||||
for entry in self.entries:
|
||||
if ask_delete(entry):
|
||||
|
|
48
jrnl/cli.py
48
jrnl/cli.py
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
jrnl
|
||||
|
||||
|
@ -7,8 +6,7 @@
|
|||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
|
@ -18,34 +16,17 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import packaging.version
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from . import install, util
|
||||
from . import jrnl
|
||||
from . import install
|
||||
|
||||
from .parse_args import parse_args
|
||||
from .config import scope_config
|
||||
from .exception import UserAbort
|
||||
from .Journal import open_journal
|
||||
from .util import UserAbort
|
||||
from .util import get_journal_name
|
||||
from .util import WARNING_COLOR, RESET_COLOR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||
config["journals"][scope].update(new_config)
|
||||
else:
|
||||
config.update(new_config)
|
||||
from .config import get_journal_name
|
||||
|
||||
|
||||
def configure_logger(debug=False):
|
||||
|
@ -58,19 +39,6 @@ def configure_logger(debug=False):
|
|||
|
||||
|
||||
def run(manual_args=None):
|
||||
if packaging.version.parse(platform.python_version()) < packaging.version.parse(
|
||||
"3.7"
|
||||
):
|
||||
print(
|
||||
f"""{ERROR_COLOR}
|
||||
ERROR: Python version {platform.python_version()} not supported.
|
||||
|
||||
Please update to Python 3.7 (or higher) in order to use jrnl.
|
||||
{RESET_COLOR}""",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
"""
|
||||
Flow:
|
||||
1. Parse cli arguments
|
||||
|
@ -86,7 +54,7 @@ Please update to Python 3.7 (or higher) in order to use jrnl.
|
|||
|
||||
args = parse_args(manual_args)
|
||||
configure_logger(args.debug)
|
||||
log.debug("Parsed args: %s", args)
|
||||
logging.debug("Parsed args: %s", args)
|
||||
|
||||
# Run command if possible before config is available
|
||||
if callable(args.preconfig_cmd):
|
||||
|
@ -98,7 +66,7 @@ Please update to Python 3.7 (or higher) in order to use jrnl.
|
|||
config = install.load_or_install_jrnl()
|
||||
original_config = config.copy()
|
||||
args = get_journal_name(args, config)
|
||||
config = util.scope_config(config, args.journal_name)
|
||||
config = scope_config(config, args.journal_name)
|
||||
except UserAbort as err:
|
||||
print(f"\n{err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
77
jrnl/color.py
Normal file
77
jrnl/color.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env python
|
||||
import colorama
|
||||
import re
|
||||
from string import punctuation
|
||||
from string import whitespace
|
||||
from .os_compat import on_windows
|
||||
|
||||
if on_windows:
|
||||
colorama.init()
|
||||
|
||||
WARNING_COLOR = colorama.Fore.YELLOW
|
||||
ERROR_COLOR = colorama.Fore.RED
|
||||
RESET_COLOR = colorama.Fore.RESET
|
||||
|
||||
|
||||
def colorize(string, color, bold=False):
|
||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||
the user is "NONE" or the color doesn't exist in the colorama.Fore attributes,
|
||||
it returns the string without any modification."""
|
||||
color_escape = getattr(colorama.Fore, color.upper(), None)
|
||||
if not color_escape:
|
||||
return string
|
||||
elif not bold:
|
||||
return color_escape + string + colorama.Fore.RESET
|
||||
else:
|
||||
return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL
|
||||
|
||||
|
||||
def highlight_tags_with_background_color(entry, text, color, is_title=False):
|
||||
"""
|
||||
Takes a string and colorizes the tags in it based upon the config value for
|
||||
color.tags, while colorizing the rest of the text based on `color`.
|
||||
:param entry: Entry object, for access to journal config
|
||||
:param text: Text to be colorized
|
||||
:param color: Color for non-tag text, passed to colorize()
|
||||
:param is_title: Boolean flag indicating if the text is a title or not
|
||||
:return: Colorized str
|
||||
"""
|
||||
|
||||
def colorized_text_generator(fragments):
|
||||
"""Efficiently generate colorized tags / text from text fragments.
|
||||
Taken from @shobrook. Thanks, buddy :)
|
||||
:param fragments: List of strings representing parts of entry (tag or word).
|
||||
:rtype: List of tuples
|
||||
:returns [(colorized_str, original_str)]"""
|
||||
for part in fragments:
|
||||
if part and part[0] not in config["tagsymbols"]:
|
||||
yield (colorize(part, color, bold=is_title), part)
|
||||
elif part:
|
||||
yield (colorize(part, config["colors"]["tags"], bold=True), part)
|
||||
|
||||
config = entry.journal.config
|
||||
if config["highlight"]: # highlight tags
|
||||
text_fragments = re.split(entry.tag_regex(config["tagsymbols"]), text)
|
||||
|
||||
# Colorizing tags inside of other blocks of text
|
||||
final_text = ""
|
||||
previous_piece = ""
|
||||
for colorized_piece, piece in colorized_text_generator(text_fragments):
|
||||
# If this piece is entirely punctuation or whitespace or the start
|
||||
# of a line or the previous piece was a tag or this piece is a tag,
|
||||
# then add it to the final text without a leading space.
|
||||
if (
|
||||
all(char in punctuation + whitespace for char in piece)
|
||||
or previous_piece.endswith("\n")
|
||||
or (previous_piece and previous_piece[0] in config["tagsymbols"])
|
||||
or piece[0] in config["tagsymbols"]
|
||||
):
|
||||
final_text += colorized_piece
|
||||
else:
|
||||
# Otherwise add a leading space and then append the piece.
|
||||
final_text += " " + colorized_piece
|
||||
|
||||
previous_piece = piece
|
||||
return final_text.lstrip()
|
||||
else:
|
||||
return text
|
|
@ -34,7 +34,7 @@ def preconfig_version(_):
|
|||
|
||||
|
||||
def postconfig_list(config, **kwargs):
|
||||
from .util import list_journals
|
||||
from .output import list_journals
|
||||
|
||||
print(list_journals(config))
|
||||
|
||||
|
@ -56,7 +56,7 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
|||
"""
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .Journal import open_journal
|
||||
from .cli import update_config
|
||||
from .config import update_config
|
||||
from .install import save_config
|
||||
|
||||
# Open the journal
|
||||
|
@ -84,7 +84,7 @@ def postconfig_decrypt(args, config, original_config, **kwargs):
|
|||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
from .Journal import PlainJournal
|
||||
from .Journal import open_journal
|
||||
from .cli import update_config
|
||||
from .config import update_config
|
||||
from .install import save_config
|
||||
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
|
87
jrnl/config.py
Normal file
87
jrnl/config.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import logging
|
||||
import colorama
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .output import list_journals
|
||||
|
||||
|
||||
def scope_config(config, journal_name):
|
||||
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
|
||||
logging.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
|
||||
return config
|
||||
|
||||
|
||||
def verify_config(config):
|
||||
"""
|
||||
Ensures the keys set for colors are valid colorama.Fore attributes, or "None"
|
||||
:return: True if all keys are set correctly, False otherwise
|
||||
"""
|
||||
all_valid_colors = True
|
||||
for key, color in config["colors"].items():
|
||||
upper_color = color.upper()
|
||||
if upper_color == "NONE":
|
||||
continue
|
||||
if not getattr(colorama.Fore, upper_color, None):
|
||||
print(
|
||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
||||
key, color, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
all_valid_colors = False
|
||||
return all_valid_colors
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
"""Tries to load a config file from YAML."""
|
||||
with open(config_path) as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def is_config_json(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_file = f.read()
|
||||
return config_file.strip().startswith("{")
|
||||
|
||||
|
||||
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)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||
config["journals"][scope].update(new_config)
|
||||
else:
|
||||
config.update(new_config)
|
||||
|
||||
|
||||
def get_journal_name(args, config):
|
||||
from . import install
|
||||
|
||||
args.journal_name = install.DEFAULT_JOURNAL_KEY
|
||||
if args.text and args.text[0] in config["journals"]:
|
||||
args.journal_name = args.text[0]
|
||||
args.text = args.text[1:]
|
||||
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)
|
||||
|
||||
logging.debug("Using journal name: %s", args.journal_name)
|
||||
return args
|
40
jrnl/editor.py
Normal file
40
jrnl/editor.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
import tempfile
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .os_compat import on_windows
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
os.close(filehandle)
|
||||
|
||||
with open(tmpfile, "w", encoding="utf-8") as f:
|
||||
if template:
|
||||
f.write(template)
|
||||
|
||||
try:
|
||||
subprocess.call(shlex.split(config["editor"], posix=on_windows) + [tmpfile])
|
||||
except Exception as e:
|
||||
error_msg = f"""
|
||||
{ERROR_COLOR}{str(e)}{RESET_COLOR}
|
||||
|
||||
Please check the 'editor' key in your config file for errors:
|
||||
{repr(config['editor'])}
|
||||
"""
|
||||
print(textwrap.dedent(error_msg).strip(), file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not raw:
|
||||
print("[Nothing saved to file]", file=sys.stderr)
|
||||
|
||||
return raw
|
8
jrnl/exception.py
Normal file
8
jrnl/exception.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
class UserAbort(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
|
||||
pass
|
|
@ -4,14 +4,17 @@ import glob
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import xdg.BaseDirectory
|
||||
import yaml
|
||||
from . import __version__
|
||||
from .config import load_config
|
||||
from .config import verify_config
|
||||
from .exception import UserAbort
|
||||
from .os_compat import on_windows
|
||||
from .prompt import yesno
|
||||
from .upgrade import is_old_version
|
||||
|
||||
from . import __version__, util
|
||||
from .util import UserAbort, verify_config
|
||||
|
||||
if "win32" not in sys.platform:
|
||||
if not on_windows:
|
||||
# readline is not included in Windows Active Python
|
||||
import readline
|
||||
|
||||
|
@ -29,18 +32,6 @@ CONFIG_FILE_PATH_FALLBACK = os.path.join(USER_HOME, ".jrnl_config")
|
|||
JOURNAL_PATH = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or USER_HOME
|
||||
JOURNAL_FILE_PATH = os.path.join(JOURNAL_PATH, DEFAULT_JOURNAL_NAME)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def module_exists(module_name):
|
||||
"""Checks if a module exists and can be imported"""
|
||||
try:
|
||||
__import__(module_name)
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
default_config = {
|
||||
"version": __version__,
|
||||
|
@ -93,10 +84,10 @@ def load_or_install_jrnl():
|
|||
else CONFIG_FILE_PATH_FALLBACK
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
log.debug("Reading configuration from file %s", config_path)
|
||||
config = util.load_config(config_path)
|
||||
logging.debug("Reading configuration from file %s", config_path)
|
||||
config = load_config(config_path)
|
||||
|
||||
if util.is_old_version(config_path):
|
||||
if is_old_version(config_path):
|
||||
from . import upgrade
|
||||
|
||||
try:
|
||||
|
@ -118,21 +109,21 @@ def load_or_install_jrnl():
|
|||
verify_config(config)
|
||||
|
||||
else:
|
||||
log.debug("Configuration file not found, installing jrnl...")
|
||||
logging.debug("Configuration file not found, installing jrnl...")
|
||||
try:
|
||||
config = install()
|
||||
except KeyboardInterrupt:
|
||||
raise UserAbort("Installation aborted")
|
||||
|
||||
log.debug('Using configuration "%s"', config)
|
||||
logging.debug('Using configuration "%s"', config)
|
||||
return config
|
||||
|
||||
|
||||
def install():
|
||||
if "win32" not in sys.platform:
|
||||
if not on_windows:
|
||||
readline.set_completer_delims(" \t\n;")
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(autocomplete)
|
||||
readline.set_completer(_autocomplete_path)
|
||||
|
||||
# Where to create the journal?
|
||||
path_query = f"Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): "
|
||||
|
@ -149,7 +140,7 @@ def install():
|
|||
pass
|
||||
|
||||
# Encrypt it?
|
||||
encrypt = util.yesno(
|
||||
encrypt = yesno(
|
||||
"Do you want to encrypt your journal? You can always change this later",
|
||||
default=False,
|
||||
)
|
||||
|
@ -161,7 +152,7 @@ def install():
|
|||
return default_config
|
||||
|
||||
|
||||
def autocomplete(text, state):
|
||||
def _autocomplete_path(text, state):
|
||||
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)
|
||||
|
|
39
jrnl/jrnl.py
39
jrnl/jrnl.py
|
@ -1,14 +1,13 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from . import util
|
||||
from . import install
|
||||
from . import plugins
|
||||
from .editor import get_text_from_editor
|
||||
|
||||
from .util import ERROR_COLOR
|
||||
from .util import RESET_COLOR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .os_compat import on_windows
|
||||
|
||||
|
||||
def _is_write_mode(args, config, **kwargs):
|
||||
|
@ -53,30 +52,30 @@ def write_mode(args, config, journal, **kwargs):
|
|||
4. Use stdin.read as last resort
|
||||
6. Write any found text to journal, or exit
|
||||
"""
|
||||
log.debug("Write mode: starting")
|
||||
logging.debug("Write mode: starting")
|
||||
|
||||
if args.text:
|
||||
log.debug("Write mode: cli text detected: %s", args.text)
|
||||
logging.debug("Write mode: cli text detected: %s", args.text)
|
||||
raw = " ".join(args.text).strip()
|
||||
|
||||
elif not sys.stdin.isatty():
|
||||
log.debug("Write mode: receiving piped text")
|
||||
logging.debug("Write mode: receiving piped text")
|
||||
raw = sys.stdin.read()
|
||||
|
||||
else:
|
||||
raw = _write_in_editor(config)
|
||||
|
||||
if not raw:
|
||||
log.error("Write mode: couldn't get raw text")
|
||||
logging.error("Write mode: couldn't get raw text")
|
||||
sys.exit()
|
||||
|
||||
log.debug(
|
||||
logging.debug(
|
||||
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
||||
)
|
||||
journal.new_entry(raw)
|
||||
print(f"[Entry added to {args.journal_name} journal]", file=sys.stderr)
|
||||
journal.write()
|
||||
log.debug("Write mode: completed journal.write()", args.journal_name, raw)
|
||||
logging.debug("Write mode: completed journal.write()", args.journal_name, raw)
|
||||
|
||||
|
||||
def search_mode(args, journal, **kwargs):
|
||||
|
@ -109,12 +108,12 @@ def search_mode(args, journal, **kwargs):
|
|||
|
||||
def _write_in_editor(config):
|
||||
if config["editor"]:
|
||||
log.debug("Write mode: opening editor")
|
||||
logging.debug("Write mode: opening editor")
|
||||
template = _get_editor_template(config)
|
||||
raw = util.get_text_from_editor(config, template)
|
||||
raw = get_text_from_editor(config, template)
|
||||
|
||||
else:
|
||||
_how_to_quit = "Ctrl+z and then Enter" if "win32" in sys.platform else "Ctrl+d"
|
||||
_how_to_quit = "Ctrl+z and then Enter" if on_windows else "Ctrl+d"
|
||||
print(
|
||||
f"[Writing Entry; on a blank line, press {_how_to_quit} to finish writing]\n",
|
||||
file=sys.stderr,
|
||||
|
@ -122,7 +121,7 @@ def _write_in_editor(config):
|
|||
try:
|
||||
raw = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
log.error("Write mode: keyboard interrupt")
|
||||
logging.error("Write mode: keyboard interrupt")
|
||||
print("[Entry NOT saved to journal]", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
|
@ -130,17 +129,17 @@ def _write_in_editor(config):
|
|||
|
||||
|
||||
def _get_editor_template(config, **kwargs):
|
||||
log.debug("Write mode: loading template for entry")
|
||||
logging.debug("Write mode: loading template for entry")
|
||||
|
||||
if not config["template"]:
|
||||
log.debug("Write mode: no template configured")
|
||||
logging.debug("Write mode: no template configured")
|
||||
return ""
|
||||
|
||||
try:
|
||||
template = open(config["template"]).read()
|
||||
log.debug("Write mode: template loaded: %s", template)
|
||||
logging.debug("Write mode: template loaded: %s", template)
|
||||
except OSError:
|
||||
log.error("Write mode: template not loaded")
|
||||
logging.error("Write mode: template not loaded")
|
||||
print(
|
||||
f"[Could not read template at '{config['template']}']", file=sys.stderr,
|
||||
)
|
||||
|
@ -191,7 +190,7 @@ def _edit_search_results(config, journal, old_entries, **kwargs):
|
|||
old_stats = _get_predit_stats(journal)
|
||||
|
||||
# Send user to the editor
|
||||
edited = util.get_text_from_editor(config, journal.editable_str())
|
||||
edited = get_text_from_editor(config, journal.editable_str())
|
||||
journal.parse_editable_str(edited)
|
||||
|
||||
# Print summary if available
|
||||
|
|
3
jrnl/os_compat.py
Normal file
3
jrnl/os_compat.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from sys import platform
|
||||
|
||||
on_windows = "win32" in platform
|
31
jrnl/output.py
Normal file
31
jrnl/output.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
import logging
|
||||
|
||||
|
||||
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
|
||||
import sys
|
||||
import textwrap
|
||||
from .color import RESET_COLOR, WARNING_COLOR
|
||||
|
||||
warning_msg = f"""
|
||||
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
||||
Please us {new_cmd} instead.
|
||||
"""
|
||||
warning_msg = textwrap.dedent(warning_msg)
|
||||
logging.warning(warning_msg)
|
||||
print(f"{WARNING_COLOR}{warning_msg}{RESET_COLOR}", file=sys.stderr)
|
||||
|
||||
if callback is not None:
|
||||
callback(**kwargs)
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
from . import install
|
||||
|
||||
"""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
|
||||
)
|
||||
return result
|
|
@ -11,7 +11,7 @@ from .commands import postconfig_list
|
|||
from .commands import postconfig_import
|
||||
from .commands import postconfig_encrypt
|
||||
from .commands import postconfig_decrypt
|
||||
from .util import deprecated_cmd
|
||||
from .output import deprecated_cmd
|
||||
|
||||
|
||||
class WrappingFormatter(argparse.RawTextHelpFormatter):
|
||||
|
|
|
@ -5,7 +5,7 @@ import os
|
|||
import re
|
||||
import sys
|
||||
|
||||
from ..util import RESET_COLOR, WARNING_COLOR
|
||||
from jrnl.color import RESET_COLOR, WARNING_COLOR
|
||||
from .text_exporter import TextExporter
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
# encoding: utf-8
|
||||
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from ..util import ERROR_COLOR, RESET_COLOR, slugify
|
||||
from jrnl.color import ERROR_COLOR, RESET_COLOR
|
||||
|
||||
|
||||
class TextExporter:
|
||||
|
@ -35,7 +37,7 @@ class TextExporter:
|
|||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
return entry.date.strftime(
|
||||
"%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)
|
||||
"%Y-%m-%d_{}.{}".format(cls._slugify(str(entry.title)), cls.extension)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -52,6 +54,15 @@ class TextExporter:
|
|||
)
|
||||
return "[Journal exported to {}]".format(path)
|
||||
|
||||
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)
|
||||
return slug
|
||||
|
||||
@classmethod
|
||||
def export(cls, journal, output=None):
|
||||
"""Exports to individual files if output is an existing path, or into
|
||||
|
|
|
@ -5,7 +5,7 @@ import os
|
|||
import re
|
||||
import sys
|
||||
|
||||
from ..util import ERROR_COLOR, RESET_COLOR, WARNING_COLOR
|
||||
from jrnl.color import ERROR_COLOR, RESET_COLOR, WARNING_COLOR
|
||||
from .text_exporter import TextExporter
|
||||
|
||||
|
||||
|
|
28
jrnl/prompt.py
Normal file
28
jrnl/prompt.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import sys
|
||||
import getpass
|
||||
|
||||
|
||||
def create_password(
|
||||
journal_name: str, prompt: str = "Enter password for new journal: "
|
||||
) -> str:
|
||||
while True:
|
||||
pw = getpass.getpass(prompt)
|
||||
if not pw:
|
||||
print("Password can't be an empty string!", file=sys.stderr)
|
||||
continue
|
||||
elif pw == getpass.getpass("Enter password again: "):
|
||||
break
|
||||
|
||||
print("Passwords did not match, please try again", file=sys.stderr)
|
||||
|
||||
if yesno("Do you want to store the password in your keychain?", default=True):
|
||||
from .EncryptedJournal import set_keychain
|
||||
|
||||
set_keychain(journal_name, pw)
|
||||
return pw
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||
response = input(prompt)
|
||||
return {"y": True, "n": False}.get(response.lower().strip(), default)
|
|
@ -1,9 +1,15 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from . import Journal, __version__, util
|
||||
from . import __version__
|
||||
from . import Journal
|
||||
from .config import is_config_json
|
||||
from .config import load_config
|
||||
from .config import scope_config
|
||||
from .exception import UpgradeValidationException
|
||||
from .exception import UserAbort
|
||||
from .prompt import yesno
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .util import UserAbort
|
||||
|
||||
|
||||
def backup(filename, binary=False):
|
||||
|
@ -19,7 +25,7 @@ def backup(filename, binary=False):
|
|||
except FileNotFoundError:
|
||||
print(f"\nError: {filename} does not exist.")
|
||||
try:
|
||||
cont = util.yesno(f"\nCreate {filename}?", default=False)
|
||||
cont = yesno(f"\nCreate {filename}?", default=False)
|
||||
if not cont:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
@ -35,7 +41,7 @@ def check_exists(path):
|
|||
|
||||
|
||||
def upgrade_jrnl(config_path):
|
||||
config = util.load_config(config_path)
|
||||
config = load_config(config_path)
|
||||
|
||||
print(
|
||||
f"""Welcome to jrnl {__version__}.
|
||||
|
@ -113,7 +119,7 @@ older versions of jrnl anymore.
|
|||
)
|
||||
|
||||
try:
|
||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||
cont = yesno("\nContinue upgrading jrnl?", default=False)
|
||||
if not cont:
|
||||
raise KeyboardInterrupt
|
||||
except KeyboardInterrupt:
|
||||
|
@ -126,7 +132,7 @@ older versions of jrnl anymore.
|
|||
)
|
||||
backup(path, binary=True)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
journal_name, scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||
|
||||
|
@ -137,7 +143,7 @@ older versions of jrnl anymore.
|
|||
)
|
||||
backup(path)
|
||||
old_journal = Journal.open_journal(
|
||||
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||
journal_name, scope_config(config, journal_name), legacy=True
|
||||
)
|
||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||
|
||||
|
@ -166,7 +172,5 @@ older versions of jrnl anymore.
|
|||
print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
|
||||
|
||||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
|
||||
pass
|
||||
def is_old_version(config_path):
|
||||
return is_config_json(config_path)
|
||||
|
|
338
jrnl/util.py
338
jrnl/util.py
|
@ -1,338 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import getpass as gp
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from string import punctuation, whitespace
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from typing import Callable, Optional
|
||||
import unicodedata
|
||||
|
||||
import colorama
|
||||
import yaml
|
||||
|
||||
if "win32" in sys.platform:
|
||||
colorama.init()
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
WARNING_COLOR = colorama.Fore.YELLOW
|
||||
ERROR_COLOR = colorama.Fore.RED
|
||||
RESET_COLOR = colorama.Fore.RESET
|
||||
|
||||
# Based on Segtok by Florian Leitner
|
||||
# https://github.com/fnl/segtok
|
||||
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,
|
||||
[\]\)]* # optional closing brackets and
|
||||
\s+ # a sequence of required spaces.
|
||||
)""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n")
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_password(
|
||||
journal_name: str, prompt: str = "Enter password for new journal: "
|
||||
) -> str:
|
||||
while True:
|
||||
pw = gp.getpass(prompt)
|
||||
if not pw:
|
||||
print("Password can't be an empty string!", file=sys.stderr)
|
||||
continue
|
||||
elif pw == gp.getpass("Enter password again: "):
|
||||
break
|
||||
|
||||
print("Passwords did not match, please try again", file=sys.stderr)
|
||||
|
||||
if yesno("Do you want to store the password in your keychain?", default=True):
|
||||
set_keychain(journal_name, pw)
|
||||
return pw
|
||||
|
||||
|
||||
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)
|
||||
# Password is bad:
|
||||
if result is None and pwd_from_keychain:
|
||||
set_keychain(keychain, None)
|
||||
attempt = 1
|
||||
while result is None and attempt < max_attempts:
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = gp.getpass()
|
||||
result = decrypt_func(password)
|
||||
attempt += 1
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
print("Extremely wrong password.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_keychain(journal_name):
|
||||
import keyring
|
||||
|
||||
try:
|
||||
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)
|
||||
except keyring.errors.PasswordDeleteError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
keyring.set_password("jrnl", journal_name, password)
|
||||
except keyring.errors.NoKeyringError:
|
||||
print(
|
||||
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||
response = input(prompt)
|
||||
return {"y": True, "n": False}.get(response.lower().strip(), default)
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
"""Tries to load a config file from YAML.
|
||||
"""
|
||||
with open(config_path) as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def is_config_json(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_file = f.read()
|
||||
return config_file.strip().startswith("{")
|
||||
|
||||
|
||||
def is_old_version(config_path):
|
||||
return is_config_json(config_path)
|
||||
|
||||
|
||||
def scope_config(config, journal_name):
|
||||
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
|
||||
)
|
||||
config.update(journal_conf)
|
||||
else:
|
||||
# But also just give them a string to point to the journal file
|
||||
config["journal"] = journal_conf
|
||||
return config
|
||||
|
||||
|
||||
def verify_config(config):
|
||||
"""
|
||||
Ensures the keys set for colors are valid colorama.Fore attributes, or "None"
|
||||
:return: True if all keys are set correctly, False otherwise
|
||||
"""
|
||||
all_valid_colors = True
|
||||
for key, color in config["colors"].items():
|
||||
upper_color = color.upper()
|
||||
if upper_color == "NONE":
|
||||
continue
|
||||
if not getattr(colorama.Fore, upper_color, None):
|
||||
print(
|
||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
||||
key, color, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
all_valid_colors = False
|
||||
return all_valid_colors
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
os.close(filehandle)
|
||||
|
||||
with open(tmpfile, "w", encoding="utf-8") as f:
|
||||
if template:
|
||||
f.write(template)
|
||||
|
||||
try:
|
||||
subprocess.call(
|
||||
shlex.split(config["editor"], posix="win32" not in sys.platform) + [tmpfile]
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = f"""
|
||||
{ERROR_COLOR}{str(e)}{RESET_COLOR}
|
||||
|
||||
Please check the 'editor' key in your config file for errors:
|
||||
{repr(config['editor'])}
|
||||
"""
|
||||
print(textwrap.dedent(error_msg).strip(), file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not raw:
|
||||
print("[Nothing saved to file]", file=sys.stderr)
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def colorize(string, color, bold=False):
|
||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||
the user is "NONE" or the color doesn't exist in the colorama.Fore attributes,
|
||||
it returns the string without any modification."""
|
||||
color_escape = getattr(colorama.Fore, color.upper(), None)
|
||||
if not color_escape:
|
||||
return string
|
||||
elif not bold:
|
||||
return color_escape + string + colorama.Fore.RESET
|
||||
else:
|
||||
return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL
|
||||
|
||||
|
||||
def highlight_tags_with_background_color(entry, text, color, is_title=False):
|
||||
"""
|
||||
Takes a string and colorizes the tags in it based upon the config value for
|
||||
color.tags, while colorizing the rest of the text based on `color`.
|
||||
:param entry: Entry object, for access to journal config
|
||||
:param text: Text to be colorized
|
||||
:param color: Color for non-tag text, passed to colorize()
|
||||
:param is_title: Boolean flag indicating if the text is a title or not
|
||||
:return: Colorized str
|
||||
"""
|
||||
|
||||
def colorized_text_generator(fragments):
|
||||
"""Efficiently generate colorized tags / text from text fragments.
|
||||
Taken from @shobrook. Thanks, buddy :)
|
||||
:param fragments: List of strings representing parts of entry (tag or word).
|
||||
:rtype: List of tuples
|
||||
:returns [(colorized_str, original_str)]"""
|
||||
for part in fragments:
|
||||
if part and part[0] not in config["tagsymbols"]:
|
||||
yield (colorize(part, color, bold=is_title), part)
|
||||
elif part:
|
||||
yield (colorize(part, config["colors"]["tags"], bold=True), part)
|
||||
|
||||
config = entry.journal.config
|
||||
if config["highlight"]: # highlight tags
|
||||
text_fragments = re.split(entry.tag_regex(config["tagsymbols"]), text)
|
||||
|
||||
# Colorizing tags inside of other blocks of text
|
||||
final_text = ""
|
||||
previous_piece = ""
|
||||
for colorized_piece, piece in colorized_text_generator(text_fragments):
|
||||
# If this piece is entirely punctuation or whitespace or the start
|
||||
# of a line or the previous piece was a tag or this piece is a tag,
|
||||
# then add it to the final text without a leading space.
|
||||
if (
|
||||
all(char in punctuation + whitespace for char in piece)
|
||||
or previous_piece.endswith("\n")
|
||||
or (previous_piece and previous_piece[0] in config["tagsymbols"])
|
||||
or piece[0] in config["tagsymbols"]
|
||||
):
|
||||
final_text += colorized_piece
|
||||
else:
|
||||
# Otherwise add a leading space and then append the piece.
|
||||
final_text += " " + colorized_piece
|
||||
|
||||
previous_piece = piece
|
||||
return final_text.lstrip()
|
||||
else:
|
||||
return text
|
||||
|
||||
|
||||
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)
|
||||
return slug
|
||||
|
||||
|
||||
def split_title(text):
|
||||
"""Splits the first sentence off from a text."""
|
||||
sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip())
|
||||
if not sep:
|
||||
sep = SENTENCE_SPLITTER.search(text)
|
||||
if not sep:
|
||||
return text, ""
|
||||
return text[: sep.end()].strip(), text[sep.end() :].strip()
|
||||
|
||||
|
||||
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
|
||||
import sys
|
||||
import textwrap
|
||||
from .util import RESET_COLOR, WARNING_COLOR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
warning_msg = f"""
|
||||
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
||||
Please us {new_cmd} instead.
|
||||
"""
|
||||
warning_msg = textwrap.dedent(warning_msg)
|
||||
log.warning(warning_msg)
|
||||
print(f"{WARNING_COLOR}{warning_msg}{RESET_COLOR}", file=sys.stderr)
|
||||
|
||||
if callback is not None:
|
||||
callback(**kwargs)
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
from . import install
|
||||
|
||||
"""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
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_journal_name(args, config):
|
||||
from . import install
|
||||
|
||||
args.journal_name = install.DEFAULT_JOURNAL_KEY
|
||||
if args.text and args.text[0] in config["journals"]:
|
||||
args.journal_name = args.text[0]
|
||||
args.text = args.text[1:]
|
||||
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)
|
||||
|
||||
log.debug("Using journal name: %s", args.journal_name)
|
||||
return args
|
|
@ -1,4 +1,4 @@
|
|||
from jrnl.cli import parse_args
|
||||
from jrnl.parse_args import parse_args
|
||||
|
||||
import pytest
|
||||
import shlex
|
||||
|
|
Loading…
Add table
Reference in a new issue