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 getpass
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
|
||||
|
@ -24,7 +22,8 @@ from .prompt import create_password
|
|||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
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):
|
||||
|
@ -46,21 +45,26 @@ def decrypt_content(
|
|||
keychain: str = None,
|
||||
max_attempts: int = 3,
|
||||
) -> 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)
|
||||
password = pwd_from_keychain or getpass.getpass()
|
||||
password = pwd_from_keychain or get_pw()
|
||||
result = decrypt_func(password)
|
||||
# Password is bad:
|
||||
if result is None and pwd_from_keychain:
|
||||
set_keychain(keychain, None)
|
||||
attempt = 1
|
||||
while result is None and attempt < max_attempts:
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = getpass.getpass()
|
||||
print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING))
|
||||
password = get_pw()
|
||||
result = decrypt_func(password)
|
||||
attempt += 1
|
||||
|
||||
if result is None:
|
||||
raise JrnlException(Message(MsgText.PasswordMaxTriesExceeded, MsgType.ERROR))
|
||||
raise JrnlException(Message(MsgText.PasswordMaxTriesExceeded, MsgStyle.ERROR))
|
||||
|
||||
return result
|
||||
|
||||
|
@ -79,13 +83,22 @@ class EncryptedJournal(Journal):
|
|||
if not os.path.exists(filename):
|
||||
if not os.path.isdir(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.password = create_password(self.name)
|
||||
|
||||
print(
|
||||
f"Encrypted 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)
|
||||
|
@ -179,7 +192,7 @@ def get_keychain(journal_name):
|
|||
return keyring.get_password("jrnl", journal_name)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if not isinstance(e, keyring.errors.NoKeyringError):
|
||||
print("Failed to retrieve keyring", file=sys.stderr)
|
||||
print_msg(Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR))
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -196,9 +209,7 @@ def set_keychain(journal_name, password):
|
|||
keyring.set_password("jrnl", journal_name, password)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if isinstance(e, keyring.errors.NoKeyringError):
|
||||
print(
|
||||
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
msg = Message(MsgText.KeyringBackendNotFound, MsgStyle.WARNING)
|
||||
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"])
|
||||
for filename in filenames:
|
||||
if os.stat(filename).st_size <= 0:
|
||||
# print("empty file: {}".format(filename))
|
||||
os.remove(filename)
|
||||
|
||||
def delete_entries(self, entries_to_delete):
|
||||
|
|
|
@ -6,13 +6,17 @@ import datetime
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from . import Entry
|
||||
from . import time
|
||||
from .prompt import yesno
|
||||
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:
|
||||
def __init__(self, name, count=0):
|
||||
|
@ -83,9 +87,24 @@ class Journal:
|
|||
if not os.path.exists(filename):
|
||||
if not os.path.isdir(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)
|
||||
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)
|
||||
self.entries = self._parse(text)
|
||||
|
@ -269,14 +288,17 @@ class Journal:
|
|||
for entry in self.entries:
|
||||
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.
|
||||
Returns the entries the user wishes to apply the action on."""
|
||||
to_act = []
|
||||
|
||||
def ask_action(entry):
|
||||
return yesno(
|
||||
f"{message} '{entry.pprint(short=True)}'?",
|
||||
Message(
|
||||
msg,
|
||||
params={"entry_title": entry.pprint(short=True)},
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
@ -415,9 +437,14 @@ def open_journal(journal_name, config, legacy=False):
|
|||
|
||||
if os.path.isdir(config["journal"]):
|
||||
if config["encrypt"]:
|
||||
print(
|
||||
"Warning: This journal's config has 'encrypt' set to true, but this type of journal can't be encrypted.",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.ConfigEncryptedForUnencryptableJournalType,
|
||||
MsgStyle.WARNING,
|
||||
{
|
||||
"journal_name": journal_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
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.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
def configure_logger(debug=False):
|
||||
|
@ -45,7 +45,13 @@ def cli(manual_args=None):
|
|||
|
||||
except KeyboardInterrupt:
|
||||
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:
|
||||
# uncaught exception
|
||||
|
@ -61,13 +67,15 @@ def cli(manual_args=None):
|
|||
debug = True
|
||||
|
||||
if debug:
|
||||
print("\n")
|
||||
from rich.console import Console
|
||||
|
||||
traceback.print_tb(sys.exc_info()[2])
|
||||
Console(stderr=True).print_exception(extra_lines=1)
|
||||
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.UncaughtException,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{"name": type(e).__name__, "exception": e},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -9,10 +9,6 @@ from .os_compat import on_windows
|
|||
if on_windows():
|
||||
colorama.init()
|
||||
|
||||
WARNING_COLOR = colorama.Fore.YELLOW
|
||||
ERROR_COLOR = colorama.Fore.RED
|
||||
RESET_COLOR = colorama.Fore.RESET
|
||||
|
||||
|
||||
def colorize(string, color, bold=False):
|
||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||
|
|
|
@ -13,10 +13,12 @@ avoid any possible overhead for these standalone commands.
|
|||
"""
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.prompt import create_password
|
||||
|
||||
|
||||
|
@ -77,7 +79,7 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CannotEncryptJournalType,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"journal_name": args.journal_name,
|
||||
"journal_type": journal.__class__.__name__,
|
||||
|
@ -95,9 +97,12 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
|||
journal.config["encrypt"] = True
|
||||
new_journal.write(args.filename)
|
||||
|
||||
print(
|
||||
f"Journal encrypted to {args.filename or new_journal.config['journal']}.",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalEncryptedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{"path": args.filename or new_journal.config["journal"]},
|
||||
)
|
||||
)
|
||||
|
||||
# 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.write(args.filename)
|
||||
print(
|
||||
f"Journal decrypted to {args.filename or new_journal.config['journal']}.",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalDecryptedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{"path": args.filename or new_journal.config["journal"]},
|
||||
)
|
||||
)
|
||||
|
||||
# Update the config, if we decrypted in place
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import colorama
|
||||
from ruamel.yaml import YAML
|
||||
import xdg.BaseDirectory
|
||||
|
||||
from . import __version__
|
||||
from jrnl.output import list_journals
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
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
|
||||
|
||||
# Constants
|
||||
|
@ -75,7 +73,7 @@ def get_config_path():
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.ConfigDirectoryIsFile,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"config_directory_path": os.path.join(
|
||||
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
|
||||
|
@ -143,11 +141,15 @@ def verify_config_colors(config):
|
|||
if upper_color == "NONE":
|
||||
continue
|
||||
if not getattr(colorama.Fore, upper_color, None):
|
||||
print(
|
||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
||||
key, color, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.InvalidColor,
|
||||
MsgStyle.NORMAL,
|
||||
{
|
||||
"key": key,
|
||||
"color": color,
|
||||
},
|
||||
)
|
||||
)
|
||||
all_valid_colors = False
|
||||
return all_valid_colors
|
||||
|
@ -197,7 +199,7 @@ def get_journal_name(args, config):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.NoDefaultJournal,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{"journals": list_journals(config)},
|
||||
),
|
||||
)
|
||||
|
|
|
@ -12,7 +12,7 @@ from jrnl.output import print_msg
|
|||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
|
@ -33,7 +33,7 @@ def get_text_from_editor(config, template=""):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.EditorMisconfigured,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{"editor_key": config["editor"]},
|
||||
)
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ def get_text_from_editor(config, template=""):
|
|||
os.remove(tmpfile)
|
||||
|
||||
if not raw:
|
||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgType.ERROR))
|
||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.ERROR))
|
||||
|
||||
return raw
|
||||
|
||||
|
@ -52,7 +52,7 @@ def get_text_from_stdin():
|
|||
print_msg(
|
||||
Message(
|
||||
MsgText.WritingEntryStart,
|
||||
MsgType.TITLE,
|
||||
MsgStyle.TITLE,
|
||||
{
|
||||
"how_to_quit": MsgText.HowToQuitWindows
|
||||
if on_windows()
|
||||
|
@ -66,8 +66,8 @@ def get_text_from_stdin():
|
|||
except KeyboardInterrupt:
|
||||
logging.error("Write mode: keyboard interrupt")
|
||||
raise JrnlException(
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
|
||||
Message(MsgText.JournalNotSaved, MsgType.WARNING),
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
|
||||
Message(MsgText.JournalNotSaved, MsgStyle.WARNING),
|
||||
)
|
||||
|
||||
return raw
|
||||
|
|
|
@ -20,10 +20,11 @@ from .config import verify_config_colors
|
|||
from .prompt import yesno
|
||||
from .upgrade import is_old_version
|
||||
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
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]
|
||||
save_config(config_data, alt_config_path)
|
||||
config_path = alt_config_path if alt_config_path else get_config_path()
|
||||
print(
|
||||
f"[Configuration updated to newest version at {config_path}]",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
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):
|
||||
raise JrnlException(
|
||||
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)
|
||||
|
||||
if config is None:
|
||||
print("Unable to parse config file", file=sys.stderr)
|
||||
sys.exit()
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CantParseConfigFile,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"config_path": config_path,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if is_old_version(config_path):
|
||||
from jrnl import upgrade
|
||||
|
@ -103,8 +112,17 @@ def install():
|
|||
|
||||
# Where to create the journal?
|
||||
default_journal_path = get_default_journal_path()
|
||||
path_query = f"Path to your journal file (leave blank for {default_journal_path}): "
|
||||
journal_path = absolute_path(input(path_query).strip() or default_journal_path)
|
||||
user_given_path = print_msg(
|
||||
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["journals"][DEFAULT_JOURNAL_KEY] = journal_path
|
||||
|
||||
|
@ -116,13 +134,10 @@ def install():
|
|||
pass
|
||||
|
||||
# Encrypt it?
|
||||
encrypt = yesno(
|
||||
"Do you want to encrypt your journal? You can always change this later",
|
||||
default=False,
|
||||
)
|
||||
encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False)
|
||||
if encrypt:
|
||||
default_config["encrypt"] = True
|
||||
print("Journal will be encrypted.", file=sys.stderr)
|
||||
print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL))
|
||||
|
||||
save_config(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 . import time
|
||||
from .override import apply_overrides
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.output import print_msgs
|
||||
from .path import expand_path
|
||||
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
def run(args):
|
||||
|
@ -139,13 +141,19 @@ def write_mode(args, config, journal, **kwargs):
|
|||
|
||||
if not raw or raw.isspace():
|
||||
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(
|
||||
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
||||
)
|
||||
journal.new_entry(raw)
|
||||
print(f"[Entry added to {args.journal_name} journal]", file=sys.stderr)
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalEntryAdded,
|
||||
MsgStyle.NORMAL,
|
||||
{"journal_name": args.journal_name},
|
||||
)
|
||||
)
|
||||
journal.write()
|
||||
logging.debug("Write mode: completed journal.write()")
|
||||
|
||||
|
@ -229,7 +237,7 @@ def _get_editor_template(config, **kwargs):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CantReadTemplate,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{"template": template_path},
|
||||
)
|
||||
)
|
||||
|
@ -277,7 +285,7 @@ def _edit_search_results(config, journal, old_entries, **kwargs):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.EditorNotConfigured,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{"config_file": get_config_path()},
|
||||
)
|
||||
)
|
||||
|
@ -307,40 +315,45 @@ def _print_edited_summary(journal, old_stats, **kwargs):
|
|||
"deleted": old_stats["count"] - len(journal),
|
||||
"modified": len([e for e in journal.entries if e.modified]),
|
||||
}
|
||||
|
||||
prompts = []
|
||||
stats["modified"] -= stats["added"]
|
||||
msgs = []
|
||||
|
||||
if stats["added"] > 0:
|
||||
prompts.append(f"{stats['added']} {_pluralize_entry(stats['added'])} added")
|
||||
stats["modified"] -= stats["added"]
|
||||
my_msg = (
|
||||
MsgText.JournalCountAddedSingular
|
||||
if stats["added"] == 1
|
||||
else MsgText.JournalCountAddedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["added"]}))
|
||||
|
||||
if stats["deleted"] > 0:
|
||||
prompts.append(
|
||||
f"{stats['deleted']} {_pluralize_entry(stats['deleted'])} deleted"
|
||||
my_msg = (
|
||||
MsgText.JournalCountDeletedSingular
|
||||
if stats["deleted"] == 1
|
||||
else MsgText.JournalCountDeletedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["deleted"]}))
|
||||
|
||||
if stats["modified"]:
|
||||
prompts.append(
|
||||
f"{stats['modified']} {_pluralize_entry(stats['modified'])} modified"
|
||||
if stats["modified"] > 0:
|
||||
my_msg = (
|
||||
MsgText.JournalCountModifiedSingular
|
||||
if stats["modified"] == 1
|
||||
else MsgText.JournalCountModifiedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["modified"]}))
|
||||
|
||||
if prompts:
|
||||
print(f"[{', '.join(prompts).capitalize()}]", file=sys.stderr)
|
||||
print_msgs(msgs)
|
||||
|
||||
|
||||
def _get_predit_stats(journal):
|
||||
return {"count": len(journal)}
|
||||
|
||||
|
||||
def _pluralize_entry(num):
|
||||
return "entry" if num == 1 else "entries"
|
||||
|
||||
|
||||
def _delete_search_results(journal, old_entries, **kwargs):
|
||||
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:
|
||||
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):
|
||||
if not journal.entries:
|
||||
raise JrnlException(Message(MsgText.NothingToModify, MsgType.WARNING))
|
||||
raise JrnlException(Message(MsgText.NothingToModify, MsgStyle.WARNING))
|
||||
|
||||
# separate entries we are not editing
|
||||
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:
|
||||
entries_to_change = journal.entries
|
||||
else:
|
||||
entries_to_change = journal.prompt_action_entries("Change time")
|
||||
entries_to_change = journal.prompt_action_entries(
|
||||
MsgText.ChangeTimeEntryQuestion
|
||||
)
|
||||
|
||||
if 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
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from jrnl.color import colorize
|
||||
from jrnl.color import RESET_COLOR
|
||||
from jrnl.color import WARNING_COLOR
|
||||
from typing import Union
|
||||
from rich.text import Text
|
||||
from rich.console import Console
|
||||
|
||||
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):
|
||||
|
||||
warning_msg = f"""
|
||||
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
||||
Please use {new_cmd} instead.
|
||||
"""
|
||||
warning_msg = textwrap.dedent(warning_msg)
|
||||
logging.warning(warning_msg)
|
||||
print(f"{WARNING_COLOR}{warning_msg}{RESET_COLOR}", file=sys.stderr)
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.DeprecatedCommand,
|
||||
MsgStyle.WARNING,
|
||||
{"old_cmd": old_cmd, "new_cmd": new_cmd},
|
||||
)
|
||||
)
|
||||
|
||||
if callback is not None:
|
||||
callback(**kwargs)
|
||||
|
@ -38,14 +37,56 @@ def list_journals(configuration):
|
|||
return result
|
||||
|
||||
|
||||
def print_msg(msg: Message):
|
||||
msg_text = textwrap.dedent(msg.text.value.format(**msg.params)).strip().split("\n")
|
||||
def print_msg(msg: Message, **kwargs) -> Union[None, str]:
|
||||
"""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,
|
||||
# because python gets confused by the ansi color codes
|
||||
msg_text[0] = f"[{colorize(msg_text[0][1:-1], msg.type.color)}]"
|
||||
def print_msgs(
|
||||
msgs: list[Message],
|
||||
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.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
|
@ -90,7 +90,7 @@ def check_provided_linewrap_viability(linewrap, card, journal):
|
|||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.LineWrapTooSmallForDateFormat,
|
||||
MsgType.NORMAL,
|
||||
MsgStyle.NORMAL,
|
||||
{
|
||||
"config_linewrap": linewrap,
|
||||
"columns": width_violation,
|
||||
|
|
|
@ -7,7 +7,8 @@ import sys
|
|||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.output import print_msg
|
||||
|
||||
|
||||
class JRNLImporter:
|
||||
|
@ -28,14 +29,20 @@ class JRNLImporter:
|
|||
other_journal_txt = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
raise JrnlException(
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgType.ERROR),
|
||||
Message(MsgText.ImportAborted, MsgType.WARNING),
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
|
||||
Message(MsgText.ImportAborted, MsgStyle.WARNING),
|
||||
)
|
||||
|
||||
journal.import_(other_journal_txt)
|
||||
new_cnt = len(journal.entries)
|
||||
print(
|
||||
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
||||
file=sys.stderr,
|
||||
)
|
||||
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 re
|
||||
import sys
|
||||
|
||||
from jrnl.color import RESET_COLOR
|
||||
from jrnl.color import WARNING_COLOR
|
||||
|
||||
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):
|
||||
"""This Exporter can convert entries and journals into Markdown."""
|
||||
|
@ -63,10 +64,12 @@ class MarkdownExporter(TextExporter):
|
|||
newbody = newbody + os.linesep
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print(
|
||||
f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.HeadingsPastH6,
|
||||
MsgStyle.WARNING,
|
||||
{"date": date_str, "title": entry.title},
|
||||
)
|
||||
)
|
||||
|
||||
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||
|
|
|
@ -6,8 +6,10 @@ import os
|
|||
import re
|
||||
import unicodedata
|
||||
|
||||
from jrnl.color import ERROR_COLOR
|
||||
from jrnl.color import RESET_COLOR
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
class TextExporter:
|
||||
|
@ -29,14 +31,18 @@ class TextExporter:
|
|||
@classmethod
|
||||
def write_file(cls, journal, path):
|
||||
"""Exports a journal into a single file."""
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_journal(journal))
|
||||
return f"[Journal exported to {path}]"
|
||||
except IOError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
except RuntimeError as e:
|
||||
return e
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalExportedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{
|
||||
"path": path,
|
||||
},
|
||||
)
|
||||
)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
|
@ -48,17 +54,17 @@ class TextExporter:
|
|||
def write_files(cls, journal, path):
|
||||
"""Exports a journal into individual files for each entry."""
|
||||
for entry in journal.entries:
|
||||
try:
|
||||
full_path = os.path.join(path, cls.make_filename(entry))
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_entry(entry))
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalExportedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{"path": path},
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return e
|
||||
return "[Journal exported to {}]".format(path)
|
||||
)
|
||||
return ""
|
||||
|
||||
def _slugify(string):
|
||||
"""Slugifies a string.
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
|
||||
import os
|
||||
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 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):
|
||||
"""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):
|
||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||
if to_multifile is False:
|
||||
raise RuntimeError(
|
||||
f"{ERROR_COLOR}ERROR{RESET_COLOR}: YAML export must be to individual files. Please \
|
||||
specify a directory to export to."
|
||||
)
|
||||
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
||||
|
||||
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
|
@ -78,11 +76,12 @@ class YAMLExporter(TextExporter):
|
|||
spacebody = spacebody + "\t" + line
|
||||
|
||||
if warn_on_heading_level is True:
|
||||
print(
|
||||
"{}WARNING{}: Headings increased past H6 on export - {} {}".format(
|
||||
WARNING_COLOR, RESET_COLOR, date_str, entry.title
|
||||
),
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.HeadingsPastH6,
|
||||
MsgStyle.WARNING,
|
||||
{"date": date_str, "title": entry.title},
|
||||
)
|
||||
)
|
||||
|
||||
dayone_attributes = ""
|
||||
|
@ -129,8 +128,4 @@ class YAMLExporter(TextExporter):
|
|||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns an error, as YAML export requires a directory as a target."""
|
||||
raise RuntimeError(
|
||||
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
||||
ERROR_COLOR, RESET_COLOR
|
||||
)
|
||||
)
|
||||
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
||||
|
|
|
@ -1,32 +1,65 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import getpass
|
||||
import sys
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.output import print_msgs
|
||||
|
||||
|
||||
def create_password(journal_name: str) -> str:
|
||||
|
||||
prompt = f"Enter password for journal '{journal_name}': "
|
||||
|
||||
kwargs = {
|
||||
"get_input": True,
|
||||
"hide_input": True,
|
||||
}
|
||||
while True:
|
||||
pw = getpass.getpass(prompt)
|
||||
pw = print_msg(
|
||||
Message(
|
||||
MsgText.PasswordFirstEntry,
|
||||
MsgStyle.PROMPT,
|
||||
params={"journal_name": journal_name},
|
||||
),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if not pw:
|
||||
print("Password can't be an empty string!", file=sys.stderr)
|
||||
print_msg(Message(MsgText.PasswordCanNotBeEmpty, MsgStyle.WARNING))
|
||||
continue
|
||||
elif pw == getpass.getpass("Enter password again: "):
|
||||
|
||||
elif pw == print_msg(
|
||||
Message(MsgText.PasswordConfirmEntry, MsgStyle.PROMPT), **kwargs
|
||||
):
|
||||
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
|
||||
|
||||
set_keychain(journal_name, pw)
|
||||
|
||||
return pw
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||
response = input(prompt)
|
||||
return {"y": True, "n": False}.get(response.lower().strip(), default)
|
||||
def yesno(prompt: Message, default: bool = True) -> bool:
|
||||
response = print_msgs(
|
||||
[
|
||||
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
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from . import Journal
|
||||
from . import __version__
|
||||
|
@ -14,15 +13,14 @@ from .prompt import yesno
|
|||
from .path import expand_path
|
||||
|
||||
from jrnl.output import print_msg
|
||||
|
||||
from jrnl.output import print_msgs
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.messages import MsgType
|
||||
from jrnl.messages import MsgStyle
|
||||
|
||||
|
||||
def backup(filename, binary=False):
|
||||
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||
filename = expand_path(filename)
|
||||
|
||||
try:
|
||||
|
@ -31,11 +29,18 @@ def backup(filename, binary=False):
|
|||
|
||||
with open(filename + ".backup", "wb" if binary else "w") as backup:
|
||||
backup.write(contents)
|
||||
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.BackupCreated, MsgStyle.NORMAL, {"filename": "filename.backup"}
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
if not cont:
|
||||
raise JrnlException(Message(MsgText.UpgradeAborted), MsgType.WARNING)
|
||||
raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING))
|
||||
|
||||
|
||||
def check_exists(path):
|
||||
|
@ -48,23 +53,7 @@ def check_exists(path):
|
|||
def upgrade_jrnl(config_path):
|
||||
config = load_config(config_path)
|
||||
|
||||
print(
|
||||
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.
|
||||
"""
|
||||
)
|
||||
print_msg(Message(MsgText.WelcomeToJrnl, MsgStyle.NORMAL, {"version": __version__}))
|
||||
|
||||
encrypted_journals = {}
|
||||
plain_journals = {}
|
||||
|
@ -79,8 +68,10 @@ older versions of jrnl anymore.
|
|||
encrypt = config.get("encrypt")
|
||||
path = expand_path(journal_conf)
|
||||
|
||||
if not os.path.exists(path):
|
||||
print(f"\nError: {path} does not exist.")
|
||||
if os.path.exists(path):
|
||||
path = os.path.expanduser(path)
|
||||
else:
|
||||
print_msg(Message(MsgText.DoesNotExist, MsgStyle.ERROR, {"name": path}))
|
||||
continue
|
||||
|
||||
if encrypt:
|
||||
|
@ -90,46 +81,54 @@ older versions of jrnl anymore.
|
|||
else:
|
||||
plain_journals[journal_name] = path
|
||||
|
||||
longest_journal_name = max([len(journal) for journal in config["journals"]])
|
||||
if encrypted_journals:
|
||||
print(
|
||||
f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in encrypted_journals.items():
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
kwargs = {
|
||||
# longest journal name
|
||||
"pad": max([len(journal) for journal in config["journals"]]),
|
||||
}
|
||||
|
||||
_print_journal_summary(
|
||||
journals=encrypted_journals,
|
||||
header=Message(
|
||||
MsgText.JournalsToUpgrade,
|
||||
params={
|
||||
"version": __version__,
|
||||
},
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if plain_journals:
|
||||
print(
|
||||
f"\nFollowing plain text journals will upgraded to jrnl {__version__}:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for journal, path in plain_journals.items():
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
_print_journal_summary(
|
||||
journals=plain_journals,
|
||||
header=Message(
|
||||
MsgText.JournalsToUpgrade,
|
||||
params={
|
||||
"version": __version__,
|
||||
},
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if other_journals:
|
||||
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||
for journal, path in other_journals.items():
|
||||
print(
|
||||
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||
file=sys.stderr,
|
||||
_print_journal_summary(
|
||||
journals=other_journals,
|
||||
header=Message(MsgText.JournalsToIgnore),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
cont = yesno("\nContinue upgrading jrnl?", default=False)
|
||||
cont = yesno(Message(MsgText.ContinueUpgrade), default=False)
|
||||
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():
|
||||
print(
|
||||
f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.UpgradingJournal,
|
||||
params={
|
||||
"journal_name": journal_name,
|
||||
"path": path,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
backup(path, binary=True)
|
||||
old_journal = Journal.open_journal(
|
||||
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))
|
||||
|
||||
for journal_name, path in plain_journals.items():
|
||||
print(
|
||||
f"\nUpgrading plain text '{journal_name}' journal stored in {path}...",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.UpgradingJournal,
|
||||
params={
|
||||
"journal_name": journal_name,
|
||||
"path": path,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
backup(path)
|
||||
old_journal = Journal.open_journal(
|
||||
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()]
|
||||
|
||||
if len(failed_journals) > 0:
|
||||
print_msg("Aborting upgrade.", msg=Message.NORMAL)
|
||||
|
||||
raise JrnlException(
|
||||
Message(MsgText.AbortingUpgrade, MsgStyle.WARNING),
|
||||
Message(
|
||||
MsgText.JournalFailedUpgrade,
|
||||
MsgType.ERROR,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"s": "s" if len(failed_journals) > 1 else "",
|
||||
"failed_journals": "\n".join(j.name for j in failed_journals),
|
||||
},
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# write all journals - or - don't
|
||||
for j in all_journals:
|
||||
j.write()
|
||||
|
||||
print("\nUpgrading config...", file=sys.stderr)
|
||||
print_msg(Message(MsgText.UpgradingConfig, MsgStyle.NORMAL))
|
||||
|
||||
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):
|
||||
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-bdd = { version = ">=4.0.1", optional = true }
|
||||
toml = { version = ">=0.10", optional = true }
|
||||
rich = "^12.2.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mkdocs = ">=1.0,<1.3"
|
||||
|
|
|
@ -73,7 +73,7 @@ Feature: Multiple journals
|
|||
these three eyes
|
||||
these three eyes
|
||||
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
|
||||
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
|
||||
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
|
||||
When we run "jrnl -999"
|
||||
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
|
||||
Given we use the config "encrypted.yaml"
|
||||
# And we use the password "bad doggie no biscuit" if prompted
|
||||
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
When we run "jrnl --decrypt"
|
||||
Then the output should contain "Journal decrypted"
|
||||
And the config for journal "default" should contain "encrypt: false"
|
||||
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
|
||||
Given we use the config "<config_file>"
|
||||
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
|
||||
| config_file |
|
||||
|
|
|
@ -429,7 +429,7 @@ Feature: Custom formats
|
|||
Given we use the config "<config_file>"
|
||||
And we use the password "test" if prompted
|
||||
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"
|
||||
|
||||
Examples: configs
|
||||
|
|
|
@ -87,7 +87,7 @@ Feature: Multiple journals
|
|||
these three eyes
|
||||
these three eyes
|
||||
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
|
||||
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:''
|
||||
Given we use the config "basic_encrypted.yaml"
|
||||
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
|
||||
Then the stdin prompt should 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
|
||||
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"
|
||||
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!"
|
||||
|
|
|
@ -41,7 +41,7 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
|||
Scenario: Upgrade with missing journal
|
||||
Given we use the config "upgrade_from_195_with_missing_journal.json"
|
||||
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
|
||||
|
||||
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
|
||||
Y
|
||||
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 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
|
||||
Given we use the config "<config_file>"
|
||||
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
|
||||
When we run "jrnl -on '2013-07-25'"
|
||||
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
|
||||
[2021-11-13] worked on jrnl tests
|
||||
When we run "jrnl --edit"
|
||||
Then the output should contain
|
||||
[1 entry added]
|
||||
Then the output should contain "1 entry added"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
|
@ -252,8 +251,7 @@ Feature: Writing new entries.
|
|||
[2021-11-12] worked on jrnl tests again
|
||||
[2021-11-13] worked on jrnl tests a little bit more
|
||||
When we run "jrnl --edit"
|
||||
Then the output should contain
|
||||
[3 entries added]
|
||||
Then the error output should contain "3 entries added"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
|
@ -269,8 +267,8 @@ Feature: Writing new entries.
|
|||
And we write to the editor if opened
|
||||
[2021-11-13] I am replacing my whole journal with this entry
|
||||
When we run "jrnl --edit"
|
||||
Then the output should contain
|
||||
[2 entries deleted, 1 entry modified]
|
||||
Then the output should contain "2 entries deleted"
|
||||
Then the output should contain "3 entries modified"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
|
@ -287,7 +285,7 @@ Feature: Writing new entries.
|
|||
[2021-11-13] I am replacing the last entry with this entry
|
||||
When we run "jrnl --edit -1"
|
||||
Then the output should contain
|
||||
[1 entry modified]
|
||||
1 entry modified
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
|
@ -304,7 +302,7 @@ Feature: Writing new entries.
|
|||
This is a small addendum to my latest entry.
|
||||
When we run "jrnl --edit"
|
||||
Then the output should contain
|
||||
[1 entry modified]
|
||||
1 entry modified
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
|
|
|
@ -6,12 +6,15 @@ import os
|
|||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from collections.abc import Iterable
|
||||
from keyring import backend
|
||||
from keyring import errors
|
||||
from pytest import fixture
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock
|
||||
from .helpers import get_fixture
|
||||
import toml
|
||||
from rich.console import Console
|
||||
|
||||
from jrnl.config import load_config
|
||||
from jrnl.os_compat import split_args
|
||||
|
@ -85,7 +88,6 @@ def cli_run(
|
|||
mock_editor,
|
||||
mock_user_input,
|
||||
mock_overrides,
|
||||
mock_password,
|
||||
):
|
||||
# Check if we need more mocks
|
||||
mock_factories.update(mock_args)
|
||||
|
@ -94,7 +96,6 @@ def cli_run(
|
|||
mock_factories.update(mock_editor)
|
||||
mock_factories.update(mock_config_path)
|
||||
mock_factories.update(mock_user_input)
|
||||
mock_factories.update(mock_password)
|
||||
|
||||
return {
|
||||
"status": 0,
|
||||
|
@ -180,26 +181,6 @@ def toml_version(working_dir):
|
|||
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
|
||||
def input_method():
|
||||
return ""
|
||||
|
@ -221,30 +202,58 @@ def should_not():
|
|||
|
||||
|
||||
@fixture
|
||||
def mock_user_input(request, is_tty):
|
||||
def _generator(target):
|
||||
def mock_user_input(request, password_input, stdin_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:
|
||||
user_input = Exception("Unexpected call for user input")
|
||||
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 {
|
||||
"stdin": _generator("sys.stdin.read"),
|
||||
"input": _generator("builtins.input"),
|
||||
"user_input": _mock_user_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
|
||||
def is_tty(input_method):
|
||||
assert input_method in ["", "enter", "pipe"]
|
||||
return input_method != "pipe"
|
||||
assert input_method in ["", "enter", "pipe", "type"]
|
||||
return input_method not in ["pipe", "type"]
|
||||
|
||||
|
||||
@fixture
|
||||
|
|
|
@ -120,9 +120,9 @@ def config_exists(config_file, temp_dir, working_dir):
|
|||
shutil.copy2(config_source, config_dest)
|
||||
|
||||
|
||||
@given(parse('we use the password "{pw}" if prompted'), target_fixture="password")
|
||||
def use_password_forever(pw):
|
||||
return pw
|
||||
@given(parse('we use the password "{password}" if prompted'))
|
||||
def use_password_forever(password):
|
||||
return password
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}"
|
||||
assert expected_output
|
||||
if which_output_stream is None:
|
||||
assert ((expected_output in cli_run["stdout"]) == we_should) or (
|
||||
(expected_output in cli_run["stderr"]) == we_should
|
||||
)
|
||||
), output_str
|
||||
|
||||
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":
|
||||
assert (expected_output in cli_run["stderr"]) == we_should
|
||||
assert (expected_output in cli_run["stderr"]) == we_should, output_str
|
||||
|
||||
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}"))
|
||||
|
@ -164,12 +167,12 @@ def config_var_in_memory(
|
|||
|
||||
@then("we should be prompted for a password")
|
||||
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")
|
||||
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}"))
|
||||
|
@ -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):
|
||||
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}"'))
|
||||
|
|
|
@ -21,12 +21,12 @@ def when_we_change_directory(directory_name):
|
|||
|
||||
# These variables are used in the `@when(re(...))` section below
|
||||
command = '(?P<command>[^"]*)'
|
||||
input_method = "(?P<input_method>enter|pipe)"
|
||||
user_input = '("(?P<user_input>[^"]*)")'
|
||||
input_method = "(?P<input_method>enter|pipe|type)"
|
||||
all_input = '("(?P<all_input>[^"]*)")'
|
||||
|
||||
|
||||
@when(parse('we run "jrnl {command}" and {input_method}\n{user_input}'))
|
||||
@when(re(f'we run "jrnl ?{command}" and {input_method} {user_input}'))
|
||||
@when(parse('we run "jrnl {command}" and {input_method}\n{all_input}'))
|
||||
@when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}'))
|
||||
@when(parse('we run "jrnl {command}"'))
|
||||
@when('we run "jrnl"')
|
||||
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