mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
Rework how all output and messaging works in jrnl (#1475)
* fix missed statement from last PR * replace print statement for adding an entry to a journal * clean up linting and format * change print statement over to new print_msg function * make print_msg always print to stderr * change print statement over to new print_msg function * update importer to use new message function * update yaml format to use new message function * code cleanup * update yaml format to use new message function * update yaml format to use new exception handling * update Journal class to use new message function * update install module to use new message function * update config module to use new message function * update upgrade module to properly use new message and exception handling * fix typo * update upgrade module to use new message handling * update welcome message to use new handling * update upgrade module to use new message handling * update upgrade module journal summaries to use new message handling * take out old code * update upgrade module to use new message handling * update upgrade module to use new message handling * update more modules to use new message handling * take out old comment * update deprecated_cmd to use new message handling * update text_exporter with new message handling, get rid of old color constants * get rid of hardcoded text * whitespace changes * rework MsgType into MsgStyle so messages can have different styles * add comment * Move around code to separate concerns of each function a bit more * update create_password and yesno prompt functions for new messaging * fix missing newline for keyboard interrupts * fix misc linting * fix bug with panel titles always showing 'error' after one error * fix missing import * update debug output after uncaught exception * update exception for new exception handling * rewrite yesno function to use new centralized messages * reduce the debug output slightly * clean up print_msgs function * clean up create_password function * clean up misc linting * rename screen_input to hide_input to be more clear * update encrypted journal prompt to use new messaging functionality * fix typo in message key * move rich console into function so we can mock properly * update password mock to use rich console instead of getpass * add more helpful output to then step * fix test by updating expected output * update message to use new functionality * rework mocks in test suite for new messaging functionality * fix linting issue * fix more tests * fix more tests * fix more tests * fix more tests * fix merge bug * update prompt_action_entries to use new messaging functionality * Add new input_method "type" This does the same thing as input_method "pipe" but is more clear what it's doing (typing text into the builtin composer) * get rid of old commented code * get rid of unused code * move some files around Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
parent
4d683a13c0
commit
f53110c69b
38 changed files with 912 additions and 470 deletions
|
@ -1,9 +1,7 @@
|
||||||
import base64
|
import base64
|
||||||
import getpass
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -24,7 +22,8 @@ from .prompt import create_password
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
|
||||||
|
|
||||||
def make_key(password):
|
def make_key(password):
|
||||||
|
@ -46,21 +45,26 @@ def decrypt_content(
|
||||||
keychain: str = None,
|
keychain: str = None,
|
||||||
max_attempts: int = 3,
|
max_attempts: int = 3,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
def get_pw():
|
||||||
|
return print_msg(
|
||||||
|
Message(MsgText.Password, MsgStyle.PROMPT), get_input=True, hide_input=True
|
||||||
|
)
|
||||||
|
|
||||||
pwd_from_keychain = keychain and get_keychain(keychain)
|
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||||
password = pwd_from_keychain or getpass.getpass()
|
password = pwd_from_keychain or get_pw()
|
||||||
result = decrypt_func(password)
|
result = decrypt_func(password)
|
||||||
# Password is bad:
|
# Password is bad:
|
||||||
if result is None and pwd_from_keychain:
|
if result is None and pwd_from_keychain:
|
||||||
set_keychain(keychain, None)
|
set_keychain(keychain, None)
|
||||||
attempt = 1
|
attempt = 1
|
||||||
while result is None and attempt < max_attempts:
|
while result is None and attempt < max_attempts:
|
||||||
print("Wrong password, try again.", file=sys.stderr)
|
print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING))
|
||||||
password = getpass.getpass()
|
password = get_pw()
|
||||||
result = decrypt_func(password)
|
result = decrypt_func(password)
|
||||||
attempt += 1
|
attempt += 1
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
raise JrnlException(Message(MsgText.PasswordMaxTriesExceeded, MsgType.ERROR))
|
raise JrnlException(Message(MsgText.PasswordMaxTriesExceeded, MsgStyle.ERROR))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -79,13 +83,22 @@ class EncryptedJournal(Journal):
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
if not os.path.isdir(dirname):
|
if not os.path.isdir(dirname):
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
print(f"[Directory {dirname} created]", file=sys.stderr)
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.DirectoryCreated,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"directory_name": dirname},
|
||||||
|
)
|
||||||
|
)
|
||||||
self.create_file(filename)
|
self.create_file(filename)
|
||||||
self.password = create_password(self.name)
|
self.password = create_password(self.name)
|
||||||
|
|
||||||
print(
|
print_msg(
|
||||||
f"Encrypted journal '{self.name}' created at {filename}",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.JournalCreated,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"journal_name": self.name, "filename": filename},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
text = self._load(filename)
|
text = self._load(filename)
|
||||||
|
@ -179,7 +192,7 @@ def get_keychain(journal_name):
|
||||||
return keyring.get_password("jrnl", journal_name)
|
return keyring.get_password("jrnl", journal_name)
|
||||||
except keyring.errors.KeyringError as e:
|
except keyring.errors.KeyringError as e:
|
||||||
if not isinstance(e, keyring.errors.NoKeyringError):
|
if not isinstance(e, keyring.errors.NoKeyringError):
|
||||||
print("Failed to retrieve keyring", file=sys.stderr)
|
print_msg(Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,9 +209,7 @@ def set_keychain(journal_name, password):
|
||||||
keyring.set_password("jrnl", journal_name, password)
|
keyring.set_password("jrnl", journal_name, password)
|
||||||
except keyring.errors.KeyringError as e:
|
except keyring.errors.KeyringError as e:
|
||||||
if isinstance(e, keyring.errors.NoKeyringError):
|
if isinstance(e, keyring.errors.NoKeyringError):
|
||||||
print(
|
msg = Message(MsgText.KeyringBackendNotFound, MsgStyle.WARNING)
|
||||||
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print("Failed to retrieve keyring", file=sys.stderr)
|
msg = Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR)
|
||||||
|
print_msg(msg)
|
||||||
|
|
|
@ -81,7 +81,6 @@ class Folder(Journal.Journal):
|
||||||
filenames = get_files(self.config["journal"])
|
filenames = get_files(self.config["journal"])
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
if os.stat(filename).st_size <= 0:
|
if os.stat(filename).st_size <= 0:
|
||||||
# print("empty file: {}".format(filename))
|
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|
||||||
def delete_entries(self, entries_to_delete):
|
def delete_entries(self, entries_to_delete):
|
||||||
|
|
|
@ -6,13 +6,17 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import time
|
from . import time
|
||||||
from .prompt import yesno
|
from .prompt import yesno
|
||||||
from .path import expand_path
|
from .path import expand_path
|
||||||
|
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
from jrnl.messages import Message
|
||||||
|
from jrnl.messages import MsgText
|
||||||
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
class Tag:
|
class Tag:
|
||||||
def __init__(self, name, count=0):
|
def __init__(self, name, count=0):
|
||||||
|
@ -83,9 +87,24 @@ class Journal:
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
if not os.path.isdir(dirname):
|
if not os.path.isdir(dirname):
|
||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
print(f"[Directory {dirname} created]", file=sys.stderr)
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.DirectoryCreated,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"directory_name": dirname},
|
||||||
|
)
|
||||||
|
)
|
||||||
self.create_file(filename)
|
self.create_file(filename)
|
||||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.JournalCreated,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{
|
||||||
|
"journal_name": self.name,
|
||||||
|
"filename": filename,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
text = self._load(filename)
|
text = self._load(filename)
|
||||||
self.entries = self._parse(text)
|
self.entries = self._parse(text)
|
||||||
|
@ -269,14 +288,17 @@ class Journal:
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
entry.date = date
|
entry.date = date
|
||||||
|
|
||||||
def prompt_action_entries(self, message):
|
def prompt_action_entries(self, msg: MsgText):
|
||||||
"""Prompts for action for each entry in a journal, using given message.
|
"""Prompts for action for each entry in a journal, using given message.
|
||||||
Returns the entries the user wishes to apply the action on."""
|
Returns the entries the user wishes to apply the action on."""
|
||||||
to_act = []
|
to_act = []
|
||||||
|
|
||||||
def ask_action(entry):
|
def ask_action(entry):
|
||||||
return yesno(
|
return yesno(
|
||||||
f"{message} '{entry.pprint(short=True)}'?",
|
Message(
|
||||||
|
msg,
|
||||||
|
params={"entry_title": entry.pprint(short=True)},
|
||||||
|
),
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -415,9 +437,14 @@ def open_journal(journal_name, config, legacy=False):
|
||||||
|
|
||||||
if os.path.isdir(config["journal"]):
|
if os.path.isdir(config["journal"]):
|
||||||
if config["encrypt"]:
|
if config["encrypt"]:
|
||||||
print(
|
print_msg(
|
||||||
"Warning: This journal's config has 'encrypt' set to true, but this type of journal can't be encrypted.",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.ConfigEncryptedForUnencryptableJournalType,
|
||||||
|
MsgStyle.WARNING,
|
||||||
|
{
|
||||||
|
"journal_name": journal_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
||||||
|
|
16
jrnl/cli.py
16
jrnl/cli.py
|
@ -12,7 +12,7 @@ from jrnl.output import print_msg
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
def configure_logger(debug=False):
|
def configure_logger(debug=False):
|
||||||
|
@ -45,7 +45,13 @@ def cli(manual_args=None):
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
status_code = 1
|
status_code = 1
|
||||||
print_msg("\nKeyboardInterrupt", "\nAborted by user", msg=Message.ERROR)
|
|
||||||
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.KeyboardInterruptMsg,
|
||||||
|
MsgStyle.ERROR_ON_NEW_LINE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# uncaught exception
|
# uncaught exception
|
||||||
|
@ -61,13 +67,15 @@ def cli(manual_args=None):
|
||||||
debug = True
|
debug = True
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
print("\n")
|
from rich.console import Console
|
||||||
|
|
||||||
traceback.print_tb(sys.exc_info()[2])
|
traceback.print_tb(sys.exc_info()[2])
|
||||||
|
Console(stderr=True).print_exception(extra_lines=1)
|
||||||
|
|
||||||
print_msg(
|
print_msg(
|
||||||
Message(
|
Message(
|
||||||
MsgText.UncaughtException,
|
MsgText.UncaughtException,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{"name": type(e).__name__, "exception": e},
|
{"name": type(e).__name__, "exception": e},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,10 +9,6 @@ from .os_compat import on_windows
|
||||||
if on_windows():
|
if on_windows():
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
WARNING_COLOR = colorama.Fore.YELLOW
|
|
||||||
ERROR_COLOR = colorama.Fore.RED
|
|
||||||
RESET_COLOR = colorama.Fore.RESET
|
|
||||||
|
|
||||||
|
|
||||||
def colorize(string, color, bold=False):
|
def colorize(string, color, bold=False):
|
||||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||||
|
|
|
@ -13,10 +13,12 @@ avoid any possible overhead for these standalone commands.
|
||||||
"""
|
"""
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from jrnl.output import print_msg
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
from jrnl.prompt import create_password
|
from jrnl.prompt import create_password
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +79,7 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.CannotEncryptJournalType,
|
MsgText.CannotEncryptJournalType,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{
|
{
|
||||||
"journal_name": args.journal_name,
|
"journal_name": args.journal_name,
|
||||||
"journal_type": journal.__class__.__name__,
|
"journal_type": journal.__class__.__name__,
|
||||||
|
@ -95,9 +97,12 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
||||||
journal.config["encrypt"] = True
|
journal.config["encrypt"] = True
|
||||||
new_journal.write(args.filename)
|
new_journal.write(args.filename)
|
||||||
|
|
||||||
print(
|
print_msg(
|
||||||
f"Journal encrypted to {args.filename or new_journal.config['journal']}.",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.JournalEncryptedTo,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"path": args.filename or new_journal.config["journal"]},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update the config, if we encrypted in place
|
# Update the config, if we encrypted in place
|
||||||
|
@ -120,9 +125,12 @@ def postconfig_decrypt(args, config, original_config, **kwargs):
|
||||||
|
|
||||||
new_journal = PlainJournal.from_journal(journal)
|
new_journal = PlainJournal.from_journal(journal)
|
||||||
new_journal.write(args.filename)
|
new_journal.write(args.filename)
|
||||||
print(
|
print_msg(
|
||||||
f"Journal decrypted to {args.filename or new_journal.config['journal']}.",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.JournalDecryptedTo,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"path": args.filename or new_journal.config["journal"]},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update the config, if we decrypted in place
|
# Update the config, if we decrypted in place
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
import xdg.BaseDirectory
|
import xdg.BaseDirectory
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
from jrnl.output import list_journals
|
||||||
|
from jrnl.output import print_msg
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
from .color import ERROR_COLOR
|
|
||||||
from .color import RESET_COLOR
|
|
||||||
from .output import list_journals
|
|
||||||
from .path import home_dir
|
from .path import home_dir
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
|
@ -75,7 +73,7 @@ def get_config_path():
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.ConfigDirectoryIsFile,
|
MsgText.ConfigDirectoryIsFile,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{
|
{
|
||||||
"config_directory_path": os.path.join(
|
"config_directory_path": os.path.join(
|
||||||
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
|
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
|
||||||
|
@ -143,11 +141,15 @@ def verify_config_colors(config):
|
||||||
if upper_color == "NONE":
|
if upper_color == "NONE":
|
||||||
continue
|
continue
|
||||||
if not getattr(colorama.Fore, upper_color, None):
|
if not getattr(colorama.Fore, upper_color, None):
|
||||||
print(
|
print_msg(
|
||||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
Message(
|
||||||
key, color, ERROR_COLOR, RESET_COLOR
|
MsgText.InvalidColor,
|
||||||
),
|
MsgStyle.NORMAL,
|
||||||
file=sys.stderr,
|
{
|
||||||
|
"key": key,
|
||||||
|
"color": color,
|
||||||
|
},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
all_valid_colors = False
|
all_valid_colors = False
|
||||||
return all_valid_colors
|
return all_valid_colors
|
||||||
|
@ -197,7 +199,7 @@ def get_journal_name(args, config):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.NoDefaultJournal,
|
MsgText.NoDefaultJournal,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{"journals": list_journals(config)},
|
{"journals": list_journals(config)},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from jrnl.output import print_msg
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
def get_text_from_editor(config, template=""):
|
def get_text_from_editor(config, template=""):
|
||||||
|
@ -33,7 +33,7 @@ def get_text_from_editor(config, template=""):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.EditorMisconfigured,
|
MsgText.EditorMisconfigured,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{"editor_key": config["editor"]},
|
{"editor_key": config["editor"]},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -43,7 +43,7 @@ def get_text_from_editor(config, template=""):
|
||||||
os.remove(tmpfile)
|
os.remove(tmpfile)
|
||||||
|
|
||||||
if not raw:
|
if not raw:
|
||||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgType.ERROR))
|
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.ERROR))
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ def get_text_from_stdin():
|
||||||
print_msg(
|
print_msg(
|
||||||
Message(
|
Message(
|
||||||
MsgText.WritingEntryStart,
|
MsgText.WritingEntryStart,
|
||||||
MsgType.TITLE,
|
MsgStyle.TITLE,
|
||||||
{
|
{
|
||||||
"how_to_quit": MsgText.HowToQuitWindows
|
"how_to_quit": MsgText.HowToQuitWindows
|
||||||
if on_windows()
|
if on_windows()
|
||||||
|
@ -66,8 +66,8 @@ def get_text_from_stdin():
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.error("Write mode: keyboard interrupt")
|
logging.error("Write mode: keyboard interrupt")
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
|
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
|
||||||
Message(MsgText.JournalNotSaved, MsgType.WARNING),
|
Message(MsgText.JournalNotSaved, MsgStyle.WARNING),
|
||||||
)
|
)
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
|
|
|
@ -20,10 +20,11 @@ from .config import verify_config_colors
|
||||||
from .prompt import yesno
|
from .prompt import yesno
|
||||||
from .upgrade import is_old_version
|
from .upgrade import is_old_version
|
||||||
|
|
||||||
|
from jrnl.output import print_msg
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
def upgrade_config(config_data, alt_config_path=None):
|
def upgrade_config(config_data, alt_config_path=None):
|
||||||
|
@ -38,9 +39,10 @@ def upgrade_config(config_data, alt_config_path=None):
|
||||||
config_data[key] = default_config[key]
|
config_data[key] = default_config[key]
|
||||||
save_config(config_data, alt_config_path)
|
save_config(config_data, alt_config_path)
|
||||||
config_path = alt_config_path if alt_config_path else get_config_path()
|
config_path = alt_config_path if alt_config_path else get_config_path()
|
||||||
print(
|
print_msg(
|
||||||
f"[Configuration updated to newest version at {config_path}]",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.ConfigUpdated, MsgStyle.NORMAL, {"config_path": config_path}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +59,7 @@ def find_alt_config(alt_config):
|
||||||
if not os.path.exists(alt_config):
|
if not os.path.exists(alt_config):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.AltConfigNotFound, MsgType.ERROR, {"config_file": alt_config}
|
MsgText.AltConfigNotFound, MsgStyle.ERROR, {"config_file": alt_config}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,8 +81,15 @@ def load_or_install_jrnl(alt_config_path):
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
print("Unable to parse config file", file=sys.stderr)
|
raise JrnlException(
|
||||||
sys.exit()
|
Message(
|
||||||
|
MsgText.CantParseConfigFile,
|
||||||
|
MsgStyle.ERROR,
|
||||||
|
{
|
||||||
|
"config_path": config_path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if is_old_version(config_path):
|
if is_old_version(config_path):
|
||||||
from jrnl import upgrade
|
from jrnl import upgrade
|
||||||
|
@ -103,8 +112,17 @@ def install():
|
||||||
|
|
||||||
# Where to create the journal?
|
# Where to create the journal?
|
||||||
default_journal_path = get_default_journal_path()
|
default_journal_path = get_default_journal_path()
|
||||||
path_query = f"Path to your journal file (leave blank for {default_journal_path}): "
|
user_given_path = print_msg(
|
||||||
journal_path = absolute_path(input(path_query).strip() or default_journal_path)
|
Message(
|
||||||
|
MsgText.InstallJournalPathQuestion,
|
||||||
|
MsgStyle.PROMPT,
|
||||||
|
params={
|
||||||
|
"default_journal_path": default_journal_path,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
get_input=True,
|
||||||
|
)
|
||||||
|
journal_path = absolute_path(user_given_path or default_journal_path)
|
||||||
default_config = get_default_config()
|
default_config = get_default_config()
|
||||||
default_config["journals"][DEFAULT_JOURNAL_KEY] = journal_path
|
default_config["journals"][DEFAULT_JOURNAL_KEY] = journal_path
|
||||||
|
|
||||||
|
@ -116,13 +134,10 @@ def install():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Encrypt it?
|
# Encrypt it?
|
||||||
encrypt = yesno(
|
encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False)
|
||||||
"Do you want to encrypt your journal? You can always change this later",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
if encrypt:
|
if encrypt:
|
||||||
default_config["encrypt"] = True
|
default_config["encrypt"] = True
|
||||||
print("Journal will be encrypted.", file=sys.stderr)
|
print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL))
|
||||||
|
|
||||||
save_config(default_config)
|
save_config(default_config)
|
||||||
return default_config
|
return default_config
|
||||||
|
|
63
jrnl/jrnl.py
63
jrnl/jrnl.py
|
@ -14,12 +14,14 @@ from .editor import get_text_from_editor
|
||||||
from .editor import get_text_from_stdin
|
from .editor import get_text_from_stdin
|
||||||
from . import time
|
from . import time
|
||||||
from .override import apply_overrides
|
from .override import apply_overrides
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
from jrnl.output import print_msgs
|
||||||
from .path import expand_path
|
from .path import expand_path
|
||||||
|
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
def run(args):
|
def run(args):
|
||||||
|
@ -139,13 +141,19 @@ def write_mode(args, config, journal, **kwargs):
|
||||||
|
|
||||||
if not raw or raw.isspace():
|
if not raw or raw.isspace():
|
||||||
logging.error("Write mode: couldn't get raw text or entry was empty")
|
logging.error("Write mode: couldn't get raw text or entry was empty")
|
||||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgType.ERROR))
|
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.ERROR))
|
||||||
|
|
||||||
logging.debug(
|
logging.debug(
|
||||||
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
||||||
)
|
)
|
||||||
journal.new_entry(raw)
|
journal.new_entry(raw)
|
||||||
print(f"[Entry added to {args.journal_name} journal]", file=sys.stderr)
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.JournalEntryAdded,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"journal_name": args.journal_name},
|
||||||
|
)
|
||||||
|
)
|
||||||
journal.write()
|
journal.write()
|
||||||
logging.debug("Write mode: completed journal.write()")
|
logging.debug("Write mode: completed journal.write()")
|
||||||
|
|
||||||
|
@ -229,7 +237,7 @@ def _get_editor_template(config, **kwargs):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.CantReadTemplate,
|
MsgText.CantReadTemplate,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{"template": template_path},
|
{"template": template_path},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -277,7 +285,7 @@ def _edit_search_results(config, journal, old_entries, **kwargs):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.EditorNotConfigured,
|
MsgText.EditorNotConfigured,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{"config_file": get_config_path()},
|
{"config_file": get_config_path()},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -307,40 +315,45 @@ def _print_edited_summary(journal, old_stats, **kwargs):
|
||||||
"deleted": old_stats["count"] - len(journal),
|
"deleted": old_stats["count"] - len(journal),
|
||||||
"modified": len([e for e in journal.entries if e.modified]),
|
"modified": len([e for e in journal.entries if e.modified]),
|
||||||
}
|
}
|
||||||
|
stats["modified"] -= stats["added"]
|
||||||
prompts = []
|
msgs = []
|
||||||
|
|
||||||
if stats["added"] > 0:
|
if stats["added"] > 0:
|
||||||
prompts.append(f"{stats['added']} {_pluralize_entry(stats['added'])} added")
|
my_msg = (
|
||||||
stats["modified"] -= stats["added"]
|
MsgText.JournalCountAddedSingular
|
||||||
|
if stats["added"] == 1
|
||||||
|
else MsgText.JournalCountAddedPlural
|
||||||
|
)
|
||||||
|
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["added"]}))
|
||||||
|
|
||||||
if stats["deleted"] > 0:
|
if stats["deleted"] > 0:
|
||||||
prompts.append(
|
my_msg = (
|
||||||
f"{stats['deleted']} {_pluralize_entry(stats['deleted'])} deleted"
|
MsgText.JournalCountDeletedSingular
|
||||||
|
if stats["deleted"] == 1
|
||||||
|
else MsgText.JournalCountDeletedPlural
|
||||||
)
|
)
|
||||||
|
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["deleted"]}))
|
||||||
|
|
||||||
if stats["modified"]:
|
if stats["modified"] > 0:
|
||||||
prompts.append(
|
my_msg = (
|
||||||
f"{stats['modified']} {_pluralize_entry(stats['modified'])} modified"
|
MsgText.JournalCountModifiedSingular
|
||||||
|
if stats["modified"] == 1
|
||||||
|
else MsgText.JournalCountModifiedPlural
|
||||||
)
|
)
|
||||||
|
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["modified"]}))
|
||||||
|
|
||||||
if prompts:
|
print_msgs(msgs)
|
||||||
print(f"[{', '.join(prompts).capitalize()}]", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_predit_stats(journal):
|
def _get_predit_stats(journal):
|
||||||
return {"count": len(journal)}
|
return {"count": len(journal)}
|
||||||
|
|
||||||
|
|
||||||
def _pluralize_entry(num):
|
|
||||||
return "entry" if num == 1 else "entries"
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_search_results(journal, old_entries, **kwargs):
|
def _delete_search_results(journal, old_entries, **kwargs):
|
||||||
if not journal.entries:
|
if not journal.entries:
|
||||||
raise JrnlException(Message(MsgText.NothingToDelete, MsgType.ERROR))
|
raise JrnlException(Message(MsgText.NothingToDelete, MsgStyle.ERROR))
|
||||||
|
|
||||||
entries_to_delete = journal.prompt_action_entries("Delete entry")
|
entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion)
|
||||||
|
|
||||||
if entries_to_delete:
|
if entries_to_delete:
|
||||||
journal.entries = old_entries
|
journal.entries = old_entries
|
||||||
|
@ -351,7 +364,7 @@ def _delete_search_results(journal, old_entries, **kwargs):
|
||||||
|
|
||||||
def _change_time_search_results(args, journal, old_entries, no_prompt=False, **kwargs):
|
def _change_time_search_results(args, journal, old_entries, no_prompt=False, **kwargs):
|
||||||
if not journal.entries:
|
if not journal.entries:
|
||||||
raise JrnlException(Message(MsgText.NothingToModify, MsgType.WARNING))
|
raise JrnlException(Message(MsgText.NothingToModify, MsgStyle.WARNING))
|
||||||
|
|
||||||
# separate entries we are not editing
|
# separate entries we are not editing
|
||||||
other_entries = _other_entries(journal, old_entries)
|
other_entries = _other_entries(journal, old_entries)
|
||||||
|
@ -359,7 +372,9 @@ def _change_time_search_results(args, journal, old_entries, no_prompt=False, **k
|
||||||
if no_prompt:
|
if no_prompt:
|
||||||
entries_to_change = journal.entries
|
entries_to_change = journal.entries
|
||||||
else:
|
else:
|
||||||
entries_to_change = journal.prompt_action_entries("Change time")
|
entries_to_change = journal.prompt_action_entries(
|
||||||
|
MsgText.ChangeTimeEntryQuestion
|
||||||
|
)
|
||||||
|
|
||||||
if entries_to_change:
|
if entries_to_change:
|
||||||
other_entries += [e for e in journal.entries if e not in entries_to_change]
|
other_entries += [e for e in journal.entries if e not in entries_to_change]
|
||||||
|
|
141
jrnl/messages.py
141
jrnl/messages.py
|
@ -1,141 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
from typing import NamedTuple
|
|
||||||
from typing import Mapping
|
|
||||||
|
|
||||||
|
|
||||||
class _MsgColor(NamedTuple):
|
|
||||||
# This is a colorama color, and colorama doesn't support enums or type hints
|
|
||||||
# see: https://github.com/tartley/colorama/issues/91
|
|
||||||
color: str
|
|
||||||
|
|
||||||
|
|
||||||
class MsgType(Enum):
|
|
||||||
TITLE = _MsgColor("cyan")
|
|
||||||
NORMAL = _MsgColor("white")
|
|
||||||
WARNING = _MsgColor("yellow")
|
|
||||||
ERROR = _MsgColor("red")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color(self) -> _MsgColor:
|
|
||||||
return self.value.color
|
|
||||||
|
|
||||||
|
|
||||||
class MsgText(Enum):
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
# --- Exceptions ---#
|
|
||||||
UncaughtException = """
|
|
||||||
{name}
|
|
||||||
{exception}
|
|
||||||
|
|
||||||
This is probably a bug. Please file an issue at:
|
|
||||||
https://github.com/jrnl-org/jrnl/issues/new/choose
|
|
||||||
"""
|
|
||||||
|
|
||||||
ConfigDirectoryIsFile = """
|
|
||||||
The path to your jrnl configuration directory is a file, not a directory:
|
|
||||||
|
|
||||||
{config_directory_path}
|
|
||||||
|
|
||||||
Removing this file will allow jrnl to save its configuration.
|
|
||||||
"""
|
|
||||||
|
|
||||||
LineWrapTooSmallForDateFormat = """
|
|
||||||
The provided linewrap value of {config_linewrap} is too small by
|
|
||||||
{columns} columns to display the timestamps in the configured time
|
|
||||||
format for journal {journal}.
|
|
||||||
|
|
||||||
You can avoid this error by specifying a linewrap value that is larger
|
|
||||||
by at least {columns} in the configuration file or by using
|
|
||||||
--config-override at the command line
|
|
||||||
"""
|
|
||||||
|
|
||||||
CannotEncryptJournalType = """
|
|
||||||
The journal {journal_name} can't be encrypted because it is a
|
|
||||||
{journal_type} journal.
|
|
||||||
|
|
||||||
To encrypt it, create a new journal referencing a file, export
|
|
||||||
this journal to the new journal, then encrypt the new journal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
KeyboardInterruptMsg = "Aborted by user"
|
|
||||||
|
|
||||||
CantReadTemplate = """
|
|
||||||
Unreadable template
|
|
||||||
Could not read template file at:
|
|
||||||
{template}
|
|
||||||
"""
|
|
||||||
|
|
||||||
NoDefaultJournal = "No default journal configured\n{journals}"
|
|
||||||
|
|
||||||
# --- Journal status ---#
|
|
||||||
JournalNotSaved = "Entry NOT saved to journal"
|
|
||||||
|
|
||||||
# --- Editor ---#
|
|
||||||
WritingEntryStart = """
|
|
||||||
Writing Entry
|
|
||||||
To finish writing, press {how_to_quit} on a blank line.
|
|
||||||
"""
|
|
||||||
HowToQuitWindows = "Ctrl+z and then Enter"
|
|
||||||
HowToQuitLinux = "Ctrl+d"
|
|
||||||
|
|
||||||
EditorMisconfigured = """
|
|
||||||
No such file or directory: '{editor_key}'
|
|
||||||
|
|
||||||
Please check the 'editor' key in your config file for errors:
|
|
||||||
editor: '{editor_key}'
|
|
||||||
"""
|
|
||||||
|
|
||||||
EditorNotConfigured = """
|
|
||||||
There is no editor configured
|
|
||||||
|
|
||||||
To use the --edit option, please specify an editor your config file:
|
|
||||||
{config_file}
|
|
||||||
|
|
||||||
For examples of how to configure an external editor, see:
|
|
||||||
https://jrnl.sh/en/stable/external-editors/
|
|
||||||
"""
|
|
||||||
|
|
||||||
NoTextReceived = """
|
|
||||||
No entry to save, because no text was received
|
|
||||||
"""
|
|
||||||
|
|
||||||
# --- Upgrade --- #
|
|
||||||
JournalFailedUpgrade = """
|
|
||||||
The following journal{s} failed to upgrade:
|
|
||||||
{failed_journals}
|
|
||||||
|
|
||||||
Please tell us about this problem at the following URL:
|
|
||||||
https://github.com/jrnl-org/jrnl/issues/new?title=JournalFailedUpgrade
|
|
||||||
"""
|
|
||||||
|
|
||||||
UpgradeAborted = "jrnl was NOT upgraded"
|
|
||||||
|
|
||||||
ImportAborted = "Entries were NOT imported"
|
|
||||||
|
|
||||||
# -- Config --- #
|
|
||||||
AltConfigNotFound = """
|
|
||||||
Alternate configuration file not found at the given path:
|
|
||||||
{config_file}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# --- Password --- #
|
|
||||||
PasswordMaxTriesExceeded = """
|
|
||||||
Too many attempts with wrong password
|
|
||||||
"""
|
|
||||||
|
|
||||||
# --- Search --- #
|
|
||||||
NothingToDelete = """
|
|
||||||
No entries to delete, because the search returned no results
|
|
||||||
"""
|
|
||||||
|
|
||||||
NothingToModify = """
|
|
||||||
No entries to modify, because the search returned no results
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Message(NamedTuple):
|
|
||||||
text: MsgText
|
|
||||||
type: MsgType = MsgType.NORMAL
|
|
||||||
params: Mapping = {}
|
|
11
jrnl/messages/Message.py
Normal file
11
jrnl/messages/Message.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from .MsgText import MsgText
|
||||||
|
from .MsgStyle import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
|
class Message(NamedTuple):
|
||||||
|
text: MsgText
|
||||||
|
style: MsgStyle = MsgStyle.NORMAL
|
||||||
|
params: Mapping = {}
|
89
jrnl/messages/MsgStyle.py
Normal file
89
jrnl/messages/MsgStyle.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
from enum import Enum
|
||||||
|
from typing import NamedTuple
|
||||||
|
from typing import Callable
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich import box
|
||||||
|
|
||||||
|
from .MsgText import MsgText
|
||||||
|
|
||||||
|
|
||||||
|
class MsgStyle(Enum):
|
||||||
|
class _Color(NamedTuple):
|
||||||
|
"""
|
||||||
|
String representing a standard color to display
|
||||||
|
see: https://rich.readthedocs.io/en/stable/appendix/colors.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
color: str
|
||||||
|
|
||||||
|
class _Decoration(Enum):
|
||||||
|
NONE = {
|
||||||
|
"callback": lambda x, **_: x,
|
||||||
|
"args": {},
|
||||||
|
}
|
||||||
|
BOX = {
|
||||||
|
"callback": Panel,
|
||||||
|
"args": {
|
||||||
|
"expand": False,
|
||||||
|
"padding": (0, 2),
|
||||||
|
"title_align": "left",
|
||||||
|
"box": box.HEAVY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def callback(self) -> Callable:
|
||||||
|
return self.value["callback"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def args(self) -> dict:
|
||||||
|
return self.value["args"]
|
||||||
|
|
||||||
|
PROMPT = {
|
||||||
|
"decoration": _Decoration.NONE,
|
||||||
|
"color": _Color("white"),
|
||||||
|
"append_space": True,
|
||||||
|
}
|
||||||
|
TITLE = {
|
||||||
|
"decoration": _Decoration.BOX,
|
||||||
|
"color": _Color("cyan"),
|
||||||
|
}
|
||||||
|
NORMAL = {
|
||||||
|
"decoration": _Decoration.BOX,
|
||||||
|
"color": _Color("white"),
|
||||||
|
}
|
||||||
|
WARNING = {
|
||||||
|
"decoration": _Decoration.BOX,
|
||||||
|
"color": _Color("yellow"),
|
||||||
|
}
|
||||||
|
ERROR = {
|
||||||
|
"decoration": _Decoration.BOX,
|
||||||
|
"color": _Color("red"),
|
||||||
|
"box_title": str(MsgText.Error),
|
||||||
|
}
|
||||||
|
ERROR_ON_NEW_LINE = {
|
||||||
|
"decoration": _Decoration.BOX,
|
||||||
|
"color": _Color("red"),
|
||||||
|
"prepend_newline": True,
|
||||||
|
"box_title": str(MsgText.Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decoration(self) -> _Decoration:
|
||||||
|
return self.value["decoration"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self) -> _Color:
|
||||||
|
return self.value["color"].color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prepend_newline(self) -> bool:
|
||||||
|
return self.value.get("prepend_newline", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def append_space(self) -> bool:
|
||||||
|
return self.value.get("append_space", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def box_title(self) -> MsgText:
|
||||||
|
return self.value.get("box_title", None)
|
248
jrnl/messages/MsgText.py
Normal file
248
jrnl/messages/MsgText.py
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MsgText(Enum):
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
# -- Welcome --- #
|
||||||
|
WelcomeToJrnl = """
|
||||||
|
Welcome to jrnl {version}!
|
||||||
|
|
||||||
|
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
|
||||||
|
you can enjoy all of the great new features that come with jrnl 2:
|
||||||
|
|
||||||
|
- Support for storing your journal in multiple files
|
||||||
|
- Faster reading and writing for large journals
|
||||||
|
- New encryption back-end that makes installing jrnl much easier
|
||||||
|
- Tons of bug fixes
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AllDoneUpgrade = "We're all done here and you can start enjoying jrnl 2"
|
||||||
|
|
||||||
|
# --- Prompts --- #
|
||||||
|
InstallJournalPathQuestion = """
|
||||||
|
Path to your journal file (leave blank for {default_journal_path}):
|
||||||
|
"""
|
||||||
|
DeleteEntryQuestion = "Delete entry '{entry_title}'?"
|
||||||
|
ChangeTimeEntryQuestion = "Change time for '{entry_title}'?"
|
||||||
|
EncryptJournalQuestion = """
|
||||||
|
Do you want to encrypt your journal? (You can always change this later)
|
||||||
|
"""
|
||||||
|
YesOrNoPromptDefaultYes = "[Y/n]"
|
||||||
|
YesOrNoPromptDefaultNo = "[y/N]"
|
||||||
|
ContinueUpgrade = "Continue upgrading jrnl?"
|
||||||
|
|
||||||
|
# these should be lowercase, if possible in language
|
||||||
|
# "lowercase" means whatever `.lower()` returns
|
||||||
|
OneCharacterYes = "y"
|
||||||
|
OneCharacterNo = "n"
|
||||||
|
|
||||||
|
# --- Exceptions ---#
|
||||||
|
Error = "Error"
|
||||||
|
UncaughtException = """
|
||||||
|
{name}
|
||||||
|
{exception}
|
||||||
|
|
||||||
|
This is probably a bug. Please file an issue at:
|
||||||
|
https://github.com/jrnl-org/jrnl/issues/new/choose
|
||||||
|
"""
|
||||||
|
|
||||||
|
ConfigDirectoryIsFile = """
|
||||||
|
Problem with config file!
|
||||||
|
The path to your jrnl configuration directory is a file, not a directory:
|
||||||
|
|
||||||
|
{config_directory_path}
|
||||||
|
|
||||||
|
Removing this file will allow jrnl to save its configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CantParseConfigFile = """
|
||||||
|
Unable to parse config file at:
|
||||||
|
{config_path}
|
||||||
|
"""
|
||||||
|
|
||||||
|
LineWrapTooSmallForDateFormat = """
|
||||||
|
The provided linewrap value of {config_linewrap} is too small by
|
||||||
|
{columns} columns to display the timestamps in the configured time
|
||||||
|
format for journal {journal}.
|
||||||
|
|
||||||
|
You can avoid this error by specifying a linewrap value that is larger
|
||||||
|
by at least {columns} in the configuration file or by using
|
||||||
|
--config-override at the command line
|
||||||
|
"""
|
||||||
|
|
||||||
|
CannotEncryptJournalType = """
|
||||||
|
The journal {journal_name} can't be encrypted because it is a
|
||||||
|
{journal_type} journal.
|
||||||
|
|
||||||
|
To encrypt it, create a new journal referencing a file, export
|
||||||
|
this journal to the new journal, then encrypt the new journal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ConfigEncryptedForUnencryptableJournalType = """
|
||||||
|
The config for journal "{journal_name}" has 'encrypt' set to true, but this type
|
||||||
|
of journal can't be encrypted. Please fix your config file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
KeyboardInterruptMsg = "Aborted by user"
|
||||||
|
|
||||||
|
CantReadTemplate = """
|
||||||
|
Unreadable template
|
||||||
|
Could not read template file at:
|
||||||
|
{template}
|
||||||
|
"""
|
||||||
|
|
||||||
|
NoDefaultJournal = "No default journal configured\n{journals}"
|
||||||
|
|
||||||
|
DoesNotExist = "{name} does not exist"
|
||||||
|
|
||||||
|
# --- Journal status ---#
|
||||||
|
JournalNotSaved = "Entry NOT saved to journal"
|
||||||
|
JournalEntryAdded = "Entry added to {journal_name} journal"
|
||||||
|
|
||||||
|
JournalCountAddedSingular = "{num} entry added"
|
||||||
|
JournalCountModifiedSingular = "{num} entry modified"
|
||||||
|
JournalCountDeletedSingular = "{num} entry deleted"
|
||||||
|
|
||||||
|
JournalCountAddedPlural = "{num} entries added"
|
||||||
|
JournalCountModifiedPlural = "{num} entries modified"
|
||||||
|
JournalCountDeletedPlural = "{num} entries deleted"
|
||||||
|
|
||||||
|
JournalCreated = "Journal '{journal_name}' created at {filename}"
|
||||||
|
DirectoryCreated = "Directory {directory_name} created"
|
||||||
|
JournalEncrypted = "Journal will be encrypted"
|
||||||
|
JournalEncryptedTo = "Journal encrypted to {path}"
|
||||||
|
JournalDecryptedTo = "Journal decrypted to {path}"
|
||||||
|
BackupCreated = "Created a backup at {filename}"
|
||||||
|
|
||||||
|
# --- Editor ---#
|
||||||
|
WritingEntryStart = """
|
||||||
|
Writing Entry
|
||||||
|
To finish writing, press {how_to_quit} on a blank line.
|
||||||
|
"""
|
||||||
|
HowToQuitWindows = "Ctrl+z and then Enter"
|
||||||
|
HowToQuitLinux = "Ctrl+d"
|
||||||
|
|
||||||
|
EditorMisconfigured = """
|
||||||
|
No such file or directory: '{editor_key}'
|
||||||
|
|
||||||
|
Please check the 'editor' key in your config file for errors:
|
||||||
|
editor: '{editor_key}'
|
||||||
|
"""
|
||||||
|
|
||||||
|
EditorNotConfigured = """
|
||||||
|
There is no editor configured
|
||||||
|
|
||||||
|
To use the --edit option, please specify an editor your config file:
|
||||||
|
{config_file}
|
||||||
|
|
||||||
|
For examples of how to configure an external editor, see:
|
||||||
|
https://jrnl.sh/en/stable/external-editors/
|
||||||
|
"""
|
||||||
|
|
||||||
|
NoTextReceived = """
|
||||||
|
No entry to save, because no text was received
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Upgrade --- #
|
||||||
|
JournalFailedUpgrade = """
|
||||||
|
The following journal{s} failed to upgrade:
|
||||||
|
{failed_journals}
|
||||||
|
|
||||||
|
Please tell us about this problem at the following URL:
|
||||||
|
https://github.com/jrnl-org/jrnl/issues/new?title=JournalFailedUpgrade
|
||||||
|
"""
|
||||||
|
|
||||||
|
UpgradeAborted = "jrnl was NOT upgraded"
|
||||||
|
|
||||||
|
AbortingUpgrade = "Aborting upgrade..."
|
||||||
|
|
||||||
|
ImportAborted = "Entries were NOT imported"
|
||||||
|
|
||||||
|
JournalsToUpgrade = """
|
||||||
|
The following journals will be upgraded to jrnl {version}:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
JournalsToIgnore = """
|
||||||
|
The following journals will not be touched:
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
UpgradingJournal = """
|
||||||
|
Upgrading '{journal_name}' journal stored in {path}...
|
||||||
|
"""
|
||||||
|
|
||||||
|
UpgradingConfig = "Upgrading config..."
|
||||||
|
|
||||||
|
PaddedJournalName = "{journal_name:{pad}} -> {path}"
|
||||||
|
|
||||||
|
# -- Config --- #
|
||||||
|
AltConfigNotFound = """
|
||||||
|
Alternate configuration file not found at the given path:
|
||||||
|
{config_file}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ConfigUpdated = """
|
||||||
|
Configuration updated to newest version at {config_path}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Password --- #
|
||||||
|
Password = "Password:"
|
||||||
|
PasswordFirstEntry = "Enter password for journal '{journal_name}': "
|
||||||
|
PasswordConfirmEntry = "Enter password again: "
|
||||||
|
PasswordMaxTriesExceeded = "Too many attempts with wrong password"
|
||||||
|
PasswordCanNotBeEmpty = "Password can't be empty!"
|
||||||
|
PasswordDidNotMatch = "Passwords did not match, please try again"
|
||||||
|
WrongPasswordTryAgain = "Wrong password, try again"
|
||||||
|
PasswordStoreInKeychain = "Do you want to store the password in your keychain?"
|
||||||
|
|
||||||
|
# --- Search --- #
|
||||||
|
NothingToDelete = """
|
||||||
|
No entries to delete, because the search returned no results
|
||||||
|
"""
|
||||||
|
|
||||||
|
NothingToModify = """
|
||||||
|
No entries to modify, because the search returned no results
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Formats --- #
|
||||||
|
HeadingsPastH6 = """
|
||||||
|
Headings increased past H6 on export - {date} {title}
|
||||||
|
"""
|
||||||
|
|
||||||
|
YamlMustBeDirectory = """
|
||||||
|
YAML export must be to a directory, not a single file
|
||||||
|
"""
|
||||||
|
|
||||||
|
JournalExportedTo = "Journal exported to {path}"
|
||||||
|
|
||||||
|
# --- Import --- #
|
||||||
|
ImportSummary = """
|
||||||
|
{count} imported to {journal_name} journal
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Color --- #
|
||||||
|
InvalidColor = "{key} set to invalid color: {color}"
|
||||||
|
|
||||||
|
# --- Keyring --- #
|
||||||
|
KeyringBackendNotFound = """
|
||||||
|
Keyring backend not found.
|
||||||
|
|
||||||
|
Please install one of the supported backends by visiting:
|
||||||
|
https://pypi.org/project/keyring/
|
||||||
|
"""
|
||||||
|
|
||||||
|
KeyringRetrievalFailure = "Failed to retrieve keyring"
|
||||||
|
|
||||||
|
# --- Deprecation --- #
|
||||||
|
DeprecatedCommand = """
|
||||||
|
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
||||||
|
Please use {new_cmd} instead.
|
||||||
|
"""
|
7
jrnl/messages/__init__.py
Normal file
7
jrnl/messages/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from .Message import Message
|
||||||
|
from .MsgStyle import MsgStyle
|
||||||
|
from .MsgText import MsgText
|
||||||
|
|
||||||
|
Message = Message
|
||||||
|
MsgStyle = MsgStyle
|
||||||
|
MsgText = MsgText
|
|
@ -1,25 +1,24 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from jrnl.color import colorize
|
from typing import Union
|
||||||
from jrnl.color import RESET_COLOR
|
from rich.text import Text
|
||||||
from jrnl.color import WARNING_COLOR
|
from rich.console import Console
|
||||||
|
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
|
from jrnl.messages import MsgStyle
|
||||||
|
from jrnl.messages import MsgText
|
||||||
|
|
||||||
|
|
||||||
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
|
def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
|
||||||
|
print_msg(
|
||||||
warning_msg = f"""
|
Message(
|
||||||
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
MsgText.DeprecatedCommand,
|
||||||
Please use {new_cmd} instead.
|
MsgStyle.WARNING,
|
||||||
"""
|
{"old_cmd": old_cmd, "new_cmd": new_cmd},
|
||||||
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:
|
if callback is not None:
|
||||||
callback(**kwargs)
|
callback(**kwargs)
|
||||||
|
@ -38,14 +37,56 @@ def list_journals(configuration):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def print_msg(msg: Message):
|
def print_msg(msg: Message, **kwargs) -> Union[None, str]:
|
||||||
msg_text = textwrap.dedent(msg.text.value.format(**msg.params)).strip().split("\n")
|
"""Helper function to print a single message"""
|
||||||
|
kwargs["style"] = msg.style
|
||||||
|
return print_msgs([msg], **kwargs)
|
||||||
|
|
||||||
longest_string = len(max(msg_text, key=len))
|
|
||||||
msg_text = [f"[ {line:<{longest_string}} ]" for line in msg_text]
|
|
||||||
|
|
||||||
# colorize can't be called until after the lines are padded,
|
def print_msgs(
|
||||||
# because python gets confused by the ansi color codes
|
msgs: list[Message],
|
||||||
msg_text[0] = f"[{colorize(msg_text[0][1:-1], msg.type.color)}]"
|
delimiter: str = "\n",
|
||||||
|
style: MsgStyle = MsgStyle.NORMAL,
|
||||||
|
get_input: bool = False,
|
||||||
|
hide_input: bool = False,
|
||||||
|
) -> Union[None, str]:
|
||||||
|
# Same as print_msg, but for a list
|
||||||
|
text = Text("", end="")
|
||||||
|
kwargs = style.decoration.args
|
||||||
|
|
||||||
print("\n".join(msg_text), file=sys.stderr)
|
for i, msg in enumerate(msgs):
|
||||||
|
kwargs = _add_extra_style_args_if_needed(kwargs, msg=msg)
|
||||||
|
|
||||||
|
m = format_msg_text(msg)
|
||||||
|
|
||||||
|
if i != len(msgs) - 1:
|
||||||
|
m.append(delimiter)
|
||||||
|
|
||||||
|
text.append(m)
|
||||||
|
|
||||||
|
if style.append_space:
|
||||||
|
text.append(" ")
|
||||||
|
|
||||||
|
decorated_text = style.decoration.callback(text, **kwargs)
|
||||||
|
|
||||||
|
# Always print messages to stderr
|
||||||
|
console = _get_console(stderr=True)
|
||||||
|
|
||||||
|
if get_input:
|
||||||
|
return str(console.input(prompt=decorated_text, password=hide_input))
|
||||||
|
console.print(decorated_text, new_line_start=style.prepend_newline)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_console(stderr: bool = True) -> Console:
|
||||||
|
return Console(stderr=stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_extra_style_args_if_needed(args, msg):
|
||||||
|
args["border_style"] = msg.style.color
|
||||||
|
args["title"] = msg.style.box_title
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def format_msg_text(msg: Message) -> Text:
|
||||||
|
text = textwrap.dedent(msg.text.value.format(**msg.params)).strip()
|
||||||
|
return Text(text)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
|
@ -90,7 +90,7 @@ def check_provided_linewrap_viability(linewrap, card, journal):
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(
|
Message(
|
||||||
MsgText.LineWrapTooSmallForDateFormat,
|
MsgText.LineWrapTooSmallForDateFormat,
|
||||||
MsgType.NORMAL,
|
MsgStyle.NORMAL,
|
||||||
{
|
{
|
||||||
"config_linewrap": linewrap,
|
"config_linewrap": linewrap,
|
||||||
"columns": width_violation,
|
"columns": width_violation,
|
||||||
|
|
|
@ -7,7 +7,8 @@ import sys
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
|
||||||
|
|
||||||
class JRNLImporter:
|
class JRNLImporter:
|
||||||
|
@ -28,14 +29,20 @@ class JRNLImporter:
|
||||||
other_journal_txt = sys.stdin.read()
|
other_journal_txt = sys.stdin.read()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
|
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
|
||||||
Message(MsgText.ImportAborted, MsgType.WARNING),
|
Message(MsgText.ImportAborted, MsgStyle.WARNING),
|
||||||
)
|
)
|
||||||
|
|
||||||
journal.import_(other_journal_txt)
|
journal.import_(other_journal_txt)
|
||||||
new_cnt = len(journal.entries)
|
new_cnt = len(journal.entries)
|
||||||
print(
|
|
||||||
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
journal.write()
|
journal.write()
|
||||||
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.ImportSummary,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{
|
||||||
|
"count": new_cnt - old_cnt,
|
||||||
|
"journal_name": journal.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -4,13 +4,14 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from jrnl.color import RESET_COLOR
|
|
||||||
from jrnl.color import WARNING_COLOR
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
|
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
from jrnl.messages import Message
|
||||||
|
from jrnl.messages import MsgText
|
||||||
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
class MarkdownExporter(TextExporter):
|
class MarkdownExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into Markdown."""
|
"""This Exporter can convert entries and journals into Markdown."""
|
||||||
|
@ -63,10 +64,12 @@ class MarkdownExporter(TextExporter):
|
||||||
newbody = newbody + os.linesep
|
newbody = newbody + os.linesep
|
||||||
|
|
||||||
if warn_on_heading_level is True:
|
if warn_on_heading_level is True:
|
||||||
print(
|
print_msg(
|
||||||
f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
Message(
|
||||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
MsgText.HeadingsPastH6,
|
||||||
file=sys.stderr,
|
MsgStyle.WARNING,
|
||||||
|
{"date": date_str, "title": entry.title},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||||
|
|
|
@ -6,8 +6,10 @@ import os
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from jrnl.color import ERROR_COLOR
|
from jrnl.output import print_msg
|
||||||
from jrnl.color import RESET_COLOR
|
from jrnl.messages import Message
|
||||||
|
from jrnl.messages import MsgText
|
||||||
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
class TextExporter:
|
class TextExporter:
|
||||||
|
@ -29,14 +31,18 @@ class TextExporter:
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_file(cls, journal, path):
|
def write_file(cls, journal, path):
|
||||||
"""Exports a journal into a single file."""
|
"""Exports a journal into a single file."""
|
||||||
try:
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_journal(journal))
|
f.write(cls.export_journal(journal))
|
||||||
return f"[Journal exported to {path}]"
|
print_msg(
|
||||||
except IOError as e:
|
Message(
|
||||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
MsgText.JournalExportedTo,
|
||||||
except RuntimeError as e:
|
MsgStyle.NORMAL,
|
||||||
return e
|
{
|
||||||
|
"path": path,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_filename(cls, entry):
|
def make_filename(cls, entry):
|
||||||
|
@ -48,17 +54,17 @@ class TextExporter:
|
||||||
def write_files(cls, journal, path):
|
def write_files(cls, journal, path):
|
||||||
"""Exports a journal into individual files for each entry."""
|
"""Exports a journal into individual files for each entry."""
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
try:
|
|
||||||
full_path = os.path.join(path, cls.make_filename(entry))
|
full_path = os.path.join(path, cls.make_filename(entry))
|
||||||
with open(full_path, "w", encoding="utf-8") as f:
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_entry(entry))
|
f.write(cls.export_entry(entry))
|
||||||
except IOError as e:
|
print_msg(
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
Message(
|
||||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
MsgText.JournalExportedTo,
|
||||||
|
MsgStyle.NORMAL,
|
||||||
|
{"path": path},
|
||||||
)
|
)
|
||||||
except RuntimeError as e:
|
)
|
||||||
return e
|
return ""
|
||||||
return "[Journal exported to {}]".format(path)
|
|
||||||
|
|
||||||
def _slugify(string):
|
def _slugify(string):
|
||||||
"""Slugifies a string.
|
"""Slugifies a string.
|
||||||
|
|
|
@ -4,14 +4,15 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
from jrnl.color import ERROR_COLOR
|
|
||||||
from jrnl.color import RESET_COLOR
|
|
||||||
from jrnl.color import WARNING_COLOR
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
|
|
||||||
|
from jrnl.exception import JrnlException
|
||||||
|
from jrnl.messages import Message
|
||||||
|
from jrnl.messages import MsgText
|
||||||
|
from jrnl.messages import MsgStyle
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
|
||||||
|
|
||||||
class YAMLExporter(TextExporter):
|
class YAMLExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||||
|
@ -23,10 +24,7 @@ class YAMLExporter(TextExporter):
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
||||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||||
if to_multifile is False:
|
if to_multifile is False:
|
||||||
raise RuntimeError(
|
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
||||||
f"{ERROR_COLOR}ERROR{RESET_COLOR}: YAML export must be to individual files. Please \
|
|
||||||
specify a directory to export to."
|
|
||||||
)
|
|
||||||
|
|
||||||
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_wrapper = "\n" if entry.body else ""
|
||||||
|
@ -78,11 +76,12 @@ class YAMLExporter(TextExporter):
|
||||||
spacebody = spacebody + "\t" + line
|
spacebody = spacebody + "\t" + line
|
||||||
|
|
||||||
if warn_on_heading_level is True:
|
if warn_on_heading_level is True:
|
||||||
print(
|
print_msg(
|
||||||
"{}WARNING{}: Headings increased past H6 on export - {} {}".format(
|
Message(
|
||||||
WARNING_COLOR, RESET_COLOR, date_str, entry.title
|
MsgText.HeadingsPastH6,
|
||||||
),
|
MsgStyle.WARNING,
|
||||||
file=sys.stderr,
|
{"date": date_str, "title": entry.title},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
dayone_attributes = ""
|
dayone_attributes = ""
|
||||||
|
@ -129,8 +128,4 @@ class YAMLExporter(TextExporter):
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns an error, as YAML export requires a directory as a target."""
|
"""Returns an error, as YAML export requires a directory as a target."""
|
||||||
raise RuntimeError(
|
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
||||||
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
|
||||||
ERROR_COLOR, RESET_COLOR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,32 +1,65 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
from jrnl.messages import Message
|
||||||
import getpass
|
from jrnl.messages import MsgText
|
||||||
import sys
|
from jrnl.messages import MsgStyle
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
from jrnl.output import print_msgs
|
||||||
|
|
||||||
|
|
||||||
def create_password(journal_name: str) -> str:
|
def create_password(journal_name: str) -> str:
|
||||||
|
kwargs = {
|
||||||
prompt = f"Enter password for journal '{journal_name}': "
|
"get_input": True,
|
||||||
|
"hide_input": True,
|
||||||
|
}
|
||||||
while True:
|
while True:
|
||||||
pw = getpass.getpass(prompt)
|
pw = print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.PasswordFirstEntry,
|
||||||
|
MsgStyle.PROMPT,
|
||||||
|
params={"journal_name": journal_name},
|
||||||
|
),
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if not pw:
|
if not pw:
|
||||||
print("Password can't be an empty string!", file=sys.stderr)
|
print_msg(Message(MsgText.PasswordCanNotBeEmpty, MsgStyle.WARNING))
|
||||||
continue
|
continue
|
||||||
elif pw == getpass.getpass("Enter password again: "):
|
|
||||||
|
elif pw == print_msg(
|
||||||
|
Message(MsgText.PasswordConfirmEntry, MsgStyle.PROMPT), **kwargs
|
||||||
|
):
|
||||||
break
|
break
|
||||||
|
|
||||||
print("Passwords did not match, please try again", file=sys.stderr)
|
print_msg(Message(MsgText.PasswordDidNotMatch, MsgStyle.ERROR))
|
||||||
|
|
||||||
if yesno("Do you want to store the password in your keychain?", default=True):
|
if yesno(Message(MsgText.PasswordStoreInKeychain), default=True):
|
||||||
from .EncryptedJournal import set_keychain
|
from .EncryptedJournal import set_keychain
|
||||||
|
|
||||||
set_keychain(journal_name, pw)
|
set_keychain(journal_name, pw)
|
||||||
|
|
||||||
return pw
|
return pw
|
||||||
|
|
||||||
|
|
||||||
def yesno(prompt, default=True):
|
def yesno(prompt: Message, default: bool = True) -> bool:
|
||||||
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
response = print_msgs(
|
||||||
response = input(prompt)
|
[
|
||||||
return {"y": True, "n": False}.get(response.lower().strip(), default)
|
prompt,
|
||||||
|
Message(
|
||||||
|
MsgText.YesOrNoPromptDefaultYes
|
||||||
|
if default
|
||||||
|
else MsgText.YesOrNoPromptDefaultNo
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style=MsgStyle.PROMPT,
|
||||||
|
delimiter=" ",
|
||||||
|
get_input=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
answers = {
|
||||||
|
str(MsgText.OneCharacterYes): True,
|
||||||
|
str(MsgText.OneCharacterNo): False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does using `lower()` work in all languages?
|
||||||
|
return answers.get(str(response).lower().strip(), default)
|
||||||
|
|
151
jrnl/upgrade.py
151
jrnl/upgrade.py
|
@ -2,7 +2,6 @@
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import Journal
|
from . import Journal
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
@ -14,15 +13,14 @@ from .prompt import yesno
|
||||||
from .path import expand_path
|
from .path import expand_path
|
||||||
|
|
||||||
from jrnl.output import print_msg
|
from jrnl.output import print_msg
|
||||||
|
from jrnl.output import print_msgs
|
||||||
from jrnl.exception import JrnlException
|
from jrnl.exception import JrnlException
|
||||||
from jrnl.messages import Message
|
from jrnl.messages import Message
|
||||||
from jrnl.messages import MsgText
|
from jrnl.messages import MsgText
|
||||||
from jrnl.messages import MsgType
|
from jrnl.messages import MsgStyle
|
||||||
|
|
||||||
|
|
||||||
def backup(filename, binary=False):
|
def backup(filename, binary=False):
|
||||||
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
|
||||||
filename = expand_path(filename)
|
filename = expand_path(filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -31,11 +29,18 @@ def backup(filename, binary=False):
|
||||||
|
|
||||||
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)
|
backup.write(contents)
|
||||||
|
|
||||||
|
print_msg(
|
||||||
|
Message(
|
||||||
|
MsgText.BackupCreated, MsgStyle.NORMAL, {"filename": "filename.backup"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(f"\nError: {filename} does not exist.")
|
print_msg(Message(MsgText.DoesNotExist, MsgStyle.WARNING, {"name": filename}))
|
||||||
cont = yesno(f"\nCreate {filename}?", default=False)
|
cont = yesno(f"\nCreate {filename}?", default=False)
|
||||||
if not cont:
|
if not cont:
|
||||||
raise JrnlException(Message(MsgText.UpgradeAborted), MsgType.WARNING)
|
raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING))
|
||||||
|
|
||||||
|
|
||||||
def check_exists(path):
|
def check_exists(path):
|
||||||
|
@ -48,23 +53,7 @@ def check_exists(path):
|
||||||
def upgrade_jrnl(config_path):
|
def upgrade_jrnl(config_path):
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
|
|
||||||
print(
|
print_msg(Message(MsgText.WelcomeToJrnl, MsgStyle.NORMAL, {"version": __version__}))
|
||||||
f"""Welcome to jrnl {__version__}.
|
|
||||||
|
|
||||||
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
|
|
||||||
you can enjoy all of the great new features that come with jrnl 2:
|
|
||||||
|
|
||||||
- Support for storing your journal in multiple files
|
|
||||||
- Faster reading and writing for large journals
|
|
||||||
- New encryption back-end that makes installing jrnl much easier
|
|
||||||
- Tons of bug fixes
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
encrypted_journals = {}
|
encrypted_journals = {}
|
||||||
plain_journals = {}
|
plain_journals = {}
|
||||||
|
@ -79,8 +68,10 @@ older versions of jrnl anymore.
|
||||||
encrypt = config.get("encrypt")
|
encrypt = config.get("encrypt")
|
||||||
path = expand_path(journal_conf)
|
path = expand_path(journal_conf)
|
||||||
|
|
||||||
if not os.path.exists(path):
|
if os.path.exists(path):
|
||||||
print(f"\nError: {path} does not exist.")
|
path = os.path.expanduser(path)
|
||||||
|
else:
|
||||||
|
print_msg(Message(MsgText.DoesNotExist, MsgStyle.ERROR, {"name": path}))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if encrypt:
|
if encrypt:
|
||||||
|
@ -90,46 +81,54 @@ older versions of jrnl anymore.
|
||||||
else:
|
else:
|
||||||
plain_journals[journal_name] = path
|
plain_journals[journal_name] = path
|
||||||
|
|
||||||
longest_journal_name = max([len(journal) for journal in config["journals"]])
|
kwargs = {
|
||||||
if encrypted_journals:
|
# longest journal name
|
||||||
print(
|
"pad": max([len(journal) for journal in config["journals"]]),
|
||||||
f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:",
|
}
|
||||||
file=sys.stderr,
|
|
||||||
)
|
_print_journal_summary(
|
||||||
for journal, path in encrypted_journals.items():
|
journals=encrypted_journals,
|
||||||
print(
|
header=Message(
|
||||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
MsgText.JournalsToUpgrade,
|
||||||
file=sys.stderr,
|
params={
|
||||||
|
"version": __version__,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if plain_journals:
|
_print_journal_summary(
|
||||||
print(
|
journals=plain_journals,
|
||||||
f"\nFollowing plain text journals will upgraded to jrnl {__version__}:",
|
header=Message(
|
||||||
file=sys.stderr,
|
MsgText.JournalsToUpgrade,
|
||||||
)
|
params={
|
||||||
for journal, path in plain_journals.items():
|
"version": __version__,
|
||||||
print(
|
},
|
||||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
),
|
||||||
file=sys.stderr,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if other_journals:
|
_print_journal_summary(
|
||||||
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
journals=other_journals,
|
||||||
for journal, path in other_journals.items():
|
header=Message(MsgText.JournalsToIgnore),
|
||||||
print(
|
**kwargs,
|
||||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cont = yesno("\nContinue upgrading jrnl?", default=False)
|
cont = yesno(Message(MsgText.ContinueUpgrade), default=False)
|
||||||
if not cont:
|
if not cont:
|
||||||
raise JrnlException(Message(MsgText.UpgradeAborted), MsgType.WARNING)
|
raise JrnlException(Message(MsgText.UpgradeAborted), MsgStyle.WARNING)
|
||||||
|
|
||||||
for journal_name, path in encrypted_journals.items():
|
for journal_name, path in encrypted_journals.items():
|
||||||
print(
|
print_msg(
|
||||||
f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.UpgradingJournal,
|
||||||
|
params={
|
||||||
|
"journal_name": journal_name,
|
||||||
|
"path": path,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
backup(path, binary=True)
|
backup(path, binary=True)
|
||||||
old_journal = Journal.open_journal(
|
old_journal = Journal.open_journal(
|
||||||
journal_name, scope_config(config, journal_name), legacy=True
|
journal_name, scope_config(config, journal_name), legacy=True
|
||||||
|
@ -137,10 +136,16 @@ older versions of jrnl anymore.
|
||||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||||
|
|
||||||
for journal_name, path in plain_journals.items():
|
for journal_name, path in plain_journals.items():
|
||||||
print(
|
print_msg(
|
||||||
f"\nUpgrading plain text '{journal_name}' journal stored in {path}...",
|
Message(
|
||||||
file=sys.stderr,
|
MsgText.UpgradingJournal,
|
||||||
|
params={
|
||||||
|
"journal_name": journal_name,
|
||||||
|
"path": path,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
backup(path)
|
backup(path)
|
||||||
old_journal = Journal.open_journal(
|
old_journal = Journal.open_journal(
|
||||||
journal_name, scope_config(config, journal_name), legacy=True
|
journal_name, scope_config(config, journal_name), legacy=True
|
||||||
|
@ -151,29 +156,47 @@ older versions of jrnl anymore.
|
||||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||||
|
|
||||||
if len(failed_journals) > 0:
|
if len(failed_journals) > 0:
|
||||||
print_msg("Aborting upgrade.", msg=Message.NORMAL)
|
|
||||||
|
|
||||||
raise JrnlException(
|
raise JrnlException(
|
||||||
|
Message(MsgText.AbortingUpgrade, MsgStyle.WARNING),
|
||||||
Message(
|
Message(
|
||||||
MsgText.JournalFailedUpgrade,
|
MsgText.JournalFailedUpgrade,
|
||||||
MsgType.ERROR,
|
MsgStyle.ERROR,
|
||||||
{
|
{
|
||||||
"s": "s" if len(failed_journals) > 1 else "",
|
"s": "s" if len(failed_journals) > 1 else "",
|
||||||
"failed_journals": "\n".join(j.name for j in failed_journals),
|
"failed_journals": "\n".join(j.name for j in failed_journals),
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# write all journals - or - don't
|
# write all journals - or - don't
|
||||||
for j in all_journals:
|
for j in all_journals:
|
||||||
j.write()
|
j.write()
|
||||||
|
|
||||||
print("\nUpgrading config...", file=sys.stderr)
|
print_msg(Message(MsgText.UpgradingConfig, MsgStyle.NORMAL))
|
||||||
|
|
||||||
backup(config_path)
|
backup(config_path)
|
||||||
|
|
||||||
print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
|
print_msg(Message(MsgText.AllDoneUpgrade, MsgStyle.NORMAL))
|
||||||
|
|
||||||
|
|
||||||
def is_old_version(config_path):
|
def is_old_version(config_path):
|
||||||
return is_config_json(config_path)
|
return is_config_json(config_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_journal_summary(journals: dict, header: Message, pad: int) -> None:
|
||||||
|
if not journals:
|
||||||
|
return
|
||||||
|
|
||||||
|
msgs = [header]
|
||||||
|
for journal, path in journals.items():
|
||||||
|
msgs.append(
|
||||||
|
Message(
|
||||||
|
MsgText.PaddedJournalName,
|
||||||
|
params={
|
||||||
|
"journal_name": journal,
|
||||||
|
"path": path,
|
||||||
|
"pad": pad,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print_msgs(msgs)
|
||||||
|
|
|
@ -49,6 +49,7 @@ tzlocal = ">2.0, <3.0" # https://github.com/regebro/tzlocal/blob/master/CHANGE
|
||||||
pytest = { version = ">=6.2", optional = true }
|
pytest = { version = ">=6.2", optional = true }
|
||||||
pytest-bdd = { version = ">=4.0.1", optional = true }
|
pytest-bdd = { version = ">=4.0.1", optional = true }
|
||||||
toml = { version = ">=0.10", optional = true }
|
toml = { version = ">=0.10", optional = true }
|
||||||
|
rich = "^12.2.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
mkdocs = ">=1.0,<1.3"
|
mkdocs = ">=1.0,<1.3"
|
||||||
|
|
|
@ -73,7 +73,7 @@ Feature: Multiple journals
|
||||||
these three eyes
|
these three eyes
|
||||||
these three eyes
|
these three eyes
|
||||||
n
|
n
|
||||||
Then the output should contain "Encrypted journal 'new_encrypted' created"
|
Then the output should contain "Journal 'new_encrypted' created at "
|
||||||
|
|
||||||
Scenario: Don't overwrite main config when encrypting a journal in an alternate config
|
Scenario: Don't overwrite main config when encrypting a journal in an alternate config
|
||||||
Given the config "basic_onefile.yaml" exists
|
Given the config "basic_onefile.yaml" exists
|
||||||
|
|
|
@ -69,7 +69,7 @@ Feature: Reading and writing to journal with custom date formats
|
||||||
|
|
||||||
Scenario: Writing an entry at the prompt with custom date
|
Scenario: Writing an entry at the prompt with custom date
|
||||||
Given we use the config "little_endian_dates.yaml"
|
Given we use the config "little_endian_dates.yaml"
|
||||||
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
|
When we run "jrnl" and type "2013-05-10: I saw Elvis. He's alive."
|
||||||
Then we should get no error
|
Then we should get no error
|
||||||
When we run "jrnl -999"
|
When we run "jrnl -999"
|
||||||
Then the output should contain "10.05.2013 09:00 I saw Elvis."
|
Then the output should contain "10.05.2013 09:00 I saw Elvis."
|
||||||
|
|
|
@ -2,8 +2,8 @@ Feature: Encrypting and decrypting journals
|
||||||
|
|
||||||
Scenario: Decrypting a journal
|
Scenario: Decrypting a journal
|
||||||
Given we use the config "encrypted.yaml"
|
Given we use the config "encrypted.yaml"
|
||||||
# And we use the password "bad doggie no biscuit" if prompted
|
And we use the password "bad doggie no biscuit" if prompted
|
||||||
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
When we run "jrnl --decrypt"
|
||||||
Then the output should contain "Journal decrypted"
|
Then the output should contain "Journal decrypted"
|
||||||
And the config for journal "default" should contain "encrypt: false"
|
And the config for journal "default" should contain "encrypt: false"
|
||||||
When we run "jrnl -99 --short"
|
When we run "jrnl -99 --short"
|
||||||
|
@ -47,7 +47,7 @@ Feature: Encrypting and decrypting journals
|
||||||
Scenario Outline: Running jrnl with encrypt: true on unencryptable journals
|
Scenario Outline: Running jrnl with encrypt: true on unencryptable journals
|
||||||
Given we use the config "<config_file>"
|
Given we use the config "<config_file>"
|
||||||
When we run "jrnl --config-override encrypt true here is a new entry"
|
When we run "jrnl --config-override encrypt true here is a new entry"
|
||||||
Then the error output should contain "this type of journal can't be encrypted"
|
Then the error output should contain "journal can't be encrypted"
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
|
|
@ -429,7 +429,7 @@ Feature: Custom formats
|
||||||
Given we use the config "<config_file>"
|
Given we use the config "<config_file>"
|
||||||
And we use the password "test" if prompted
|
And we use the password "test" if prompted
|
||||||
When we run "jrnl --export yaml --file nonexistent_dir"
|
When we run "jrnl --export yaml --file nonexistent_dir"
|
||||||
Then the output should contain "YAML export must be to individual files"
|
Then the output should contain "YAML export must be to a directory"
|
||||||
And the output should not contain "Traceback"
|
And the output should not contain "Traceback"
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
|
|
|
@ -87,7 +87,7 @@ Feature: Multiple journals
|
||||||
these three eyes
|
these three eyes
|
||||||
these three eyes
|
these three eyes
|
||||||
n
|
n
|
||||||
Then the output should contain "Encrypted journal 'new_encrypted' created"
|
Then the output should contain "Journal 'new_encrypted' created at"
|
||||||
|
|
||||||
Scenario: Read and write to journal with emoji name
|
Scenario: Read and write to journal with emoji name
|
||||||
Given we use the config "multiple.yaml"
|
Given we use the config "multiple.yaml"
|
||||||
|
|
|
@ -3,7 +3,7 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
|
||||||
Scenario: Override configured editor with built-in input === editor:''
|
Scenario: Override configured editor with built-in input === editor:''
|
||||||
Given we use the config "basic_encrypted.yaml"
|
Given we use the config "basic_encrypted.yaml"
|
||||||
And we use the password "test" if prompted
|
And we use the password "test" if prompted
|
||||||
When we run "jrnl --config-override editor ''" and enter
|
When we run "jrnl --config-override editor ''" and type
|
||||||
This is a journal entry
|
This is a journal entry
|
||||||
Then the stdin prompt should have been called
|
Then the stdin prompt should have been called
|
||||||
And the editor should not have been called
|
And the editor should not have been called
|
||||||
|
|
|
@ -29,7 +29,8 @@ Feature: Starring entries
|
||||||
|
|
||||||
Scenario: Starring an entry will mark it in an encrypted journal
|
Scenario: Starring an entry will mark it in an encrypted journal
|
||||||
Given we use the config "encrypted.yaml"
|
Given we use the config "encrypted.yaml"
|
||||||
When we run "jrnl 20 july 2013 *: Best day of my life!" and enter "bad doggie no biscuit"
|
And we use the password "bad doggie no biscuit" if prompted
|
||||||
|
When we run "jrnl 20 july 2013 *: Best day of my life!"
|
||||||
Then the output should contain "Entry added"
|
Then the output should contain "Entry added"
|
||||||
When we run "jrnl -on 2013-07-20 -starred" and enter "bad doggie no biscuit"
|
When we run "jrnl -on 2013-07-20 -starred" and enter "bad doggie no biscuit"
|
||||||
Then the output should contain "2013-07-20 09:00 Best day of my life!"
|
Then the output should contain "2013-07-20 09:00 Best day of my life!"
|
||||||
|
|
|
@ -41,7 +41,7 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||||
Scenario: Upgrade with missing journal
|
Scenario: Upgrade with missing journal
|
||||||
Given we use the config "upgrade_from_195_with_missing_journal.json"
|
Given we use the config "upgrade_from_195_with_missing_journal.json"
|
||||||
When we run "jrnl --list" and enter "Y"
|
When we run "jrnl --list" and enter "Y"
|
||||||
Then the output should contain "Error: features/journals/missing.journal does not exist."
|
Then the output should contain "features/journals/missing.journal does not exist"
|
||||||
And we should get no error
|
And we should get no error
|
||||||
|
|
||||||
Scenario: Upgrade with missing encrypted journal
|
Scenario: Upgrade with missing encrypted journal
|
||||||
|
@ -49,6 +49,6 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||||
When we run "jrnl --list" and enter
|
When we run "jrnl --list" and enter
|
||||||
Y
|
Y
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
Then the output should contain "Error: features/journals/missing.journal does not exist."
|
Then the output should contain "features/journals/missing.journal does not exist"
|
||||||
And the output should contain "We're all done"
|
And the output should contain "We're all done"
|
||||||
And we should get no error
|
And we should get no error
|
||||||
|
|
|
@ -172,7 +172,7 @@ Feature: Writing new entries.
|
||||||
Scenario Outline: Writing an entry at the prompt (no editor) should store the entry
|
Scenario Outline: Writing an entry at the prompt (no editor) should store the entry
|
||||||
Given we use the config "<config_file>"
|
Given we use the config "<config_file>"
|
||||||
And we use the password "bad doggie no biscuit" if prompted
|
And we use the password "bad doggie no biscuit" if prompted
|
||||||
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
When we run "jrnl" and type "25 jul 2013: I saw Elvis. He's alive."
|
||||||
Then we should get no error
|
Then we should get no error
|
||||||
When we run "jrnl -on '2013-07-25'"
|
When we run "jrnl -on '2013-07-25'"
|
||||||
Then the output should contain "2013-07-25 09:00 I saw Elvis."
|
Then the output should contain "2013-07-25 09:00 I saw Elvis."
|
||||||
|
@ -233,8 +233,7 @@ Feature: Writing new entries.
|
||||||
And we append to the editor if opened
|
And we append to the editor if opened
|
||||||
[2021-11-13] worked on jrnl tests
|
[2021-11-13] worked on jrnl tests
|
||||||
When we run "jrnl --edit"
|
When we run "jrnl --edit"
|
||||||
Then the output should contain
|
Then the output should contain "1 entry added"
|
||||||
[1 entry added]
|
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
@ -252,8 +251,7 @@ Feature: Writing new entries.
|
||||||
[2021-11-12] worked on jrnl tests again
|
[2021-11-12] worked on jrnl tests again
|
||||||
[2021-11-13] worked on jrnl tests a little bit more
|
[2021-11-13] worked on jrnl tests a little bit more
|
||||||
When we run "jrnl --edit"
|
When we run "jrnl --edit"
|
||||||
Then the output should contain
|
Then the error output should contain "3 entries added"
|
||||||
[3 entries added]
|
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
@ -269,8 +267,8 @@ Feature: Writing new entries.
|
||||||
And we write to the editor if opened
|
And we write to the editor if opened
|
||||||
[2021-11-13] I am replacing my whole journal with this entry
|
[2021-11-13] I am replacing my whole journal with this entry
|
||||||
When we run "jrnl --edit"
|
When we run "jrnl --edit"
|
||||||
Then the output should contain
|
Then the output should contain "2 entries deleted"
|
||||||
[2 entries deleted, 1 entry modified]
|
Then the output should contain "3 entries modified"
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
@ -287,7 +285,7 @@ Feature: Writing new entries.
|
||||||
[2021-11-13] I am replacing the last entry with this entry
|
[2021-11-13] I am replacing the last entry with this entry
|
||||||
When we run "jrnl --edit -1"
|
When we run "jrnl --edit -1"
|
||||||
Then the output should contain
|
Then the output should contain
|
||||||
[1 entry modified]
|
1 entry modified
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
@ -304,7 +302,7 @@ Feature: Writing new entries.
|
||||||
This is a small addendum to my latest entry.
|
This is a small addendum to my latest entry.
|
||||||
When we run "jrnl --edit"
|
When we run "jrnl --edit"
|
||||||
Then the output should contain
|
Then the output should contain
|
||||||
[1 entry modified]
|
1 entry modified
|
||||||
|
|
||||||
Examples: configs
|
Examples: configs
|
||||||
| config_file |
|
| config_file |
|
||||||
|
|
|
@ -6,12 +6,15 @@ import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from keyring import backend
|
from keyring import backend
|
||||||
from keyring import errors
|
from keyring import errors
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from unittest.mock import Mock
|
||||||
from .helpers import get_fixture
|
from .helpers import get_fixture
|
||||||
import toml
|
import toml
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
from jrnl.config import load_config
|
from jrnl.config import load_config
|
||||||
from jrnl.os_compat import split_args
|
from jrnl.os_compat import split_args
|
||||||
|
@ -85,7 +88,6 @@ def cli_run(
|
||||||
mock_editor,
|
mock_editor,
|
||||||
mock_user_input,
|
mock_user_input,
|
||||||
mock_overrides,
|
mock_overrides,
|
||||||
mock_password,
|
|
||||||
):
|
):
|
||||||
# Check if we need more mocks
|
# Check if we need more mocks
|
||||||
mock_factories.update(mock_args)
|
mock_factories.update(mock_args)
|
||||||
|
@ -94,7 +96,6 @@ def cli_run(
|
||||||
mock_factories.update(mock_editor)
|
mock_factories.update(mock_editor)
|
||||||
mock_factories.update(mock_config_path)
|
mock_factories.update(mock_config_path)
|
||||||
mock_factories.update(mock_user_input)
|
mock_factories.update(mock_user_input)
|
||||||
mock_factories.update(mock_password)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": 0,
|
"status": 0,
|
||||||
|
@ -180,26 +181,6 @@ def toml_version(working_dir):
|
||||||
return pyproject_contents["tool"]["poetry"]["version"]
|
return pyproject_contents["tool"]["poetry"]["version"]
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
|
||||||
def mock_password(request):
|
|
||||||
def _mock_password():
|
|
||||||
password = get_fixture(request, "password")
|
|
||||||
user_input = get_fixture(request, "user_input")
|
|
||||||
|
|
||||||
if password:
|
|
||||||
password = password.splitlines()
|
|
||||||
|
|
||||||
elif user_input:
|
|
||||||
password = user_input.splitlines()
|
|
||||||
|
|
||||||
if not password:
|
|
||||||
password = Exception("Unexpected call for password")
|
|
||||||
|
|
||||||
return patch("getpass.getpass", side_effect=password)
|
|
||||||
|
|
||||||
return {"getpass": _mock_password}
|
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def input_method():
|
def input_method():
|
||||||
return ""
|
return ""
|
||||||
|
@ -221,30 +202,58 @@ def should_not():
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def mock_user_input(request, is_tty):
|
def mock_user_input(request, password_input, stdin_input):
|
||||||
def _generator(target):
|
|
||||||
def _mock_user_input():
|
def _mock_user_input():
|
||||||
user_input = get_fixture(request, "user_input", None)
|
# user_input needs to be here because we don't know it until cli_run starts
|
||||||
|
user_input = get_fixture(request, "all_input", None)
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
user_input = Exception("Unexpected call for user input")
|
user_input = Exception("Unexpected call for user input")
|
||||||
else:
|
else:
|
||||||
user_input = user_input.splitlines() if is_tty else [user_input]
|
user_input = iter(user_input.splitlines())
|
||||||
|
|
||||||
return patch(target, side_effect=user_input)
|
def mock_console_input(**kwargs):
|
||||||
|
if kwargs["password"] and not isinstance(password_input, Exception):
|
||||||
|
return password_input
|
||||||
|
|
||||||
return _mock_user_input
|
if isinstance(user_input, Iterable):
|
||||||
|
return next(user_input)
|
||||||
|
|
||||||
|
# exceptions
|
||||||
|
return user_input if not kwargs["password"] else password_input
|
||||||
|
|
||||||
|
mock_console = Mock(wraps=Console(stderr=True))
|
||||||
|
mock_console.input = Mock(side_effect=mock_console_input)
|
||||||
|
|
||||||
|
return patch("jrnl.output._get_console", return_value=mock_console)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"stdin": _generator("sys.stdin.read"),
|
"user_input": _mock_user_input,
|
||||||
"input": _generator("builtins.input"),
|
"stdin_input": lambda: patch("sys.stdin.read", side_effect=stdin_input),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@fixture
|
||||||
|
def password_input(request):
|
||||||
|
password_input = get_fixture(request, "password", None)
|
||||||
|
if password_input is None:
|
||||||
|
password_input = Exception("Unexpected call for password input")
|
||||||
|
return password_input
|
||||||
|
|
||||||
|
|
||||||
|
@fixture
|
||||||
|
def stdin_input(request, is_tty):
|
||||||
|
stdin_input = get_fixture(request, "all_input", None)
|
||||||
|
if stdin_input is None or is_tty:
|
||||||
|
stdin_input = Exception("Unexpected call for stdin input")
|
||||||
|
else:
|
||||||
|
stdin_input = [stdin_input]
|
||||||
|
return stdin_input
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def is_tty(input_method):
|
def is_tty(input_method):
|
||||||
assert input_method in ["", "enter", "pipe"]
|
assert input_method in ["", "enter", "pipe", "type"]
|
||||||
return input_method != "pipe"
|
return input_method not in ["pipe", "type"]
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
|
|
|
@ -120,9 +120,9 @@ def config_exists(config_file, temp_dir, working_dir):
|
||||||
shutil.copy2(config_source, config_dest)
|
shutil.copy2(config_source, config_dest)
|
||||||
|
|
||||||
|
|
||||||
@given(parse('we use the password "{pw}" if prompted'), target_fixture="password")
|
@given(parse('we use the password "{password}" if prompted'))
|
||||||
def use_password_forever(pw):
|
def use_password_forever(password):
|
||||||
return pw
|
return password
|
||||||
|
|
||||||
|
|
||||||
@given("we create a cache directory", target_fixture="cache_dir")
|
@given("we create a cache directory", target_fixture="cache_dir")
|
||||||
|
|
|
@ -47,20 +47,23 @@ def output_should_contain(
|
||||||
):
|
):
|
||||||
we_should = parse_should_or_should_not(should_or_should_not)
|
we_should = parse_should_or_should_not(should_or_should_not)
|
||||||
|
|
||||||
|
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}"
|
||||||
assert expected_output
|
assert expected_output
|
||||||
if which_output_stream is None:
|
if which_output_stream is None:
|
||||||
assert ((expected_output in cli_run["stdout"]) == we_should) or (
|
assert ((expected_output in cli_run["stdout"]) == we_should) or (
|
||||||
(expected_output in cli_run["stderr"]) == we_should
|
(expected_output in cli_run["stderr"]) == we_should
|
||||||
)
|
), output_str
|
||||||
|
|
||||||
elif which_output_stream == "standard":
|
elif which_output_stream == "standard":
|
||||||
assert (expected_output in cli_run["stdout"]) == we_should
|
assert (expected_output in cli_run["stdout"]) == we_should, output_str
|
||||||
|
|
||||||
elif which_output_stream == "error":
|
elif which_output_stream == "error":
|
||||||
assert (expected_output in cli_run["stderr"]) == we_should
|
assert (expected_output in cli_run["stderr"]) == we_should, output_str
|
||||||
|
|
||||||
else:
|
else:
|
||||||
assert (expected_output in cli_run[which_output_stream]) == we_should
|
assert (
|
||||||
|
expected_output in cli_run[which_output_stream]
|
||||||
|
) == we_should, output_str
|
||||||
|
|
||||||
|
|
||||||
@then(parse("the output should not contain\n{expected_output}"))
|
@then(parse("the output should not contain\n{expected_output}"))
|
||||||
|
@ -164,12 +167,12 @@ def config_var_in_memory(
|
||||||
|
|
||||||
@then("we should be prompted for a password")
|
@then("we should be prompted for a password")
|
||||||
def password_was_called(cli_run):
|
def password_was_called(cli_run):
|
||||||
assert cli_run["mocks"]["getpass"].called
|
assert cli_run["mocks"]["user_input"].called
|
||||||
|
|
||||||
|
|
||||||
@then("we should not be prompted for a password")
|
@then("we should not be prompted for a password")
|
||||||
def password_was_not_called(cli_run):
|
def password_was_not_called(cli_run):
|
||||||
assert not cli_run["mocks"]["getpass"].called
|
assert not cli_run["mocks"]["user_input"].called
|
||||||
|
|
||||||
|
|
||||||
@then(parse("the cache directory should contain the files\n{file_list}"))
|
@then(parse("the cache directory should contain the files\n{file_list}"))
|
||||||
|
@ -371,7 +374,7 @@ def count_editor_args(num_args, cli_run, editor_state, should_or_should_not):
|
||||||
def stdin_prompt_called(cli_run, should_or_should_not):
|
def stdin_prompt_called(cli_run, should_or_should_not):
|
||||||
we_should = parse_should_or_should_not(should_or_should_not)
|
we_should = parse_should_or_should_not(should_or_should_not)
|
||||||
|
|
||||||
assert cli_run["mocks"]["stdin"].called == we_should
|
assert cli_run["mocks"]["stdin_input"].called == we_should
|
||||||
|
|
||||||
|
|
||||||
@then(parse('the editor filename should end with "{suffix}"'))
|
@then(parse('the editor filename should end with "{suffix}"'))
|
||||||
|
|
|
@ -21,12 +21,12 @@ def when_we_change_directory(directory_name):
|
||||||
|
|
||||||
# These variables are used in the `@when(re(...))` section below
|
# These variables are used in the `@when(re(...))` section below
|
||||||
command = '(?P<command>[^"]*)'
|
command = '(?P<command>[^"]*)'
|
||||||
input_method = "(?P<input_method>enter|pipe)"
|
input_method = "(?P<input_method>enter|pipe|type)"
|
||||||
user_input = '("(?P<user_input>[^"]*)")'
|
all_input = '("(?P<all_input>[^"]*)")'
|
||||||
|
|
||||||
|
|
||||||
@when(parse('we run "jrnl {command}" and {input_method}\n{user_input}'))
|
@when(parse('we run "jrnl {command}" and {input_method}\n{all_input}'))
|
||||||
@when(re(f'we run "jrnl ?{command}" and {input_method} {user_input}'))
|
@when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}'))
|
||||||
@when(parse('we run "jrnl {command}"'))
|
@when(parse('we run "jrnl {command}"'))
|
||||||
@when('we run "jrnl"')
|
@when('we run "jrnl"')
|
||||||
def we_run_jrnl(cli_run, capsys, keyring):
|
def we_run_jrnl(cli_run, capsys, keyring):
|
||||||
|
|
27
tests/unit/test_output.py
Normal file
27
tests/unit/test_output.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
from unittest.mock import Mock
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from jrnl.messages import Message
|
||||||
|
from jrnl.output import print_msg
|
||||||
|
|
||||||
|
|
||||||
|
@patch("jrnl.output.print_msgs")
|
||||||
|
def test_print_msg_calls_print_msgs_as_list_with_style(print_msgs):
|
||||||
|
test_msg = Mock(Message)
|
||||||
|
print_msg(test_msg)
|
||||||
|
print_msgs.assert_called_once_with([test_msg], style=test_msg.style)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("jrnl.output.print_msgs")
|
||||||
|
def test_print_msg_calls_print_msgs_with_kwargs(print_msgs):
|
||||||
|
test_msg = Mock(Message)
|
||||||
|
kwargs = {
|
||||||
|
"delimter": "test delimiter 🤡",
|
||||||
|
"get_input": True,
|
||||||
|
"hide_input": True,
|
||||||
|
"some_rando_arg": "💩",
|
||||||
|
}
|
||||||
|
print_msg(test_msg, **kwargs)
|
||||||
|
print_msgs.assert_called_once_with([test_msg], style=test_msg.style, **kwargs)
|
Loading…
Add table
Reference in a new issue