diff --git a/jrnl/__init__.py b/jrnl/__init__.py deleted file mode 100644 index 6186dc2e..00000000 --- a/jrnl/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -try: - from jrnl.__version__ import __version__ -except ImportError: - __version__ = "source" -__title__ = "jrnl" diff --git a/jrnl/__main__.py b/jrnl/__main__.py deleted file mode 100644 index 085da2a0..00000000 --- a/jrnl/__main__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import sys - -from jrnl.main import run - -if __name__ == "__main__": - sys.exit(run()) diff --git a/jrnl/__version__.py b/jrnl/__version__.py deleted file mode 100644 index 6689f558..00000000 --- a/jrnl/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "v4.0-beta3" diff --git a/jrnl/args.py b/jrnl/args.py deleted file mode 100644 index 9b7dafe7..00000000 --- a/jrnl/args.py +++ /dev/null @@ -1,446 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import argparse -import re -import textwrap - -from jrnl.commands import postconfig_decrypt -from jrnl.commands import postconfig_encrypt -from jrnl.commands import postconfig_import -from jrnl.commands import postconfig_list -from jrnl.commands import preconfig_diagnostic -from jrnl.commands import preconfig_version -from jrnl.output import deprecated_cmd -from jrnl.plugins import EXPORT_FORMATS -from jrnl.plugins import IMPORT_FORMATS -from jrnl.plugins import util - - -class WrappingFormatter(argparse.RawTextHelpFormatter): - """Used in help screen""" - - def _split_lines(self, text: str, width: int) -> list[str]: - text = text.split("\n\n") - text = map(lambda t: self._whitespace_matcher.sub(" ", t).strip(), text) - text = map(lambda t: textwrap.wrap(t, width=56), text) - text = [item for sublist in text for item in sublist] - return text - - -class IgnoreNoneAppendAction(argparse._AppendAction): - """ - Pass -not without a following string and avoid appending - a None value to the excluded list - """ - - def __call__(self, parser, namespace, values, option_string=None): - if values is not None: - super().__call__(parser, namespace, values, option_string) - - -def parse_not_arg( - args: list[str], parsed_args: argparse.Namespace, parser: argparse.ArgumentParser -) -> argparse.Namespace: - """ - It's possible to use -not as a precursor to -starred and -tagged - to reverse their behaviour, however this requires some extra logic - to parse, and to ensure we still do not allow passing an empty -not - """ - - parsed_args.exclude_starred = False - parsed_args.exclude_tagged = False - - if "-not-starred" in "".join(args): - parsed_args.starred = False - parsed_args.exclude_starred = True - if "-not-tagged" in "".join(args): - parsed_args.tagged = False - parsed_args.exclude_tagged = True - if "-not" in args and not any( - [parsed_args.exclude_starred, parsed_args.exclude_tagged, parsed_args.excluded] - ): - parser.error("argument -not: expected 1 argument") - - return parsed_args - - -def parse_args(args: list[str] = []) -> argparse.Namespace: - """ - Argument parsing that is doable before the config is available. - Everything else goes into "text" for later parsing. - """ - parser = argparse.ArgumentParser( - formatter_class=WrappingFormatter, - add_help=False, - description="Collect your thoughts and notes without leaving the command line", - epilog=textwrap.dedent( - """ - We gratefully thank all contributors! - Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl - And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)""" - ), - ) - - optional = parser.add_argument_group("Optional Arguments") - optional.add_argument( - "--debug", - dest="debug", - action="store_true", - help="Print information useful for troubleshooting", - ) - - standalone = parser.add_argument_group( - "Standalone Commands", - "These commands will exit after they complete. You may only run one at a time.", - ) - standalone.add_argument("--help", action="help", help="Show this help message") - standalone.add_argument("-h", action="help", help=argparse.SUPPRESS) - standalone.add_argument( - "--version", - action="store_const", - const=preconfig_version, - dest="preconfig_cmd", - help="Print version information", - ) - standalone.add_argument( - "-v", - action="store_const", - const=preconfig_version, - dest="preconfig_cmd", - help=argparse.SUPPRESS, - ) - standalone.add_argument( - "--diagnostic", - action="store_const", - const=preconfig_diagnostic, - dest="preconfig_cmd", - help=argparse.SUPPRESS, - ) - standalone.add_argument( - "--list", - action="store_const", - const=postconfig_list, - dest="postconfig_cmd", - help=""" - List all configured journals. - - Optional parameters: - - --format [json or yaml] - """, - ) - standalone.add_argument( - "--ls", - action="store_const", - const=postconfig_list, - dest="postconfig_cmd", - help=argparse.SUPPRESS, - ) - standalone.add_argument( - "-ls", - action="store_const", - const=lambda **kwargs: deprecated_cmd( - "-ls", "--list or --ls", callback=postconfig_list, **kwargs - ), - dest="postconfig_cmd", - help=argparse.SUPPRESS, - ) - standalone.add_argument( - "--encrypt", - help="Encrypt selected journal with a password", - action="store_const", - metavar="TYPE", - const=postconfig_encrypt, - dest="postconfig_cmd", - ) - standalone.add_argument( - "--decrypt", - help="Decrypt selected journal and store it in plain text", - action="store_const", - metavar="TYPE", - const=postconfig_decrypt, - dest="postconfig_cmd", - ) - standalone.add_argument( - "--import", - action="store_const", - metavar="TYPE", - const=postconfig_import, - dest="postconfig_cmd", - help=f""" - Import entries from another journal. - - Optional parameters: - - --file FILENAME (default: uses stdin) - - --format [{util.oxford_list(IMPORT_FORMATS)}] (default: jrnl) - """, - ) - standalone.add_argument( - "--file", - metavar="FILENAME", - dest="filename", - help=argparse.SUPPRESS, - default=None, - ) - standalone.add_argument("-i", dest="filename", help=argparse.SUPPRESS) - - compose_msg = """ - To add a new entry into your journal, simply write it on the command line: - - jrnl yesterday: I was walking and I found this big log. - - The date and the following colon ("yesterday:") are optional. If you leave - them out, "now" will be used: - - jrnl Then I rolled the log over. - - Also, you can mark extra special entries ("star" them) with an asterisk: - - jrnl *And underneath was a tiny little stick. - - Please note that asterisks might be a special character in your shell, so you - might have to escape them. When in doubt about escaping, put quotes around - your entire entry: - - jrnl "saturday at 2am: *Then I was like 'That log had a child!'" """ - - composing = parser.add_argument_group( - "Writing", textwrap.dedent(compose_msg).strip() - ) - composing.add_argument("text", metavar="", nargs="*") - composing.add_argument( - "--template", - dest="template", - help="Path to template file. Can be a local path, absolute path, or a path relative to $XDG_DATA_HOME/jrnl/templates/", - ) - - read_msg = ( - "To find entries from your journal, use any combination of the below filters." - ) - reading = parser.add_argument_group("Searching", textwrap.dedent(read_msg)) - reading.add_argument( - "-on", dest="on_date", metavar="DATE", help="Show entries on this date" - ) - reading.add_argument( - "-today-in-history", - dest="today_in_history", - action="store_true", - help="Show entries of today over the years", - ) - reading.add_argument( - "-month", - dest="month", - metavar="DATE", - help="Show entries on this month of any year", - ) - reading.add_argument( - "-day", - dest="day", - metavar="DATE", - help="Show entries on this day of any month", - ) - reading.add_argument( - "-year", - dest="year", - metavar="DATE", - help="Show entries of a specific year", - ) - reading.add_argument( - "-from", - dest="start_date", - metavar="DATE", - help="Show entries after, or on, this date", - ) - reading.add_argument( - "-to", - dest="end_date", - metavar="DATE", - help="Show entries before, or on, this date (alias: -until)", - ) - reading.add_argument("-until", dest="end_date", help=argparse.SUPPRESS) - reading.add_argument( - "-contains", - dest="contains", - metavar="TEXT", - help="Show entries containing specific text (put quotes around text with spaces)", - ) - reading.add_argument( - "-and", - dest="strict", - action="store_true", - help='Show only entries that match all conditions, like saying "x AND y" (default: OR)', - ) - reading.add_argument( - "-starred", - dest="starred", - action="store_true", - help="Show only starred entries (marked with *)", - ) - reading.add_argument( - "-tagged", - dest="tagged", - action="store_true", - help="Show only entries that have at least one tag", - ) - reading.add_argument( - "-n", - dest="limit", - default=None, - metavar="NUMBER", - help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect)", - nargs="?", - type=int, - ) - reading.add_argument( - "-not", - dest="excluded", - nargs="?", - default=[], - metavar="TAG/FLAG", - action=IgnoreNoneAppendAction, - help=( - "If passed a string, will exclude entries with that tag. " - "Can be also used before -starred or -tagged flags, to exclude " - "starred or tagged entries respectively." - ), - ) - - search_options_msg = """ These help you do various tasks with the selected entries from your search. - If used on their own (with no search), they will act on your entire journal""" - exporting = parser.add_argument_group( - "Searching Options", textwrap.dedent(search_options_msg) - ) - exporting.add_argument( - "--edit", - dest="edit", - help="Opens the selected entries in your configured editor", - action="store_true", - ) - exporting.add_argument( - "--delete", - dest="delete", - action="store_true", - help="Interactively deletes selected entries", - ) - exporting.add_argument( - "--change-time", - dest="change_time", - nargs="?", - metavar="DATE", - const="now", - help="Change timestamp for selected entries (default: now)", - ) - exporting.add_argument( - "--format", - metavar="TYPE", - dest="export", - choices=EXPORT_FORMATS, - help=f""" - Display selected entries in an alternate format. - - TYPE can be: {util.oxford_list(EXPORT_FORMATS)}. - - Optional parameters: - - --file FILENAME Write output to file instead of stdout - """, - default=False, - ) - exporting.add_argument( - "--export", - metavar="TYPE", - dest="export", - choices=EXPORT_FORMATS, - help=argparse.SUPPRESS, - ) - exporting.add_argument( - "--tags", - dest="tags", - action="store_true", - help="Alias for '--format tags'. Returns a list of all tags and number of occurrences", - ) - exporting.add_argument( - "--short", - dest="short", - action="store_true", - help="Show only titles or line containing the search tags", - ) - exporting.add_argument( - "-s", - dest="short", - action="store_true", - help=argparse.SUPPRESS, - ) - exporting.add_argument( - "-o", - dest="filename", - help=argparse.SUPPRESS, - ) - - config_overrides = parser.add_argument_group( - "Config file override", - textwrap.dedent("Apply a one-off override of the config file option"), - ) - config_overrides.add_argument( - "--config-override", - dest="config_override", - action="append", - type=str, - nargs=2, - default=[], - metavar="CONFIG_KV_PAIR", - help=""" - Override configured key-value pair with CONFIG_KV_PAIR for this command invocation only. - - Examples: \n - \t - Use a different editor for this jrnl entry, call: \n - \t jrnl --config-override editor "nano" \n - \t - Override color selections\n - \t jrnl --config-override colors.body blue --config-override colors.title green - """, - ) - config_overrides.add_argument( - "--co", - dest="config_override", - action="append", - type=str, - nargs=2, - default=[], - help=argparse.SUPPRESS, - ) - - alternate_config = parser.add_argument_group( - "Specifies alternate config to be used", - textwrap.dedent("Applies alternate config for current session"), - ) - - alternate_config.add_argument( - "--config-file", - dest="config_file_path", - type=str, - default="", - help=""" - Overrides default (created when first installed) config file for this command only. - - Examples: \n - \t - Use a work config file for this jrnl entry, call: \n - \t jrnl --config-file /home/user1/work_config.yaml - \t - Use a personal config file stored on a thumb drive: \n - \t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml - """, - ) - - alternate_config.add_argument( - "--cf", dest="config_file_path", type=str, default="", help=argparse.SUPPRESS - ) - - # Handle '-123' as a shortcut for '-n 123' - num = re.compile(r"^-(\d+)$") - args = [num.sub(r"-n \1", arg) for arg in args] - parsed_args = parser.parse_intermixed_args(args) - parsed_args = parse_not_arg(args, parsed_args, parser) - - return parsed_args diff --git a/jrnl/color.py b/jrnl/color.py deleted file mode 100644 index 37b7a631..00000000 --- a/jrnl/color.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import re -from string import punctuation -from string import whitespace -from typing import TYPE_CHECKING - -import colorama - -from jrnl.os_compat import on_windows - -if TYPE_CHECKING: - from jrnl.journals import Entry - -if on_windows(): - colorama.init() - - -def colorize(string: str, color: str, bold: bool = False) -> str: - """Returns the string colored with colorama.Fore.color. If the color set by - the user is "NONE" or the color doesn't exist in the colorama.Fore attributes, - it returns the string without any modification.""" - color_escape = getattr(colorama.Fore, color.upper(), None) - if not color_escape: - return string - elif not bold: - return color_escape + string + colorama.Fore.RESET - else: - return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL - - -def highlight_tags_with_background_color( - entry: "Entry", text: str, color: str, is_title: bool = False -) -> str: - """ - Takes a string and colorizes the tags in it based upon the config value for - color.tags, while colorizing the rest of the text based on `color`. - :param entry: Entry object, for access to journal config - :param text: Text to be colorized - :param color: Color for non-tag text, passed to colorize() - :param is_title: Boolean flag indicating if the text is a title or not - :return: Colorized str - """ - - def colorized_text_generator(fragments): - """Efficiently generate colorized tags / text from text fragments. - Taken from @shobrook. Thanks, buddy :) - :param fragments: List of strings representing parts of entry (tag or word). - :rtype: List of tuples - :returns [(colorized_str, original_str)]""" - for part in fragments: - if part and part[0] not in config["tagsymbols"]: - yield colorize(part, color, bold=is_title), part - elif part: - yield colorize(part, config["colors"]["tags"], bold=True), part - - config = entry.journal.config - if config["highlight"]: # highlight tags - text_fragments = re.split(entry.tag_regex(config["tagsymbols"]), text) - - # Colorizing tags inside of other blocks of text - final_text = "" - previous_piece = "" - for colorized_piece, piece in colorized_text_generator(text_fragments): - # If this piece is entirely punctuation or whitespace or the start - # of a line or the previous piece was a tag or this piece is a tag, - # then add it to the final text without a leading space. - if ( - all(char in punctuation + whitespace for char in piece) - or previous_piece.endswith("\n") - or (previous_piece and previous_piece[0] in config["tagsymbols"]) - or piece[0] in config["tagsymbols"] - ): - final_text += colorized_piece - else: - # Otherwise add a leading space and then append the piece. - final_text += " " + colorized_piece - - previous_piece = piece - return final_text.lstrip() - else: - return text diff --git a/jrnl/commands.py b/jrnl/commands.py deleted file mode 100644 index 83618ce2..00000000 --- a/jrnl/commands.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -""" -Functions in this file are standalone commands. All standalone commands are split into -two categories depending on whether they require the config to be loaded to be able to -run. - -1. "preconfig" commands don't require the config at all, and can be run before the - config has been loaded. -2. "postconfig" commands require to config to have already been loaded, parsed, and - scoped before they can be run. - -Also, please note that all (non-builtin) imports should be scoped to each function to -avoid any possible overhead for these standalone commands. -""" - -import argparse -import logging -import platform -import sys - -from jrnl.config import cmd_requires_valid_journal_name -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg - - -def preconfig_diagnostic(_) -> None: - from jrnl import __title__ - from jrnl import __version__ - - print( - f"{__title__}: {__version__}\n" - f"Python: {sys.version}\n" - f"OS: {platform.system()} {platform.release()}" - ) - - -def preconfig_version(_) -> None: - import textwrap - - from jrnl import __title__ - from jrnl import __version__ - - output = f""" - {__title__} {__version__} - - Copyright © 2012-2023 jrnl contributors - - This is free software, and you are welcome to redistribute it under certain - conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html - """ - - output = textwrap.dedent(output).strip() - - print(output) - - -def postconfig_list(args: argparse.Namespace, config: dict, **_) -> int: - from jrnl.output import list_journals - - print(list_journals(config, args.export)) - - return 0 - - -@cmd_requires_valid_journal_name -def postconfig_import(args: argparse.Namespace, config: dict, **_) -> int: - from jrnl.journals import open_journal - from jrnl.plugins import get_importer - - # Requires opening the journal - journal = open_journal(args.journal_name, config) - - format = args.export if args.export else "jrnl" - get_importer(format).import_(journal, args.filename) - - return 0 - - -@cmd_requires_valid_journal_name -def postconfig_encrypt( - args: argparse.Namespace, config: dict, original_config: dict -) -> int: - """ - Encrypt a journal in place, or optionally to a new file - """ - from jrnl.config import update_config - from jrnl.install import save_config - from jrnl.journals import open_journal - - # Open the journal - journal = open_journal(args.journal_name, config) - - if hasattr(journal, "can_be_encrypted") and not journal.can_be_encrypted: - raise JrnlException( - Message( - MsgText.CannotEncryptJournalType, - MsgStyle.ERROR, - { - "journal_name": args.journal_name, - "journal_type": journal.__class__.__name__, - }, - ) - ) - - # If journal is encrypted, create new password - logging.debug("Clearing encryption method...") - - if journal.config["encrypt"] is True: - logging.debug("Journal already encrypted. Re-encrypting...") - print(f"Journal {journal.name} is already encrypted. Create a new password.") - journal.encryption_method.clear() - else: - journal.config["encrypt"] = True - journal.encryption_method = None - - journal.write(args.filename) - - print_msg( - Message( - MsgText.JournalEncryptedTo, - MsgStyle.NORMAL, - {"path": args.filename or journal.config["journal"]}, - ) - ) - - # Update the config, if we encrypted in place - if not args.filename: - update_config( - original_config, {"encrypt": True}, args.journal_name, force_local=True - ) - save_config(original_config) - - return 0 - - -@cmd_requires_valid_journal_name -def postconfig_decrypt( - args: argparse.Namespace, config: dict, original_config: dict -) -> int: - """Decrypts into new file. If filename is not set, we encrypt the journal file itself.""" - from jrnl.config import update_config - from jrnl.install import save_config - from jrnl.journals import open_journal - - journal = open_journal(args.journal_name, config) - - logging.debug("Clearing encryption method...") - journal.config["encrypt"] = False - journal.encryption_method = None - - journal.write(args.filename) - print_msg( - Message( - MsgText.JournalDecryptedTo, - MsgStyle.NORMAL, - {"path": args.filename or journal.config["journal"]}, - ) - ) - - # Update the config, if we decrypted in place - if not args.filename: - update_config( - original_config, {"encrypt": False}, args.journal_name, force_local=True - ) - save_config(original_config) - - return 0 diff --git a/jrnl/config.py b/jrnl/config.py deleted file mode 100644 index a2f0885c..00000000 --- a/jrnl/config.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import argparse -import logging -import os -from typing import Any -from typing import Callable - -import colorama -from rich.pretty import pretty_repr -from ruamel.yaml import YAML -from ruamel.yaml import constructor - -from jrnl import __version__ -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import list_journals -from jrnl.output import print_msg -from jrnl.path import get_config_path -from jrnl.path import get_default_journal_path - -# Constants -DEFAULT_JOURNAL_KEY = "default" - -YAML_SEPARATOR = ": " -YAML_FILE_ENCODING = "utf-8" - - -def make_yaml_valid_dict(input: list) -> dict: - """ - - Convert a two-element list of configuration key-value pair into a flat dict. - - The dict is created through the yaml loader, with the assumption that - "input[0]: input[1]" is valid yaml. - - :param input: list of configuration keys in dot-notation and their respective values. - :type input: list - :return: A single level dict of the configuration keys in dot-notation and their respective desired values - :rtype: dict - """ - - assert len(input) == 2 - - # yaml compatible strings are of the form Key:Value - yamlstr = YAML_SEPARATOR.join(input) - - runtime_modifications = YAML(typ="safe").load(yamlstr) - - return runtime_modifications - - -def save_config(config: dict, alt_config_path: str | None = None) -> None: - """Supply alt_config_path if using an alternate config through --config-file.""" - config["version"] = __version__ - - yaml = YAML(typ="safe") - yaml.default_flow_style = False # prevents collapsing of tree structure - - with open( - alt_config_path if alt_config_path else get_config_path(), - "w", - encoding=YAML_FILE_ENCODING, - ) as f: - yaml.dump(config, f) - - -def get_default_config() -> dict[str, Any]: - return { - "version": __version__, - "journals": {"default": {"journal": get_default_journal_path()}}, - "editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "", - "encrypt": False, - "template": False, - "default_hour": 9, - "default_minute": 0, - "timeformat": "%F %r", - "tagsymbols": "#@", - "highlight": True, - "linewrap": 79, - "indent_character": "|", - "colors": { - "body": "none", - "date": "none", - "tags": "none", - "title": "none", - }, - } - - -def get_default_colors() -> dict[str, Any]: - return { - "body": "none", - "date": "black", - "tags": "yellow", - "title": "cyan", - } - - -def scope_config(config: dict, journal_name: str) -> dict: - if journal_name not in config["journals"]: - return config - config = config.copy() - journal_conf = config["journals"].get(journal_name) - if type(journal_conf) is dict: - # We can override the default config on a by-journal basis - logging.debug( - "Updating configuration with specific journal overrides:\n%s", - pretty_repr(journal_conf), - ) - config.update(journal_conf) - else: - # But also just give them a string to point to the journal file - config["journal"] = journal_conf - - logging.debug("Scoped config:\n%s", pretty_repr(config)) - return config - - -def verify_config_colors(config: dict) -> bool: - """ - Ensures the keys set for colors are valid colorama.Fore attributes, or "None" - :return: True if all keys are set correctly, False otherwise - """ - all_valid_colors = True - for key, color in config["colors"].items(): - upper_color = color.upper() - if upper_color == "NONE": - continue - if not getattr(colorama.Fore, upper_color, None): - print_msg( - Message( - MsgText.InvalidColor, - MsgStyle.NORMAL, - { - "key": key, - "color": color, - }, - ) - ) - all_valid_colors = False - return all_valid_colors - - -def load_config(config_path: str) -> dict: - """Tries to load a config file from YAML.""" - try: - with open(config_path, encoding=YAML_FILE_ENCODING) as f: - yaml = YAML(typ="safe") - yaml.allow_duplicate_keys = False - return yaml.load(f) - except constructor.DuplicateKeyError as e: - print_msg( - Message( - MsgText.ConfigDoubleKeys, - MsgStyle.WARNING, - { - "error_message": e, - }, - ) - ) - with open(config_path, encoding=YAML_FILE_ENCODING) as f: - yaml = YAML(typ="safe") - yaml.allow_duplicate_keys = True - return yaml.load(f) - - -def is_config_json(config_path: str) -> bool: - with open(config_path, "r", encoding="utf-8") as f: - config_file = f.read() - return config_file.strip().startswith("{") - - -def update_config( - config: dict, new_config: dict, scope: str | None, force_local: bool = False -) -> None: - """Updates a config dict with new values - either global if scope is None - or config['journals'][scope] is just a string pointing to a journal file, - or within the scope""" - if scope and type(config["journals"][scope]) is dict: # Update to journal specific - config["journals"][scope].update(new_config) - elif scope and force_local: # Convert to dict - config["journals"][scope] = {"journal": config["journals"][scope]} - config["journals"][scope].update(new_config) - else: - config.update(new_config) - - -def get_journal_name(args: argparse.Namespace, config: dict) -> argparse.Namespace: - args.journal_name = DEFAULT_JOURNAL_KEY - - # The first arg might be a journal name - if args.text: - potential_journal_name = args.text[0] - if potential_journal_name[-1] == ":": - potential_journal_name = potential_journal_name[0:-1] - - if potential_journal_name in config["journals"]: - args.journal_name = potential_journal_name - args.text = args.text[1:] - - logging.debug("Using journal name: %s", args.journal_name) - return args - - -def cmd_requires_valid_journal_name(func: Callable) -> Callable: - def wrapper(args: argparse.Namespace, config: dict, original_config: dict): - validate_journal_name(args.journal_name, config) - func(args=args, config=config, original_config=original_config) - - return wrapper - - -def validate_journal_name(journal_name: str, config: dict) -> None: - if journal_name not in config["journals"]: - raise JrnlException( - Message( - MsgText.NoNamedJournal, - MsgStyle.ERROR, - { - "journal_name": journal_name, - "journals": list_journals(config), - }, - ), - ) diff --git a/jrnl/controller.py b/jrnl/controller.py deleted file mode 100644 index e0e222b5..00000000 --- a/jrnl/controller.py +++ /dev/null @@ -1,453 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging -import sys -from typing import TYPE_CHECKING - -from jrnl import install -from jrnl import plugins -from jrnl import time -from jrnl.config import DEFAULT_JOURNAL_KEY -from jrnl.config import get_config_path -from jrnl.config import get_journal_name -from jrnl.config import scope_config -from jrnl.editor import get_text_from_editor -from jrnl.editor import get_text_from_stdin -from jrnl.editor import read_template_file -from jrnl.exception import JrnlException -from jrnl.journals import open_journal -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.output import print_msgs -from jrnl.override import apply_overrides - -if TYPE_CHECKING: - from argparse import Namespace - - from jrnl.journals import Entry - from jrnl.journals import Journal - - -def run(args: "Namespace"): - """ - Flow: - 1. Run standalone command if it doesn't require config (help, version, etc), then exit - 2. Load config - 3. Run standalone command if it does require config (encrypt, decrypt, etc), then exit - 4. Load specified journal - 5. Start append mode, or search mode - 6. Perform actions with results from search mode (if needed) - 7. Profit - """ - - # Run command if possible before config is available - if callable(args.preconfig_cmd): - return args.preconfig_cmd(args) - - # Load the config, and extract journal name - config = install.load_or_install_jrnl(args.config_file_path) - original_config = config.copy() - - # Apply config overrides - config = apply_overrides(args, config) - - args = get_journal_name(args, config) - config = scope_config(config, args.journal_name) - - # Run post-config command now that config is ready - if callable(args.postconfig_cmd): - return args.postconfig_cmd( - args=args, config=config, original_config=original_config - ) - - # --- All the standalone commands are now done --- # - - # Get the journal we're going to be working with - journal = open_journal(args.journal_name, config) - - kwargs = { - "args": args, - "config": config, - "journal": journal, - "old_entries": journal.entries, - } - - if _is_append_mode(**kwargs): - append_mode(**kwargs) - return - - # If not append mode, then we're in search mode (only 2 modes exist) - search_mode(**kwargs) - entries_found_count = len(journal) - _print_entries_found_count(entries_found_count, args) - - # Actions - _perform_actions_on_search_results(**kwargs) - - if entries_found_count != 0 and _has_action_args(args): - _print_changed_counts(journal) - else: - # display only occurs if no other action occurs - _display_search_results(**kwargs) - - -def _perform_actions_on_search_results(**kwargs): - args = kwargs["args"] - - # Perform actions (if needed) - if args.change_time: - _change_time_search_results(**kwargs) - - if args.delete: - _delete_search_results(**kwargs) - - # open results in editor (if `--edit` was used) - if args.edit: - _edit_search_results(**kwargs) - - -def _is_append_mode(args: "Namespace", config: dict, **kwargs) -> bool: - """Determines if we are in append mode (as opposed to search mode)""" - # Are any search filters present? If so, then search mode. - append_mode = ( - not _has_search_args(args) - and not _has_action_args(args) - and not _has_display_args(args) - ) - - # Might be writing and want to move to editor part of the way through - if args.edit and args.text: - append_mode = True - - # If the text is entirely tags, then we are also searching (not writing) - if append_mode and args.text and _has_only_tags(config["tagsymbols"], args.text): - append_mode = False - - return append_mode - - -def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -> None: - """ - Gets input from the user to write to the journal - 0. Check for a template passed as an argument, or in the global config - 1. Check for input from cli - 2. Check input being piped in - 3. Open editor if configured (prepopulated with template if available) - 4. Use stdin.read as last resort - 6. Write any found text to journal, or exit - """ - logging.debug("Append mode: starting") - - template_text = _get_template(args, config) - - if args.text: - logging.debug(f"Append mode: cli text detected: {args.text}") - raw = " ".join(args.text).strip() - if args.edit: - raw = _write_in_editor(config, raw) - elif not sys.stdin.isatty(): - logging.debug("Append mode: receiving piped text") - raw = sys.stdin.read() - else: - raw = _write_in_editor(config, template_text) - - if template_text is not None and raw == template_text: - logging.error("Append mode: raw text was the same as the template") - raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL)) - - if not raw or raw.isspace(): - logging.error("Append mode: couldn't get raw text or entry was empty") - raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL)) - - logging.debug( - f"Append mode: appending raw text to journal '{args.journal_name}': {raw}" - ) - journal.new_entry(raw) - if args.journal_name != DEFAULT_JOURNAL_KEY: - print_msg( - Message( - MsgText.JournalEntryAdded, - MsgStyle.NORMAL, - {"journal_name": args.journal_name}, - ) - ) - journal.write() - logging.debug("Append mode: completed journal.write()") - - -def _get_template(args, config) -> str: - # Read template file and pass as raw text into the composer - logging.debug( - f"Get template:\n--template: {args.template}\nfrom config: {config.get('template')}" - ) - template_path = args.template or config.get("template") - - template_text = None - - if template_path: - template_text = read_template_file(template_path) - - return template_text - - -def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None: - """ - Search for entries in a journal, and return the - results. If no search args, then return all results - """ - logging.debug("Search mode: starting") - - # If no search args, then return all results (don't filter anything) - if not _has_search_args(args) and not _has_display_args(args) and not args.text: - logging.debug("Search mode: has no search args") - return - - logging.debug("Search mode: has search args") - _filter_journal_entries(args, journal) - - -def _write_in_editor(config: dict, prepopulated_text: str | None = None) -> str: - if config["editor"]: - logging.debug("Append mode: opening editor") - raw = get_text_from_editor(config, prepopulated_text) - else: - raw = get_text_from_stdin() - - return raw - - -def _filter_journal_entries(args: "Namespace", journal: "Journal", **kwargs) -> None: - """Filter journal entries in-place based upon search args""" - if args.on_date: - args.start_date = args.end_date = args.on_date - - if args.today_in_history: - now = time.parse("now") - args.day = now.day - args.month = now.month - - journal.filter( - tags=args.text, - month=args.month, - day=args.day, - year=args.year, - start_date=args.start_date, - end_date=args.end_date, - strict=args.strict, - starred=args.starred, - tagged=args.tagged, - exclude=args.excluded, - exclude_starred=args.exclude_starred, - exclude_tagged=args.exclude_tagged, - contains=args.contains, - ) - journal.limit(args.limit) - - -def _print_entries_found_count(count: int, args: "Namespace") -> None: - logging.debug(f"count: {count}") - if count == 0: - if args.edit or args.change_time: - print_msg(Message(MsgText.NothingToModify, MsgStyle.WARNING)) - elif args.delete: - print_msg(Message(MsgText.NothingToDelete, MsgStyle.WARNING)) - else: - print_msg(Message(MsgText.NoEntriesFound, MsgStyle.NORMAL)) - return - elif args.limit and args.limit == count: - # Don't show count if the user expects a limited number of results - logging.debug("args.limit is true-ish") - return - - logging.debug("Printing general summary") - my_msg = ( - MsgText.EntryFoundCountSingular if count == 1 else MsgText.EntryFoundCountPlural - ) - print_msg(Message(my_msg, MsgStyle.NORMAL, {"num": count})) - - -def _other_entries(journal: "Journal", entries: list["Entry"]) -> list["Entry"]: - """Find entries that are not in journal""" - return [e for e in entries if e not in journal.entries] - - -def _edit_search_results( - config: dict, journal: "Journal", old_entries: list["Entry"], **kwargs -) -> None: - """ - 1. Send the given journal entries to the user-configured editor - 2. Print out stats on any modifications to journal - 3. Write modifications to journal - """ - if not config["editor"]: - raise JrnlException( - Message( - MsgText.EditorNotConfigured, - MsgStyle.ERROR, - {"config_file": get_config_path()}, - ) - ) - - # separate entries we are not editing - other_entries = _other_entries(journal, old_entries) - - # Send user to the editor - try: - edited = get_text_from_editor(config, journal.editable_str()) - except JrnlException as e: - if e.has_message_text(MsgText.NoTextReceived): - raise JrnlException( - Message(MsgText.NoEditsReceivedJournalNotDeleted, MsgStyle.WARNING) - ) - else: - raise e - - journal.parse_editable_str(edited) - - # Put back entries we separated earlier, sort, and write the journal - journal.entries += other_entries - journal.sort() - journal.write() - - -def _print_changed_counts(journal: "Journal", **kwargs) -> None: - stats = journal.get_change_counts() - msgs = [] - - if stats["added"] > 0: - 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: - my_msg = ( - MsgText.JournalCountDeletedSingular - if stats["deleted"] == 1 - else MsgText.JournalCountDeletedPlural - ) - msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["deleted"]})) - - 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 not msgs: - msgs.append(Message(MsgText.NoEditsReceived, MsgStyle.NORMAL)) - - print_msgs(msgs) - - -def _get_predit_stats(journal: "Journal") -> dict[str, int]: - return {"count": len(journal)} - - -def _delete_search_results( - journal: "Journal", old_entries: list["Entry"], **kwargs -) -> None: - entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion) - - journal.entries = old_entries - - if entries_to_delete: - journal.delete_entries(entries_to_delete) - - journal.write() - - -def _change_time_search_results( - args: "Namespace", - journal: "Journal", - old_entries: list["Entry"], - no_prompt: bool = False, - **kwargs, -) -> None: - # separate entries we are not editing - # @todo if there's only 1, don't prompt - entries_to_change = journal.prompt_action_entries(MsgText.ChangeTimeEntryQuestion) - - if entries_to_change: - date = time.parse(args.change_time) - journal.entries = old_entries - journal.change_date_entries(date, entries_to_change) - - journal.write() - - -def _display_search_results(args: "Namespace", journal: "Journal", **kwargs) -> None: - if len(journal) == 0: - return - - # Get export format from config file if not provided at the command line - args.export = args.export or kwargs["config"].get("display_format") - - if args.tags: - print(plugins.get_exporter("tags").export(journal)) - - elif args.short or args.export == "short": - print(journal.pprint(short=True)) - - elif args.export == "pretty": - print(journal.pprint()) - - elif args.export: - exporter = plugins.get_exporter(args.export) - print(exporter.export(journal, args.filename)) - else: - print(journal.pprint()) - - -def _has_search_args(args: "Namespace") -> bool: - """Looking for arguments that filter a journal""" - return any( - ( - args.contains, - args.tagged, - args.excluded, - args.exclude_starred, - args.exclude_tagged, - args.end_date, - args.today_in_history, - args.month, - args.day, - args.year, - args.limit, - args.on_date, - args.starred, - args.start_date, - args.strict, # -and - ) - ) - - -def _has_action_args(args: "Namespace") -> bool: - return any( - ( - args.change_time, - args.delete, - args.edit, - ) - ) - - -def _has_display_args(args: "Namespace") -> bool: - return any( - ( - args.tags, - args.short, - args.export, # --format - ) - ) - - -def _has_only_tags(tag_symbols: str, args_text: str) -> bool: - return all(word[0] in tag_symbols for word in " ".join(args_text).split()) diff --git a/jrnl/editor.py b/jrnl/editor.py deleted file mode 100644 index cbcf6207..00000000 --- a/jrnl/editor.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging -import os -import subprocess -import sys -import tempfile -from pathlib import Path - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.os_compat import on_windows -from jrnl.os_compat import split_args -from jrnl.output import print_msg -from jrnl.path import absolute_path -from jrnl.path import get_templates_path - - -def get_text_from_editor(config: dict, template: str = "") -> str: - suffix = ".jrnl" - if config["template"]: - template_filename = Path(config["template"]).name - suffix = "-" + template_filename - filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=suffix) - os.close(filehandle) - - with open(tmpfile, "w", encoding="utf-8") as f: - if template: - f.write(template) - - try: - subprocess.call(split_args(config["editor"]) + [tmpfile]) - except FileNotFoundError: - raise JrnlException( - Message( - MsgText.EditorMisconfigured, - MsgStyle.ERROR, - {"editor_key": config["editor"]}, - ) - ) - - with open(tmpfile, "r", encoding="utf-8") as f: - raw = f.read() - os.remove(tmpfile) - - if not raw: - raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL)) - - return raw - - -def get_text_from_stdin() -> str: - print_msg( - Message( - MsgText.WritingEntryStart, - MsgStyle.TITLE, - { - "how_to_quit": MsgText.HowToQuitWindows - if on_windows() - else MsgText.HowToQuitLinux - }, - ) - ) - - try: - raw = sys.stdin.read() - except KeyboardInterrupt: - logging.error("Append mode: keyboard interrupt") - raise JrnlException( - Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE), - Message(MsgText.JournalNotSaved, MsgStyle.WARNING), - ) - - return raw - - -def get_template_path(template_path: str, jrnl_template_dir: str) -> str: - actual_template_path = os.path.join(jrnl_template_dir, template_path) - if not os.path.exists(actual_template_path): - logging.debug( - f"Couldn't open {actual_template_path}. Treating template path like a local / abs path." - ) - actual_template_path = absolute_path(template_path) - - return actual_template_path - - -def read_template_file(template_path: str) -> str: - """ - Reads the template file given a template path in this order: - - * Check $XDG_DATA_HOME/jrnl/templates/template_path. - * Check template_arg as an absolute / relative path. - - If a file is found, its contents are returned as a string. - If not, a JrnlException is raised. - """ - - jrnl_template_dir = get_templates_path() - actual_template_path = get_template_path(template_path, jrnl_template_dir) - - try: - with open(actual_template_path, encoding="utf-8") as f: - template_data = f.read() - return template_data - except FileNotFoundError: - raise JrnlException( - Message( - MsgText.CantReadTemplate, - MsgStyle.ERROR, - { - "template_path": template_path, - "actual_template_path": actual_template_path, - "jrnl_template_dir": str(jrnl_template_dir) + os.sep, - }, - ) - ) diff --git a/jrnl/encryption/BaseEncryption.py b/jrnl/encryption/BaseEncryption.py deleted file mode 100644 index efecd87d..00000000 --- a/jrnl/encryption/BaseEncryption.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging -from abc import ABC -from abc import abstractmethod - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText - - -class BaseEncryption(ABC): - def __init__(self, journal_name: str, config: dict): - logging.debug("start") - self._encoding: str = "utf-8" - self._journal_name: str = journal_name - self._config: dict = config - - def clear(self) -> None: - pass - - def encrypt(self, text: str) -> bytes: - logging.debug("encrypting") - return self._encrypt(text) - - def decrypt(self, text: bytes) -> str: - logging.debug("decrypting") - if (result := self._decrypt(text)) is None: - raise JrnlException( - Message(MsgText.DecryptionFailedGeneric, MsgStyle.ERROR) - ) - - return result - - @abstractmethod - def _encrypt(self, text: str) -> bytes: - """ - This is needed because self.decrypt might need - to perform actions (e.g. prompt for password) - before actually encrypting. - """ - pass - - @abstractmethod - def _decrypt(self, text: bytes) -> str | None: - """ - This is needed because self.decrypt might need - to perform actions (e.g. prompt for password) - before actually decrypting. - """ - pass diff --git a/jrnl/encryption/BaseKeyEncryption.py b/jrnl/encryption/BaseKeyEncryption.py deleted file mode 100644 index f8c20bc9..00000000 --- a/jrnl/encryption/BaseKeyEncryption.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from .BaseEncryption import BaseEncryption - - -class BaseKeyEncryption(BaseEncryption): - pass diff --git a/jrnl/encryption/BasePasswordEncryption.py b/jrnl/encryption/BasePasswordEncryption.py deleted file mode 100644 index 29a28b76..00000000 --- a/jrnl/encryption/BasePasswordEncryption.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging - -from jrnl.encryption.BaseEncryption import BaseEncryption -from jrnl.exception import JrnlException -from jrnl.keyring import get_keyring_password -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.prompt import create_password -from jrnl.prompt import prompt_password - - -class BasePasswordEncryption(BaseEncryption): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - logging.debug("start") - self._attempts: int = 0 - self._max_attempts: int = 3 - self._password: str = "" - self._check_keyring: bool = True - - @property - def check_keyring(self) -> bool: - return self._check_keyring - - @check_keyring.setter - def check_keyring(self, value: bool) -> None: - self._check_keyring = value - - @property - def password(self) -> str | None: - return self._password - - @password.setter - def password(self, value: str) -> None: - self._password = value - - def clear(self): - self.password = None - self.check_keyring = False - - def encrypt(self, text: str) -> bytes: - logging.debug("encrypting") - if not self.password: - if self.check_keyring and ( - keyring_pw := get_keyring_password(self._journal_name) - ): - self.password = keyring_pw - - if not self.password: - self.password = create_password(self._journal_name) - - return self._encrypt(text) - - def decrypt(self, text: bytes) -> str: - logging.debug("decrypting") - if not self.password: - if self.check_keyring and ( - keyring_pw := get_keyring_password(self._journal_name) - ): - self.password = keyring_pw - - if not self.password: - self._prompt_password() - - while (result := self._decrypt(text)) is None: - self._prompt_password() - - return result - - def _prompt_password(self) -> None: - if self._attempts >= self._max_attempts: - raise JrnlException( - Message(MsgText.PasswordMaxTriesExceeded, MsgStyle.ERROR) - ) - - first_try = self._attempts == 0 - self.password = prompt_password(first_try=first_try) - self._attempts += 1 diff --git a/jrnl/encryption/Jrnlv1Encryption.py b/jrnl/encryption/Jrnlv1Encryption.py deleted file mode 100644 index 18a9782b..00000000 --- a/jrnl/encryption/Jrnlv1Encryption.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import hashlib -import logging - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers import algorithms -from cryptography.hazmat.primitives.ciphers import modes - -from jrnl.encryption.BasePasswordEncryption import BasePasswordEncryption - - -class Jrnlv1Encryption(BasePasswordEncryption): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - logging.debug("start") - - def _encrypt(self, _: str) -> bytes: - raise NotImplementedError - - def _decrypt(self, text: bytes) -> str | None: - logging.debug("decrypting") - iv, cipher = text[:16], text[16:] - password = self._password or "" - decryption_key = hashlib.sha256(password.encode(self._encoding)).digest() - decryptor = Cipher( - algorithms.AES(decryption_key), modes.CBC(iv), default_backend() - ).decryptor() - try: - plain_padded = decryptor.update(cipher) + decryptor.finalize() - if plain_padded[-1] in (" ", 32): - # Ancient versions of jrnl. Do not judge me. - return plain_padded.decode(self._encoding).rstrip(" ") - else: - unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() - plain = unpadder.update(plain_padded) + unpadder.finalize() - return plain.decode(self._encoding) - except ValueError: - return None diff --git a/jrnl/encryption/Jrnlv2Encryption.py b/jrnl/encryption/Jrnlv2Encryption.py deleted file mode 100644 index 97a2ec37..00000000 --- a/jrnl/encryption/Jrnlv2Encryption.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import base64 -import logging - -from cryptography.fernet import Fernet -from cryptography.fernet import InvalidToken -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - -from .BasePasswordEncryption import BasePasswordEncryption - - -class Jrnlv2Encryption(BasePasswordEncryption): - def __init__(self, *args, **kwargs) -> None: - # Salt is hard-coded - self._salt: bytes = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8" - self._key: bytes = b"" - - super().__init__(*args, **kwargs) - logging.debug("start") - - @property - def password(self): - return self._password - - @password.setter - def password(self, value: str | None): - self._password = value - self._make_key() - - def _make_key(self) -> None: - if self._password is None: - # Password was removed after being set - self._key = None - return - password = self.password.encode(self._encoding) - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=self._salt, - iterations=100_000, - backend=default_backend(), - ) - key = kdf.derive(password) - self._key = base64.urlsafe_b64encode(key) - - def _encrypt(self, text: str) -> bytes: - logging.debug("encrypting") - return Fernet(self._key).encrypt(text.encode(self._encoding)) - - def _decrypt(self, text: bytes) -> str | None: - logging.debug("decrypting") - try: - return Fernet(self._key).decrypt(text).decode(self._encoding) - except (InvalidToken, IndexError): - return None diff --git a/jrnl/encryption/NoEncryption.py b/jrnl/encryption/NoEncryption.py deleted file mode 100644 index 66fa4f80..00000000 --- a/jrnl/encryption/NoEncryption.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging - -from jrnl.encryption.BaseEncryption import BaseEncryption - - -class NoEncryption(BaseEncryption): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - logging.debug("start") - - def _encrypt(self, text: str) -> bytes: - logging.debug("encrypting") - return text.encode(self._encoding) - - def _decrypt(self, text: bytes) -> str: - logging.debug("decrypting") - return text.decode(self._encoding) diff --git a/jrnl/encryption/__init__.py b/jrnl/encryption/__init__.py deleted file mode 100644 index afa92edb..00000000 --- a/jrnl/encryption/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from enum import Enum -from importlib import import_module -from typing import TYPE_CHECKING -from typing import Type - -if TYPE_CHECKING: - from .BaseEncryption import BaseEncryption - - -class EncryptionMethods(str, Enum): - def __str__(self) -> str: - return self.value - - NONE = "NoEncryption" - JRNLV1 = "Jrnlv1Encryption" - JRNLV2 = "Jrnlv2Encryption" - - -def determine_encryption_method(config: str | bool) -> Type["BaseEncryption"]: - ENCRYPTION_METHODS = { - True: EncryptionMethods.JRNLV2, # the default - False: EncryptionMethods.NONE, - "jrnlv1": EncryptionMethods.JRNLV1, - "jrnlv2": EncryptionMethods.JRNLV2, - } - - key = config - if isinstance(config, str): - key = config.lower() - - my_class = ENCRYPTION_METHODS[key] - - return getattr(import_module(f"jrnl.encryption.{my_class}"), my_class) diff --git a/jrnl/exception.py b/jrnl/exception.py deleted file mode 100644 index 87489821..00000000 --- a/jrnl/exception.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING - -from jrnl.output import print_msg - -if TYPE_CHECKING: - from jrnl.messages import Message - from jrnl.messages import MsgText - - -class JrnlException(Exception): - """Common exceptions raised by jrnl.""" - - def __init__(self, *messages: "Message"): - self.messages = messages - - def print(self) -> None: - for msg in self.messages: - print_msg(msg) - - def has_message_text(self, message_text: "MsgText"): - return any([m.text == message_text for m in self.messages]) diff --git a/jrnl/install.py b/jrnl/install.py deleted file mode 100644 index f9d9ac1a..00000000 --- a/jrnl/install.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import contextlib -import glob -import logging -import os -import sys - -from rich.pretty import pretty_repr - -from jrnl import __version__ -from jrnl.config import DEFAULT_JOURNAL_KEY -from jrnl.config import get_config_path -from jrnl.config import get_default_colors -from jrnl.config import get_default_config -from jrnl.config import get_default_journal_path -from jrnl.config import load_config -from jrnl.config import save_config -from jrnl.config import verify_config_colors -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.path import absolute_path -from jrnl.path import expand_path -from jrnl.path import home_dir -from jrnl.prompt import yesno -from jrnl.upgrade import is_old_version - - -def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> None: - """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. - This essentially automatically ports jrnl installations if new config parameters are introduced in later - versions. - Also checks for existence of and difference in version number between config dict and current jrnl version, - and if so, update the config file accordingly. - Supply alt_config_path if using an alternate config through --config-file.""" - default_config = get_default_config() - missing_keys = set(default_config).difference(config_data) - if missing_keys: - for key in missing_keys: - config_data[key] = default_config[key] - - different_version = config_data["version"] != __version__ - if different_version: - config_data["version"] = __version__ - - if missing_keys or different_version: - save_config(config_data, alt_config_path) - config_path = alt_config_path if alt_config_path else get_config_path() - print_msg( - Message( - MsgText.ConfigUpdated, MsgStyle.NORMAL, {"config_path": config_path} - ) - ) - - -def find_default_config() -> str: - config_path = ( - get_config_path() - if os.path.exists(get_config_path()) - else os.path.join(home_dir(), ".jrnl_config") - ) - return config_path - - -def find_alt_config(alt_config: str) -> str: - if not os.path.exists(alt_config): - raise JrnlException( - Message( - MsgText.AltConfigNotFound, MsgStyle.ERROR, {"config_file": alt_config} - ) - ) - - return alt_config - - -def load_or_install_jrnl(alt_config_path: str) -> dict: - """ - If jrnl is already installed, loads and returns a default config object. - If alternate config is specified via --config-file flag, it will be used. - Else, perform various prompts to install jrnl. - """ - config_path = ( - find_alt_config(alt_config_path) if alt_config_path else find_default_config() - ) - - if os.path.exists(config_path): - logging.debug("Reading configuration from file %s", config_path) - config = load_config(config_path) - - if config is None: - raise JrnlException( - Message( - MsgText.CantParseConfigFile, - MsgStyle.ERROR, - { - "config_path": config_path, - }, - ) - ) - - if is_old_version(config_path): - from jrnl import upgrade - - upgrade.upgrade_jrnl(config_path) - - upgrade_config(config, alt_config_path) - verify_config_colors(config) - - else: - logging.debug("Configuration file not found, installing jrnl...") - config = install() - - logging.debug('Using configuration:\n"%s"', pretty_repr(config)) - return config - - -def install() -> dict: - _initialize_autocomplete() - - # Where to create the journal? - default_journal_path = get_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"] = journal_path - - # If the folder doesn't exist, create it - path = os.path.split(journal_path)[0] - with contextlib.suppress(OSError): - os.makedirs(path) - - # Encrypt it? - encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False) - if encrypt: - default_config["encrypt"] = True - print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL)) - - # Use colors? - use_colors = yesno(Message(MsgText.UseColorsQuestion), default=True) - if use_colors: - default_config["colors"] = get_default_colors() - - save_config(default_config) - - print_msg( - Message( - MsgText.InstallComplete, - MsgStyle.NORMAL, - params={"config_path": get_config_path()}, - ) - ) - - return default_config - - -def _initialize_autocomplete() -> None: - # readline is not included in Windows Active Python and perhaps some other distributions - if sys.modules.get("readline"): - import readline - - readline.set_completer_delims(" \t\n;") - readline.parse_and_bind("tab: complete") - readline.set_completer(_autocomplete_path) - - -def _autocomplete_path(text: str, state: int) -> list[str | None]: - expansions = glob.glob(expand_path(text) + "*") - expansions = [e + "/" if os.path.isdir(e) else e for e in expansions] - expansions.append(None) - return expansions[state] diff --git a/jrnl/journals/DayOneJournal.py b/jrnl/journals/DayOneJournal.py deleted file mode 100644 index ace6a5f4..00000000 --- a/jrnl/journals/DayOneJournal.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import contextlib -import datetime -import fnmatch -import os -import platform -import plistlib -import re -import socket -import time -import uuid -import zoneinfo -from pathlib import Path -from xml.parsers.expat import ExpatError - -import tzlocal - -from jrnl import __title__ -from jrnl import __version__ - -from .Entry import Entry -from .Journal import Journal - - -class DayOne(Journal): - """A special Journal handling DayOne files""" - - # InvalidFileException was added to plistlib in Python3.4 - PLIST_EXCEPTIONS = ( - (ExpatError, plistlib.InvalidFileException) - if hasattr(plistlib, "InvalidFileException") - else ExpatError - ) - - def __init__(self, **kwargs): - self.entries = [] - self._deleted_entries = [] - self.can_be_encrypted = False - super().__init__(**kwargs) - - def open(self) -> "DayOne": - filenames = [] - for root, dirnames, f in os.walk(self.config["journal"]): - for filename in fnmatch.filter(f, "*.doentry"): - filenames.append(os.path.join(root, filename)) - self.entries = [] - for filename in filenames: - with open(filename, "rb") as plist_entry: - try: - dict_entry = plistlib.load(plist_entry, fmt=plistlib.FMT_XML) - except self.PLIST_EXCEPTIONS: - pass - else: - try: - timezone = zoneinfo.ZoneInfo(dict_entry["Time Zone"]) - except KeyError: - timezone_name = str(tzlocal.get_localzone()) - timezone = zoneinfo.ZoneInfo(timezone_name) - date = dict_entry["Creation Date"] - # convert the date to UTC rather than keep messing with - # timezones - if timezone.key != "UTC": - date = date.replace(fold=1) + timezone.utcoffset(date) - - entry = Entry( - self, - date, - text=dict_entry["Entry Text"], - starred=dict_entry["Starred"], - ) - entry.uuid = dict_entry["UUID"] - entry._tags = [ - self.config["tagsymbols"][0] + tag.lower() - for tag in dict_entry.get("Tags", []) - ] - if entry._tags: - entry._tags.sort() - - """Extended DayOne attributes""" - # just ignore it if the keys don't exist - with contextlib.suppress(KeyError): - entry.creator_device_agent = dict_entry["Creator"][ - "Device Agent" - ] - entry.creator_host_name = dict_entry["Creator"]["Host Name"] - entry.creator_os_agent = dict_entry["Creator"]["OS Agent"] - entry.creator_software_agent = dict_entry["Creator"][ - "Software Agent" - ] - entry.location = dict_entry["Location"] - entry.weather = dict_entry["Weather"] - - entry.creator_generation_date = dict_entry.get("Creator", {}).get( - "Generation Date", date - ) - - self.entries.append(entry) - self.sort() - return self - - def write(self) -> None: - """Writes only the entries that have been modified into plist files.""" - for entry in self.entries: - if entry.modified: - utc_time = datetime.datetime.utcfromtimestamp( - time.mktime(entry.date.timetuple()) - ) - - if not hasattr(entry, "uuid"): - entry.uuid = uuid.uuid1().hex - if not hasattr(entry, "creator_device_agent"): - entry.creator_device_agent = "" # iPhone/iPhone5,3 - if not hasattr(entry, "creator_generation_date"): - entry.creator_generation_date = utc_time - if not hasattr(entry, "creator_host_name"): - entry.creator_host_name = socket.gethostname() - if not hasattr(entry, "creator_os_agent"): - entry.creator_os_agent = "{}/{}".format( - platform.system(), platform.release() - ) - if not hasattr(entry, "creator_software_agent"): - entry.creator_software_agent = "{}/{}".format( - __title__, __version__ - ) - - fn = ( - Path(self.config["journal"]) - / "entries" - / (entry.uuid.upper() + ".doentry") - ) - - entry_plist = { - "Creation Date": utc_time, - "Starred": entry.starred if hasattr(entry, "starred") else False, - "Entry Text": entry.title + "\n" + entry.body, - "Time Zone": str(tzlocal.get_localzone()), - "UUID": entry.uuid.upper(), - "Tags": [ - tag.strip(self.config["tagsymbols"]).replace("_", " ") - for tag in entry.tags - ], - "Creator": { - "Device Agent": entry.creator_device_agent, - "Generation Date": entry.creator_generation_date, - "Host Name": entry.creator_host_name, - "OS Agent": entry.creator_os_agent, - "Software Agent": entry.creator_software_agent, - }, - } - if hasattr(entry, "location"): - entry_plist["Location"] = entry.location - if hasattr(entry, "weather"): - entry_plist["Weather"] = entry.weather - - # plistlib expects a binary object - with fn.open(mode="wb") as f: - plistlib.dump(entry_plist, f, fmt=plistlib.FMT_XML, sort_keys=False) - - for entry in self._deleted_entries: - filename = os.path.join( - self.config["journal"], "entries", entry.uuid + ".doentry" - ) - os.remove(filename) - - def editable_str(self) -> str: - """Turns the journal into a string of entries that can be edited - manually and later be parsed with eslf.parse_editable_str.""" - return "\n".join([f"{str(e)}\n# {e.uuid}\n" for e in self.entries]) - - def _update_old_entry(self, entry: Entry, new_entry: Entry) -> None: - for attr in ("title", "body", "date", "tags"): - old_attr = getattr(entry, attr) - new_attr = getattr(new_entry, attr) - if old_attr != new_attr: - entry.modified = True - setattr(entry, attr, new_attr) - - def _get_and_remove_uuid_from_entry(self, entry: Entry) -> Entry: - uuid_regex = "^ *?# ([a-zA-Z0-9]+) *?$" - m = re.search(uuid_regex, entry.body, re.MULTILINE) - entry.uuid = m.group(1) if m else None - - # remove the uuid from the body - entry.body = re.sub(uuid_regex, "", entry.body, flags=re.MULTILINE, count=1) - entry.body = entry.body.rstrip() - - return entry - - def parse_editable_str(self, edited: str) -> None: - """Parses the output of self.editable_str and updates its entries.""" - # Method: create a new list of entries from the edited text, then match - # UUIDs of the new entries against self.entries, updating the entries - # if the edited entries differ, and deleting entries from self.entries - # if they don't show up in the edited entries anymore. - entries_from_editor = self._parse(edited) - - for entry in entries_from_editor: - entry = self._get_and_remove_uuid_from_entry(entry) - if entry._tags: - entry._tags.sort() - - # Remove deleted entries - edited_uuids = [e.uuid for e in entries_from_editor] - self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids] - self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids] - - for entry in entries_from_editor: - for old_entry in self.entries: - if entry.uuid == old_entry.uuid: - if old_entry._tags: - tags_not_in_body = [ - tag for tag in old_entry._tags if (tag not in entry._body) - ] - if tags_not_in_body: - entry._tags.extend(tags_not_in_body.sort()) - self._update_old_entry(old_entry, entry) - break diff --git a/jrnl/journals/Entry.py b/jrnl/journals/Entry.py deleted file mode 100644 index 93b825f8..00000000 --- a/jrnl/journals/Entry.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import datetime -import logging -import os -import re -from typing import TYPE_CHECKING - -import ansiwrap - -from jrnl.color import colorize -from jrnl.color import highlight_tags_with_background_color - -if TYPE_CHECKING: - from .Journal import Journal - - -class Entry: - def __init__( - self, - journal: "Journal", - date: datetime.datetime | None = None, - text: str = "", - starred: bool = False, - ): - self.journal = journal # Reference to journal mainly to access its config - self.date = date or datetime.datetime.now() - self.text = text - self._title = None - self._body = None - self._tags = None - self.starred = starred - self.modified = False - - @property - def fulltext(self) -> str: - return self.title + " " + self.body - - def _parse_text(self): - raw_text = self.text - lines = raw_text.splitlines() - if lines and lines[0].strip().endswith("*"): - self.starred = True - raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:]) - self._title, self._body = split_title(raw_text) - if self._tags is None: - self._tags = list(self._parse_tags()) - - @property - def title(self) -> str: - if self._title is None: - self._parse_text() - return self._title - - @title.setter - def title(self, x: str): - self._title = x - - @property - def body(self) -> str: - if self._body is None: - self._parse_text() - return self._body - - @body.setter - def body(self, x: str): - self._body = x - - @property - def tags(self) -> list[str]: - if self._tags is None: - self._parse_text() - return self._tags - - @tags.setter - def tags(self, x: list[str]): - self._tags = x - - @staticmethod - def tag_regex(tagsymbols: str) -> re.Pattern: - pattern = rf"(? set[str]: - tagsymbols = self.journal.config["tagsymbols"] - return { - tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text) - } - - def __str__(self): - """Returns a string representation of the entry to be written into a journal file.""" - date_str = self.date.strftime(self.journal.config["timeformat"]) - title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) - if self.starred: - title += " *" - return "{title}{sep}{body}\n".format( - title=title, - sep="\n" if self.body.rstrip("\n ") else "", - body=self.body.rstrip("\n "), - ) - - def pprint(self, short: bool = False) -> str: - """Returns a pretty-printed version of the entry. - If short is true, only print the title.""" - # Handle indentation - if self.journal.config["indent_character"]: - indent = self.journal.config["indent_character"].rstrip() + " " - else: - indent = "" - - date_str = colorize( - self.date.strftime(self.journal.config["timeformat"]), - self.journal.config["colors"]["date"], - bold=True, - ) - - if not short and self.journal.config["linewrap"]: - columns = self.journal.config["linewrap"] - - if columns == "auto": - try: - columns = os.get_terminal_size().columns - except OSError: - logging.debug( - "Can't determine terminal size automatically 'linewrap': '%s'", - self.journal.config["linewrap"], - ) - columns = 79 - - # Color date / title and bold title - title = ansiwrap.fill( - date_str - + " " - + highlight_tags_with_background_color( - self, - self.title, - self.journal.config["colors"]["title"], - is_title=True, - ), - columns, - ) - body = highlight_tags_with_background_color( - self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"] - ) - body_text = [ - colorize( - ansiwrap.fill( - line, - columns, - initial_indent=indent, - subsequent_indent=indent, - drop_whitespace=True, - ), - self.journal.config["colors"]["body"], - ) - or indent - for line in body.rstrip(" \n").splitlines() - ] - - # ansiwrap doesn't handle lines with only the "\n" character and some - # ANSI escapes properly, so we have this hack here to make sure the - # beginning of each line has the indent character and it's colored - # properly. textwrap doesn't have this issue, however, it doesn't wrap - # the strings properly as it counts ANSI escapes as literal characters. - # TL;DR: I'm sorry. - body = "\n".join( - [ - colorize(indent, self.journal.config["colors"]["body"]) + line - if not ansiwrap.strip_color(line).startswith(indent) - else line - for line in body_text - ] - ) - else: - title = ( - date_str - + " " - + highlight_tags_with_background_color( - self, - self.title.rstrip("\n"), - self.journal.config["colors"]["title"], - is_title=True, - ) - ) - body = highlight_tags_with_background_color( - self, self.body.rstrip("\n "), self.journal.config["colors"]["body"] - ) - - # Suppress bodies that are just blanks and new lines. - has_body = len(self.body) > 20 or not all( - char in (" ", "\n") for char in self.body - ) - - if short: - return title - else: - return "{title}{sep}{body}\n".format( - title=title, sep="\n" if has_body else "", body=body if has_body else "" - ) - - def __repr__(self): - return "".format( - self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M") - ) - - def __hash__(self): - return hash(self.__repr__()) - - def __eq__(self, other: "Entry"): - if ( - not isinstance(other, Entry) - or self.title.strip() != other.title.strip() - or self.body.rstrip() != other.body.rstrip() - or self.date != other.date - or self.starred != other.starred - ): - return False - return True - - def __ne__(self, other: "Entry"): - return not self.__eq__(other) - - -# Based on Segtok by Florian Leitner -# https://github.com/fnl/segtok -SENTENCE_SPLITTER = re.compile( - r""" - ( - [.!?\u2026\u203C\u203D\u2047\u2048\u2049\u22EF\uFE52\uFE57] # Sequence starting with a sentence terminal, - [\'\u2019\"\u201D]? # an optional right quote, - [\]\)]* # optional closing bracket - \s+ # AND a sequence of required spaces. - ) - |[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces. - """, - re.VERBOSE, -) - -SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n") - - -def split_title(text: str) -> tuple[str, str]: - """Splits the first sentence off from a text.""" - sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip()) - if not sep: - sep = SENTENCE_SPLITTER.search(text) - if not sep: - return text, "" - return text[: sep.end()].strip(), text[sep.end() :].strip() diff --git a/jrnl/journals/FolderJournal.py b/jrnl/journals/FolderJournal.py deleted file mode 100644 index 0d497fb8..00000000 --- a/jrnl/journals/FolderJournal.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import codecs -import os -import pathlib -from typing import TYPE_CHECKING - -from jrnl import time - -from .Journal import Journal - -if TYPE_CHECKING: - from jrnl.journals import Entry - -# glob search patterns for folder/file structure -DIGIT_PATTERN = "[0123456789]" -YEAR_PATTERN = DIGIT_PATTERN * 4 -MONTH_PATTERN = "[01]" + DIGIT_PATTERN -DAY_PATTERN = "[0123]" + DIGIT_PATTERN + ".txt" - - -class Folder(Journal): - """A Journal handling multiple files in a folder""" - - def __init__(self, name: str = "default", **kwargs): - self.entries = [] - self._diff_entry_dates = [] - self.can_be_encrypted = False - super().__init__(name, **kwargs) - - def open(self) -> "Folder": - filenames = [] - self.entries = [] - - if os.path.exists(self.config["journal"]): - filenames = Folder._get_files(self.config["journal"]) - for filename in filenames: - with codecs.open(filename, "r", "utf-8") as f: - journal = f.read() - self.entries.extend(self._parse(journal)) - self.sort() - - return self - - def write(self) -> None: - """Writes only the entries that have been modified into proper files.""" - # Create a list of dates of modified entries. Start with diff_entry_dates - modified_dates = self._diff_entry_dates - seen_dates = set(self._diff_entry_dates) - - for e in self.entries: - if e.modified: - if e.date not in modified_dates: - modified_dates.append(e.date) - if e.date not in seen_dates: - seen_dates.add(e.date) - - # For every date that had a modified entry, write to a file - for d in modified_dates: - write_entries = [] - filename = os.path.join( - self.config["journal"], - d.strftime("%Y"), - d.strftime("%m"), - d.strftime("%d") + ".txt", - ) - dirname = os.path.dirname(filename) - # create directory if it doesn't exist - if not os.path.exists(dirname): - os.makedirs(dirname) - for e in self.entries: - if ( - e.date.year == d.year - and e.date.month == d.month - and e.date.day == d.day - ): - write_entries.append(e) - journal = "\n".join([e.__str__() for e in write_entries]) - with codecs.open(filename, "w", "utf-8") as journal_file: - journal_file.write(journal) - # look for and delete empty files - filenames = [] - filenames = Folder._get_files(self.config["journal"]) - for filename in filenames: - if os.stat(filename).st_size <= 0: - os.remove(filename) - - def delete_entries(self, entries_to_delete: list["Entry"]) -> None: - """Deletes specific entries from a journal.""" - for entry in entries_to_delete: - self.entries.remove(entry) - self._diff_entry_dates.append(entry.date) - self.deleted_entry_count += 1 - - def change_date_entries(self, date: str, entries_to_change: list["Entry"]) -> None: - """Changes entry dates to given date.""" - - date = time.parse(date) - - self._diff_entry_dates.append(date) - - for entry in entries_to_change: - self._diff_entry_dates.append(entry.date) - entry.date = date - entry.modified = True - - def parse_editable_str(self, edited: str) -> None: - """Parses the output of self.editable_str and updates its entries.""" - mod_entries = self._parse(edited) - diff_entries = set(self.entries) - set(mod_entries) - for e in diff_entries: - self._diff_entry_dates.append(e.date) - # Match those entries that can be found in self.entries and set - # these to modified, so we can get a count of how many entries got - # modified and how many got deleted later. - for entry in mod_entries: - entry.modified = not any(entry == old_entry for old_entry in self.entries) - - self.increment_change_counts_by_edit(mod_entries) - self.entries = mod_entries - - @staticmethod - def _get_files(journal_path: str) -> list[str]: - """Searches through sub directories starting with journal_path and find all text files that look like entries""" - for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)): - for month_folder in Folder._get_month_folders(year_folder): - yield from Folder._get_day_files(month_folder) - - @staticmethod - def _get_year_folders(path: pathlib.Path) -> list[pathlib.Path]: - for child in path.glob(YEAR_PATTERN): - if child.is_dir(): - yield child - return - - @staticmethod - def _get_month_folders(path: pathlib.Path) -> list[pathlib.Path]: - for child in path.glob(MONTH_PATTERN): - if int(child.name) > 0 and int(child.name) <= 12 and path.is_dir(): - yield child - return - - @staticmethod - def _get_day_files(path: pathlib.Path) -> list[str]: - for child in path.glob(DAY_PATTERN): - if ( - int(child.stem) > 0 - and int(child.stem) <= 31 - and time.is_valid_date( - year=int(path.parent.name), - month=int(path.name), - day=int(child.stem), - ) - and child.is_file() - ): - yield str(child) diff --git a/jrnl/journals/Journal.py b/jrnl/journals/Journal.py deleted file mode 100644 index bd72788b..00000000 --- a/jrnl/journals/Journal.py +++ /dev/null @@ -1,511 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import datetime -import logging -import os -import re - -from jrnl import time -from jrnl.config import validate_journal_name -from jrnl.encryption import determine_encryption_method -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.path import expand_path -from jrnl.prompt import yesno - -from .Entry import Entry - - -class Tag: - def __init__(self, name, count=0): - self.name = name - self.count = count - - def __str__(self): - return self.name - - def __repr__(self): - return f"" - - -class Journal: - def __init__(self, name="default", **kwargs): - self.config = { - "journal": "journal.txt", - "encrypt": False, - "default_hour": 9, - "default_minute": 0, - "timeformat": "%Y-%m-%d %H:%M", - "tagsymbols": "@", - "highlight": True, - "linewrap": 80, - "indent_character": "|", - } - self.config.update(kwargs) - # Set up date parser - self.search_tags = None # Store tags we're highlighting - self.name = name - self.entries = [] - self.encryption_method = None - - # Track changes to journal in session. Modified is tracked in Entry - self.added_entry_count = 0 - self.deleted_entry_count = 0 - - def __len__(self): - """Returns the number of entries""" - return len(self.entries) - - def __iter__(self): - """Iterates over the journal's entries.""" - return (entry for entry in self.entries) - - @classmethod - def from_journal(cls, other: "Journal") -> "Journal": - """Creates a new journal by copying configuration and entries from - another journal object""" - new_journal = cls(other.name, **other.config) - new_journal.entries = other.entries - logging.debug( - "Imported %d entries from %s to %s", - len(new_journal), - other.__class__.__name__, - cls.__name__, - ) - return new_journal - - def import_(self, other_journal_txt: str) -> None: - imported_entries = self._parse(other_journal_txt) - for entry in imported_entries: - entry.modified = True - - self.entries = list(frozenset(self.entries) | frozenset(imported_entries)) - self.sort() - - def _get_encryption_method(self) -> None: - encryption_method = determine_encryption_method(self.config["encrypt"]) - self.encryption_method = encryption_method(self.name, self.config) - - def _decrypt(self, text: bytes) -> str: - if self.encryption_method is None: - self._get_encryption_method() - - return self.encryption_method.decrypt(text) - - def _encrypt(self, text: str) -> bytes: - if self.encryption_method is None: - self._get_encryption_method() - - return self.encryption_method.encrypt(text) - - def open(self, filename: str | None = None) -> "Journal": - """Opens the journal file defined in the config and parses it into a list of Entries. - Entries have the form (date, title, body).""" - filename = filename or self.config["journal"] - dirname = os.path.dirname(filename) - if not os.path.exists(filename): - if not os.path.isdir(dirname): - os.makedirs(dirname) - print_msg( - Message( - MsgText.DirectoryCreated, - MsgStyle.NORMAL, - {"directory_name": dirname}, - ) - ) - self.create_file(filename) - print_msg( - Message( - MsgText.JournalCreated, - MsgStyle.NORMAL, - { - "journal_name": self.name, - "filename": filename, - }, - ) - ) - self.write() - - text = self._load(filename) - text = self._decrypt(text) - self.entries = self._parse(text) - self.sort() - logging.debug("opened %s with %d entries", self.__class__.__name__, len(self)) - return self - - def write(self, filename: str | None = None) -> None: - """Dumps the journal into the config file, overwriting it""" - filename = filename or self.config["journal"] - text = self._to_text() - text = self._encrypt(text) - self._store(filename, text) - - def validate_parsing(self) -> bool: - """Confirms that the jrnl is still parsed correctly after being dumped to text.""" - new_entries = self._parse(self._to_text()) - return all(entry == new_entries[i] for i, entry in enumerate(self.entries)) - - @staticmethod - def create_file(filename: str) -> None: - with open(filename, "w"): - pass - - def _to_text(self) -> str: - return "\n".join([str(e) for e in self.entries]) - - def _load(self, filename: str) -> bytes: - with open(filename, "rb") as f: - return f.read() - - def _store(self, filename: str, text: bytes) -> None: - with open(filename, "wb") as f: - f.write(text) - - def _parse(self, journal_txt: str) -> list[Entry]: - """Parses a journal that's stored in a string and returns a list of entries""" - - # Return empty array if the journal is blank - if not journal_txt: - return [] - - # Initialise our current entry - entries = [] - - date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ") - last_entry_pos = 0 - for match in date_blob_re.finditer(journal_txt): - date_blob = match.groups()[0] - try: - new_date = datetime.datetime.strptime( - date_blob, self.config["timeformat"] - ) - except ValueError: - # Passing in a date that had brackets around it - new_date = time.parse(date_blob, bracketed=True) - - if new_date: - if entries: - entries[-1].text = journal_txt[last_entry_pos : match.start()] - last_entry_pos = match.end() - entries.append(Entry(self, date=new_date)) - - # If no entries were found, treat all the existing text as an entry made now - if not entries: - entries.append(Entry(self, date=time.parse("now"))) - - # Fill in the text of the last entry - entries[-1].text = journal_txt[last_entry_pos:] - - for entry in entries: - entry._parse_text() - return entries - - def pprint(self, short: bool = False) -> str: - """Prettyprints the journal's entries""" - return "\n".join([e.pprint(short=short) for e in self.entries]) - - def __str__(self): - return self.pprint() - - def __repr__(self): - return f"" - - def sort(self) -> None: - """Sorts the Journal's entries by date""" - self.entries = sorted(self.entries, key=lambda entry: entry.date) - - def limit(self, n: int | None = None) -> None: - """Removes all but the last n entries""" - if n: - self.entries = self.entries[-n:] - - @property - def tags(self) -> list[Tag]: - """Returns a set of tuples (count, tag) for all tags present in the journal.""" - # Astute reader: should the following line leave you as puzzled as me the first time - # I came across this construction, worry not and embrace the ensuing moment of enlightment. - tags = [tag for entry in self.entries for tag in set(entry.tags)] - # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] - tag_counts = {(tags.count(tag), tag) for tag in tags} - return [Tag(tag, count=count) for count, tag in sorted(tag_counts)] - - def filter( - self, - tags=[], - month=None, - day=None, - year=None, - start_date=None, - end_date=None, - starred=False, - tagged=False, - exclude_starred=False, - exclude_tagged=False, - strict=False, - contains=None, - exclude=[], - ): - """Removes all entries from the journal that don't match the filter. - - tags is a list of tags, each being a string that starts with one of the - tag symbols defined in the config, e.g. ["@John", "#WorldDomination"]. - - start_date and end_date define a timespan by which to filter. - - starred limits journal to starred entries - - If strict is True, all tags must be present in an entry. If false, the - - exclude is a list of the tags which should not appear in the results. - entry is kept if any tag is present, unless they appear in exclude.""" - self.search_tags = {tag.lower() for tag in tags} - excluded_tags = {tag.lower() for tag in exclude} - end_date = time.parse(end_date, inclusive=True) - start_date = time.parse(start_date) - - # If strict mode is on, all tags have to be present in entry - has_tags = ( - self.search_tags.issubset if strict else self.search_tags.intersection - ) - - def excluded(tags): - return 0 < len([tag for tag in tags if tag in excluded_tags]) - - if contains: - contains_lower = contains.casefold() - - # Create datetime object for comparison below - # this approach allows various formats - if month or day or year: - compare_d = time.parse(f"{month or 1}.{day or 1}.{year or 1}") - - result = [ - entry - for entry in self.entries - if (not tags or has_tags(entry.tags)) - and (not (starred or exclude_starred) or entry.starred == starred) - and (not (tagged or exclude_tagged) or bool(entry.tags) == tagged) - and (not month or entry.date.month == compare_d.month) - and (not day or entry.date.day == compare_d.day) - and (not year or entry.date.year == compare_d.year) - and (not start_date or entry.date >= start_date) - and (not end_date or entry.date <= end_date) - and (not exclude or not excluded(entry.tags)) - and ( - not contains - or ( - contains_lower in entry.title.casefold() - or contains_lower in entry.body.casefold() - ) - ) - ] - - self.entries = result - - def delete_entries(self, entries_to_delete: list[Entry]) -> None: - """Deletes specific entries from a journal.""" - for entry in entries_to_delete: - self.entries.remove(entry) - self.deleted_entry_count += 1 - - def change_date_entries( - self, date: datetime.datetime, entries_to_change: list[Entry] - ) -> None: - """Changes entry dates to given date.""" - date = time.parse(date) - - for entry in entries_to_change: - entry.date = date - entry.modified = True - - def prompt_action_entries(self, msg: MsgText) -> list[Entry]: - """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( - Message( - msg, - params={"entry_title": entry.pprint(short=True)}, - ), - default=False, - ) - - for entry in self.entries: - if ask_action(entry): - to_act.append(entry) - - return to_act - - def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry: - """Constructs a new entry from some raw text input. - If a date is given, it will parse and use this, otherwise scan for a date in the input first. - """ - - raw = raw.replace("\\n ", "\n").replace("\\n", "\n") - # Split raw text into title and body - sep = re.search(r"\n|[?!.]+ +\n?", raw) - first_line = raw[: sep.end()].strip() if sep else raw - starred = False - - if not date: - colon_pos = first_line.find(": ") - if colon_pos > 0: - date = time.parse( - raw[:colon_pos], - default_hour=self.config["default_hour"], - default_minute=self.config["default_minute"], - ) - if date: # Parsed successfully, strip that from the raw text - starred = raw[:colon_pos].strip().endswith("*") - raw = raw[colon_pos + 1 :].strip() - starred = ( - starred - or first_line.startswith("*") - or first_line.endswith("*") - or raw.startswith("*") - ) - if not date: # Still nothing? Meh, just live in the moment. - date = time.parse("now") - entry = Entry(self, date, raw, starred=starred) - entry.modified = True - self.entries.append(entry) - if sort: - self.sort() - return entry - - def editable_str(self) -> str: - """Turns the journal into a string of entries that can be edited - manually and later be parsed with self.parse_editable_str.""" - return "\n".join([str(e) for e in self.entries]) - - def parse_editable_str(self, edited: str) -> None: - """Parses the output of self.editable_str and updates it's entries.""" - mod_entries = self._parse(edited) - # Match those entries that can be found in self.entries and set - # these to modified, so we can get a count of how many entries got - # modified and how many got deleted later. - for entry in mod_entries: - entry.modified = not any(entry == old_entry for old_entry in self.entries) - - self.increment_change_counts_by_edit(mod_entries) - - self.entries = mod_entries - - def increment_change_counts_by_edit(self, mod_entries: Entry) -> None: - if len(mod_entries) > len(self.entries): - self.added_entry_count += len(mod_entries) - len(self.entries) - else: - self.deleted_entry_count += len(self.entries) - len(mod_entries) - - def get_change_counts(self) -> dict: - return { - "added": self.added_entry_count, - "deleted": self.deleted_entry_count, - "modified": len([e for e in self.entries if e.modified]), - } - - -class LegacyJournal(Journal): - """Legacy class to support opening journals formatted with the jrnl 1.x - standard. Main difference here is that in 1.x, timestamps were not cuddled - by square brackets. You'll not be able to save these journals anymore.""" - - def _parse(self, journal_txt: str) -> list[Entry]: - """Parses a journal that's stored in a string and returns a list of entries""" - # Entries start with a line that looks like 'date title' - let's figure out how - # long the date will be by constructing one - date_length = len(datetime.datetime.today().strftime(self.config["timeformat"])) - - # Initialise our current entry - entries = [] - current_entry = None - new_date_format_regex = re.compile(r"(^\[[^\]]+\].*?$)") - for line in journal_txt.splitlines(): - line = line.rstrip() - try: - # try to parse line as date => new entry begins - new_date = datetime.datetime.strptime( - line[:date_length], self.config["timeformat"] - ) - - # parsing successful => save old entry and create new one - if new_date and current_entry: - entries.append(current_entry) - - if line.endswith("*"): - starred = True - line = line[:-1] - else: - starred = False - - current_entry = Entry( - self, date=new_date, text=line[date_length + 1 :], starred=starred - ) - except ValueError: - # Happens when we can't parse the start of the line as an date. - # In this case, just append line to our body (after some - # escaping for the new format). - line = new_date_format_regex.sub(r" \1", line) - if current_entry: - current_entry.text += line + "\n" - - # Append last entry - if current_entry: - entries.append(current_entry) - for entry in entries: - entry._parse_text() - return entries - - -def open_journal(journal_name: str, config: dict, legacy: bool = False) -> Journal: - """ - Creates a normal, encrypted or DayOne journal based on the passed config. - If legacy is True, it will open Journals with legacy classes build for - backwards compatibility with jrnl 1.x - """ - logging.debug(f"open_journal '{journal_name}'") - validate_journal_name(journal_name, config) - config = config.copy() - config["journal"] = expand_path(config["journal"]) - - if os.path.isdir(config["journal"]): - if config["encrypt"]: - print_msg( - Message( - MsgText.ConfigEncryptedForUnencryptableJournalType, - MsgStyle.WARNING, - { - "journal_name": journal_name, - }, - ) - ) - - if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir( - config["journal"] - ): - from jrnl.journals import DayOne - - return DayOne(**config).open() - else: - from jrnl.journals import Folder - - return Folder(journal_name, **config).open() - - if not config["encrypt"]: - if legacy: - return LegacyJournal(journal_name, **config).open() - if config["journal"].endswith(os.sep): - from jrnl.journals import Folder - - return Folder(journal_name, **config).open() - return Journal(journal_name, **config).open() - - if legacy: - config["encrypt"] = "jrnlv1" - return LegacyJournal(journal_name, **config).open() - return Journal(journal_name, **config).open() diff --git a/jrnl/journals/__init__.py b/jrnl/journals/__init__.py deleted file mode 100644 index eb3dc44f..00000000 --- a/jrnl/journals/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .DayOneJournal import DayOne -from .Entry import Entry -from .FolderJournal import Folder -from .Journal import Journal -from .Journal import open_journal diff --git a/jrnl/keyring.py b/jrnl/keyring.py deleted file mode 100644 index 42396a7a..00000000 --- a/jrnl/keyring.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import keyring - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg - - -def get_keyring_password(journal_name: str = "default") -> str | None: - try: - return keyring.get_password("jrnl", journal_name) - except keyring.errors.KeyringError as e: - if not isinstance(e, keyring.errors.NoKeyringError): - print_msg(Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR)) - return None - - -def set_keyring_password(password: str, journal_name: str = "default") -> None: - try: - return keyring.set_password("jrnl", journal_name, password) - except keyring.errors.KeyringError as e: - if isinstance(e, keyring.errors.NoKeyringError): - msg = Message(MsgText.KeyringBackendNotFound, MsgStyle.WARNING) - else: - msg = Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR) - print_msg(msg) diff --git a/jrnl/messages/Message.py b/jrnl/messages/Message.py deleted file mode 100644 index 92562a59..00000000 --- a/jrnl/messages/Message.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING -from typing import Mapping -from typing import NamedTuple - -from jrnl.messages.MsgStyle import MsgStyle - -if TYPE_CHECKING: - from jrnl.messages.MsgText import MsgText - - -class Message(NamedTuple): - text: "MsgText" - style: MsgStyle = MsgStyle.NORMAL - params: Mapping = {} diff --git a/jrnl/messages/MsgStyle.py b/jrnl/messages/MsgStyle.py deleted file mode 100644 index ee898fdb..00000000 --- a/jrnl/messages/MsgStyle.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from enum import Enum -from typing import Callable -from typing import NamedTuple - -from rich import box -from rich.panel import Panel - -from jrnl.messages.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) diff --git a/jrnl/messages/MsgText.py b/jrnl/messages/MsgText.py deleted file mode 100644 index c8ac5b04..00000000 --- a/jrnl/messages/MsgText.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -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" - - InstallComplete = """ - jrnl configuration created at {config_path} - For advanced features, read the docs at https://jrnl.sh - """ - - # --- 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) - """ - UseColorsQuestion = """ - Do you want jrnl to use colors when displaying entries? (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. - """ - - DecryptionFailedGeneric = "The decryption of journal data failed." - - KeyboardInterruptMsg = "Aborted by user" - - CantReadTemplate = """ - Unable to find a template file {template_path}. - - The following paths were checked: - * {jrnl_template_dir}{template_path} - * {actual_template_path} - """ - - NoNamedJournal = "No '{journal_name}' 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/ - """ - - NoEditsReceivedJournalNotDeleted = """ - No text received from editor. Were you trying to delete all the entries? - - This seems a bit drastic, so the operation was cancelled. - - To delete all entries, use the --delete option. - """ - - NoEditsReceived = "No edits to save, because nothing was changed" - - NoTextReceived = """ - No entry to save, because no text was received - """ - NoChangesToTemplate = """ - No entry to save, because the template was not changed - """ - # --- 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} - """ - - ConfigDoubleKeys = """ - There is at least one duplicate key in your configuration file. - - Details: - {error_message} - """ - - # --- 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 - """ - - NoEntriesFound = "no entries found" - EntryFoundCountSingular = "{num} entry found" - EntryFoundCountPlural = "{num} entries found" - - # --- 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. - """ diff --git a/jrnl/messages/__init__.py b/jrnl/messages/__init__.py deleted file mode 100644 index 5f520c10..00000000 --- a/jrnl/messages/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText - -Message = Message.Message -MsgStyle = MsgStyle.MsgStyle -MsgText = MsgText.MsgText diff --git a/jrnl/os_compat.py b/jrnl/os_compat.py deleted file mode 100644 index 16a689a0..00000000 --- a/jrnl/os_compat.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import shlex -from sys import platform - - -def on_windows() -> bool: - return "win32" in platform - - -def on_posix() -> bool: - return not on_windows() - - -def split_args(args: str) -> list[str]: - """Split arguments and add escape characters as appropriate for the OS""" - return shlex.split(args, posix=on_posix()) diff --git a/jrnl/output.py b/jrnl/output.py deleted file mode 100644 index f11f2382..00000000 --- a/jrnl/output.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import textwrap -from typing import Callable - -from rich.console import Console -from rich.text import Text - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText - - -def deprecated_cmd( - old_cmd: str, new_cmd: str, callback: Callable | None = None, **kwargs -) -> None: - print_msg( - Message( - MsgText.DeprecatedCommand, - MsgStyle.WARNING, - {"old_cmd": old_cmd, "new_cmd": new_cmd}, - ) - ) - - if callback is not None: - callback(**kwargs) - - -def journal_list_to_json(journal_list: dict) -> str: - import json - - return json.dumps(journal_list) - - -def journal_list_to_yaml(journal_list: dict) -> str: - from io import StringIO - - from ruamel.yaml import YAML - - output = StringIO() - YAML().dump(journal_list, output) - return output.getvalue() - - -def journal_list_to_stdout(journal_list: dict) -> str: - result = f"Journals defined in config ({journal_list['config_path']})\n" - ml = min(max(len(k) for k in journal_list["journals"]), 20) - for journal, cfg in journal_list["journals"].items(): - result += " * {:{}} -> {}\n".format( - journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg - ) - return result - - -def list_journals(configuration: dict, format: str | None = None) -> str: - from jrnl import config - - """List the journals specified in the configuration file""" - - journal_list = { - "config_path": config.get_config_path(), - "journals": configuration["journals"], - } - - if format == "json": - return journal_list_to_json(journal_list) - elif format == "yaml": - return journal_list_to_yaml(journal_list) - else: - return journal_list_to_stdout(journal_list) - - -def print_msg(msg: Message, **kwargs) -> str | None: - """Helper function to print a single message""" - kwargs["style"] = msg.style - return print_msgs([msg], **kwargs) - - -def print_msgs( - msgs: list[Message], - delimiter: str = "\n", - style: MsgStyle = MsgStyle.NORMAL, - get_input: bool = False, - hide_input: bool = False, -) -> str | None: - # Same as print_msg, but for a list - text = Text("", end="") - kwargs = style.decoration.args - - 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: dict, msg: Message): - 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) - text = text.format(**msg.params) - # dedent again in case inserted text needs it - text = textwrap.dedent(text) - text = text.strip() - return Text(text) diff --git a/jrnl/override.py b/jrnl/override.py deleted file mode 100644 index 64a0fe86..00000000 --- a/jrnl/override.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING - -from jrnl.config import make_yaml_valid_dict -from jrnl.config import update_config - -if TYPE_CHECKING: - from argparse import Namespace - - -# import logging -def apply_overrides(args: "Namespace", base_config: dict) -> dict: - """Unpack CLI provided overrides into the configuration tree. - - :param overrides: List of configuration key-value pairs collected from the CLI - :type overrides: list - :param base_config: Configuration Loaded from the saved YAML - :type base_config: dict - :return: Configuration to be used during runtime with the overrides applied - :rtype: dict - """ - overrides = vars(args).get("config_override", None) - if not overrides: - return base_config - - cfg_with_overrides = base_config.copy() - for pairs in overrides: - pairs = make_yaml_valid_dict(pairs) - key_as_dots, override_value = _get_key_and_value_from_pair(pairs) - keys = _convert_dots_to_list(key_as_dots) - cfg_with_overrides = _recursively_apply( - cfg_with_overrides, keys, override_value - ) - - update_config(base_config, cfg_with_overrides, None) - return base_config - - -def _get_key_and_value_from_pair(pairs: dict) -> tuple: - key_as_dots, override_value = list(pairs.items())[0] - return key_as_dots, override_value - - -def _convert_dots_to_list(key_as_dots: str) -> list[str]: - keys = key_as_dots.split(".") - keys = [k for k in keys if k != ""] # remove empty elements - return keys - - -def _recursively_apply(tree: dict, nodes: list, override_value) -> dict: - """Recurse through configuration and apply overrides at the leaf of the config tree - - Credit to iJames on SO: https://stackoverflow.com/a/47276490 for algorithm - - Args: - config (dict): Configuration to modify - nodes (list): Vector of override keys; the length of the vector indicates tree depth - override_value (str): Runtime override passed from the command-line - """ - key = nodes[0] - if len(nodes) == 1: - tree[key] = override_value - else: - next_key = nodes[1:] - next_node = _get_config_node(tree, key) - _recursively_apply(next_node, next_key, override_value) - - return tree - - -def _get_config_node(config: dict, key: str): - if key in config: - pass - else: - config[key] = None - return config[key] diff --git a/jrnl/path.py b/jrnl/path.py deleted file mode 100644 index 4361cf97..00000000 --- a/jrnl/path.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import os.path -from pathlib import Path - -import xdg.BaseDirectory - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText - -# Constants -XDG_RESOURCE = "jrnl" -DEFAULT_CONFIG_NAME = "jrnl.yaml" -DEFAULT_JOURNAL_NAME = "journal.txt" - - -def home_dir() -> str: - return os.path.expanduser("~") - - -def expand_path(path: str) -> str: - return os.path.expanduser(os.path.expandvars(path)) - - -def absolute_path(path: str) -> str: - return os.path.abspath(expand_path(path)) - - -def get_default_journal_path() -> str: - journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir() - return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME) - - -def get_templates_path() -> str: - """ - Get the path to the XDG templates directory. Creates the directory if it - doesn't exist. - """ - # jrnl_xdg_resource_path is created by save_data_path if it does not exist - jrnl_xdg_resource_path = Path(xdg.BaseDirectory.save_data_path(XDG_RESOURCE)) - jrnl_templates_path = jrnl_xdg_resource_path / "templates" - # Create the directory if needed. - jrnl_templates_path.mkdir(exist_ok=True) - return str(jrnl_templates_path) - - -def get_config_directory() -> str: - try: - return xdg.BaseDirectory.save_config_path(XDG_RESOURCE) - except FileExistsError: - raise JrnlException( - Message( - MsgText.ConfigDirectoryIsFile, - MsgStyle.ERROR, - { - "config_directory_path": os.path.join( - xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE - ) - }, - ), - ) - - -def get_config_path() -> str: - try: - config_directory_path = get_config_directory() - except JrnlException: - return os.path.join(home_dir(), DEFAULT_CONFIG_NAME) - return os.path.join(config_directory_path, DEFAULT_CONFIG_NAME) diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py deleted file mode 100644 index 993d8686..00000000 --- a/jrnl/plugins/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import Type - -from jrnl.plugins.dates_exporter import DatesExporter -from jrnl.plugins.fancy_exporter import FancyExporter -from jrnl.plugins.jrnl_importer import JRNLImporter -from jrnl.plugins.json_exporter import JSONExporter -from jrnl.plugins.markdown_exporter import MarkdownExporter -from jrnl.plugins.tag_exporter import TagExporter -from jrnl.plugins.text_exporter import TextExporter -from jrnl.plugins.xml_exporter import XMLExporter -from jrnl.plugins.yaml_exporter import YAMLExporter - -__exporters = [ - JSONExporter, - MarkdownExporter, - TagExporter, - DatesExporter, - TextExporter, - XMLExporter, - YAMLExporter, - FancyExporter, -] -__importers = [JRNLImporter] - -__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names} -__exporter_types["pretty"] = None -__exporter_types["short"] = None -__importer_types = {name: plugin for plugin in __importers for name in plugin.names} - -EXPORT_FORMATS = sorted(__exporter_types.keys()) -IMPORT_FORMATS = sorted(__importer_types.keys()) - - -def get_exporter(format: str) -> Type[TextExporter] | None: - for exporter in __exporters: - if hasattr(exporter, "names") and format in exporter.names: - return exporter - return None - - -def get_importer(format: str) -> Type[JRNLImporter] | None: - for importer in __importers: - if hasattr(importer, "names") and format in importer.names: - return importer - return None diff --git a/jrnl/plugins/dates_exporter.py b/jrnl/plugins/dates_exporter.py deleted file mode 100644 index 38d101dd..00000000 --- a/jrnl/plugins/dates_exporter.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from collections import Counter -from typing import TYPE_CHECKING - -from jrnl.plugins.text_exporter import TextExporter - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class DatesExporter(TextExporter): - """This Exporter lists dates and their respective counts, for heatingmapping etc.""" - - names = ["dates"] - extension = "dates" - - @classmethod - def export_entry(cls, entry: "Entry"): - raise NotImplementedError - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns dates and their frequencies for an entire journal.""" - date_counts = Counter() - for entry in journal.entries: - # entry.date.date() gets date without time - date = str(entry.date.date()) - date_counts[date] += 1 - result = "\n".join(f"{date}, {count}" for date, count in date_counts.items()) - return result diff --git a/jrnl/plugins/fancy_exporter.py b/jrnl/plugins/fancy_exporter.py deleted file mode 100644 index 447f1347..00000000 --- a/jrnl/plugins/fancy_exporter.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging -import os -from textwrap import TextWrapper -from typing import TYPE_CHECKING - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.plugins.text_exporter import TextExporter - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class FancyExporter(TextExporter): - """This Exporter can convert entries and journals into text with unicode box drawing characters.""" - - names = ["fancy", "boxed"] - extension = "txt" - - # Top border of the card - border_a = "┎" - border_b = "─" - border_c = "╮" - border_d = "╘" - border_e = "═" - border_f = "╕" - - border_g = "┃" - border_h = "│" - border_i = "┠" - border_j = "╌" - border_k = "┤" - border_l = "┖" - border_m = "┘" - - @classmethod - def export_entry(cls, entry: "Entry") -> str: - """Returns a fancy unicode representation of a single entry.""" - date_str = entry.date.strftime(entry.journal.config["timeformat"]) - - if entry.journal.config["linewrap"]: - linewrap = entry.journal.config["linewrap"] - - if linewrap == "auto": - try: - linewrap = os.get_terminal_size().columns - except OSError: - logging.debug( - "Can't determine terminal size automatically 'linewrap': '%s'", - entry.journal.config["linewrap"], - ) - linewrap = 79 - else: - linewrap = 79 - - initial_linewrap = max((1, linewrap - len(date_str) - 2)) - body_linewrap = linewrap - 2 - card = [ - cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str - ] - check_provided_linewrap_viability(linewrap, card, entry.journal.name) - - w = TextWrapper( - width=initial_linewrap, - initial_indent=cls.border_g + " ", - subsequent_indent=cls.border_g + " ", - ) - - title_lines = w.wrap(entry.title) or [""] - card.append( - title_lines[0].ljust(initial_linewrap + 1) - + cls.border_d - + cls.border_e * (len(date_str) - 1) - + cls.border_f - ) - w.width = body_linewrap - if len(title_lines) > 1: - for line in w.wrap( - " ".join( - [ - title_line[len(w.subsequent_indent) :] - for title_line in title_lines[1:] - ] - ) - ): - card.append(line.ljust(body_linewrap + 1) + cls.border_h) - if entry.body: - card.append(cls.border_i + cls.border_j * body_linewrap + cls.border_k) - for line in entry.body.splitlines(): - body_lines = w.wrap(line) or [cls.border_g] - for body_line in body_lines: - card.append(body_line.ljust(body_linewrap + 1) + cls.border_h) - card.append(cls.border_l + cls.border_b * body_linewrap + cls.border_m) - return "\n".join(card) - - @classmethod - def export_journal(cls, journal) -> str: - """Returns a unicode representation of an entire journal.""" - return "\n".join(cls.export_entry(entry) for entry in journal) - - -def check_provided_linewrap_viability( - linewrap: int, card: list[str], journal: "Journal" -): - if len(card[0]) > linewrap: - width_violation = len(card[0]) - linewrap - raise JrnlException( - Message( - MsgText.LineWrapTooSmallForDateFormat, - MsgStyle.NORMAL, - { - "config_linewrap": linewrap, - "columns": width_violation, - "journal": journal, - }, - ) - ) diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py deleted file mode 100644 index 8c326182..00000000 --- a/jrnl/plugins/jrnl_importer.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import sys -from typing import TYPE_CHECKING - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg - -if TYPE_CHECKING: - from jrnl.journals import Journal - - -class JRNLImporter: - """This plugin imports entries from other jrnl files.""" - - names = ["jrnl"] - - @staticmethod - def import_(journal: "Journal", input: str | None = None) -> None: - """Imports from an existing file if input is specified, and - standard input otherwise.""" - old_cnt = len(journal.entries) - if input: - with open(input, "r", encoding="utf-8") as f: - other_journal_txt = f.read() - else: - try: - other_journal_txt = sys.stdin.read() - except KeyboardInterrupt: - raise JrnlException( - Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE), - Message(MsgText.ImportAborted, MsgStyle.WARNING), - ) - - journal.import_(other_journal_txt) - new_cnt = len(journal.entries) - journal.write() - print_msg( - Message( - MsgText.ImportSummary, - MsgStyle.NORMAL, - { - "count": new_cnt - old_cnt, - "journal_name": journal.name, - }, - ) - ) diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py deleted file mode 100644 index 66d2bcc3..00000000 --- a/jrnl/plugins/json_exporter.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import json -from typing import TYPE_CHECKING - -from jrnl.plugins.text_exporter import TextExporter -from jrnl.plugins.util import get_tags_count - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class JSONExporter(TextExporter): - """This Exporter can convert entries and journals into json.""" - - names = ["json"] - extension = "json" - - @classmethod - def entry_to_dict(cls, entry: "Entry") -> dict: - entry_dict = { - "title": entry.title, - "body": entry.body, - "date": entry.date.strftime("%Y-%m-%d"), - "time": entry.date.strftime("%H:%M"), - "tags": entry.tags, - "starred": entry.starred, - } - if hasattr(entry, "uuid"): - entry_dict["uuid"] = entry.uuid - if ( - hasattr(entry, "creator_device_agent") - or hasattr(entry, "creator_generation_date") - or hasattr(entry, "creator_host_name") - or hasattr(entry, "creator_os_agent") - or hasattr(entry, "creator_software_agent") - ): - entry_dict["creator"] = {} - if hasattr(entry, "creator_device_agent"): - entry_dict["creator"]["device_agent"] = entry.creator_device_agent - if hasattr(entry, "creator_generation_date"): - entry_dict["creator"]["generation_date"] = str( - entry.creator_generation_date - ) - if hasattr(entry, "creator_host_name"): - entry_dict["creator"]["host_name"] = entry.creator_host_name - if hasattr(entry, "creator_os_agent"): - entry_dict["creator"]["os_agent"] = entry.creator_os_agent - if hasattr(entry, "creator_software_agent"): - entry_dict["creator"]["software_agent"] = entry.creator_software_agent - - return entry_dict - - @classmethod - def export_entry(cls, entry: "Entry") -> str: - """Returns a json representation of a single entry.""" - return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n" - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns a json representation of an entire journal.""" - tags = get_tags_count(journal) - result = { - "tags": {tag: count for count, tag in tags}, - "entries": [cls.entry_to_dict(e) for e in journal.entries], - } - return json.dumps(result, indent=2) diff --git a/jrnl/plugins/markdown_exporter.py b/jrnl/plugins/markdown_exporter.py deleted file mode 100644 index 1512903d..00000000 --- a/jrnl/plugins/markdown_exporter.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import os -import re -from typing import TYPE_CHECKING - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.plugins.text_exporter import TextExporter - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class MarkdownExporter(TextExporter): - """This Exporter can convert entries and journals into Markdown.""" - - names = ["md", "markdown"] - extension = "md" - - @classmethod - def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str: - """Returns a markdown representation of a single entry.""" - date_str = entry.date.strftime(entry.journal.config["timeformat"]) - body_wrapper = "\n" if entry.body else "" - body = body_wrapper + entry.body - - if to_multifile is True: - heading = "#" - else: - heading = "###" - - """Increase heading levels in body text""" - newbody = "" - previous_line = "" - warn_on_heading_level = False - for line in body.splitlines(True): - if re.match(r"^#+ ", line): - """ATX style headings""" - newbody = newbody + previous_line + heading + line - if re.match(r"^#######+ ", heading + line): - warn_on_heading_level = True - line = "" - elif re.match(r"^=+$", line.rstrip()) and not re.match( - r"^$", previous_line.strip() - ): - """Setext style H1""" - newbody = newbody + heading + "# " + previous_line - line = "" - elif re.match(r"^-+$", line.rstrip()) and not re.match( - r"^$", previous_line.strip() - ): - """Setext style H2""" - newbody = newbody + heading + "## " + previous_line - line = "" - else: - newbody = newbody + previous_line - previous_line = line - newbody = newbody + previous_line # add very last line - - # make sure the export ends with a blank line - if previous_line not in ["\r", "\n", "\r\n", "\n\r"]: - newbody = newbody + os.linesep - - if warn_on_heading_level is True: - print_msg( - Message( - MsgText.HeadingsPastH6, - MsgStyle.WARNING, - {"date": date_str, "title": entry.title}, - ) - ) - - return f"{heading} {date_str} {entry.title}\n{newbody} " - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns a Markdown representation of an entire journal.""" - out = [] - year, month = -1, -1 - for e in journal.entries: - if e.date.year != year: - year = e.date.year - out.append("# " + str(year)) - out.append("") - if e.date.month != month: - month = e.date.month - out.append("## " + e.date.strftime("%B")) - out.append("") - out.append(cls.export_entry(e, False)) - result = "\n".join(out) - return result diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py deleted file mode 100644 index b8b5eb79..00000000 --- a/jrnl/plugins/tag_exporter.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING - -from jrnl.plugins.text_exporter import TextExporter -from jrnl.plugins.util import get_tags_count - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class TagExporter(TextExporter): - """This Exporter can lists the tags for entries and journals, exported as a plain text file.""" - - names = ["tags"] - extension = "tags" - - @classmethod - def export_entry(cls, entry: "Entry") -> str: - """Returns a list of tags for a single entry.""" - return ", ".join(entry.tags) - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns a list of tags and their frequency for an entire journal.""" - tag_counts = get_tags_count(journal) - result = "" - if not tag_counts: - return "[No tags found in journal.]" - elif min(tag_counts)[0] == 0: - tag_counts = filter(lambda x: x[0] > 1, tag_counts) - result += "[Removed tags that appear only once.]\n" - result += "\n".join( - "{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True) - ) - return result diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py deleted file mode 100644 index 0a514da1..00000000 --- a/jrnl/plugins/text_exporter.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import errno -import os -import re -import unicodedata -from typing import TYPE_CHECKING - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class TextExporter: - """This Exporter can convert entries and journals into text files.""" - - names = ["text", "txt"] - extension = "txt" - - @classmethod - def export_entry(cls, entry: "Entry") -> str: - """Returns a string representation of a single entry.""" - return str(entry) - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns a string representation of an entire journal.""" - return "\n".join(cls.export_entry(entry) for entry in journal) - - @classmethod - def write_file(cls, journal: "Journal", path: str) -> str: - """Exports a journal into a single file.""" - export_str = cls.export_journal(journal) - with open(path, "w", encoding="utf-8") as f: - f.write(export_str) - print_msg( - Message( - MsgText.JournalExportedTo, - MsgStyle.NORMAL, - { - "path": path, - }, - ) - ) - return "" - - @classmethod - def make_filename(cls, entry: "Entry") -> str: - return entry.date.strftime("%Y-%m-%d") + "_{}.{}".format( - cls._slugify(str(entry.title)), cls.extension - ) - - @classmethod - def write_files(cls, journal: "Journal", path: str) -> str: - """Exports a journal into individual files for each entry.""" - for entry in journal.entries: - entry_is_written = False - while not entry_is_written: - full_path = os.path.join(path, cls.make_filename(entry)) - try: - with open(full_path, "w", encoding="utf-8") as f: - f.write(cls.export_entry(entry)) - entry_is_written = True - except OSError as oserr: - title_length = len(str(entry.title)) - if ( - oserr.errno == errno.ENAMETOOLONG - or oserr.errno == errno.ENOENT - or oserr.errno == errno.EINVAL - ) and title_length > 1: - shorter_file_length = title_length // 2 - entry.title = str(entry.title)[:shorter_file_length] - else: - raise - print_msg( - Message( - MsgText.JournalExportedTo, - MsgStyle.NORMAL, - {"path": path}, - ) - ) - return "" - - def _slugify(string: str) -> str: - """Slugifies a string. - Based on public domain code from https://github.com/zacharyvoase/slugify - """ - normalized_string = str(unicodedata.normalize("NFKD", string)) - no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower() - slug = re.sub(r"[-\s]+", "-", no_punctuation) - return slug - - @classmethod - def export(cls, journal: "Journal", output: str | None = None) -> str: - """Exports to individual files if output is an existing path, or into - a single file if output is a file name, or returns the exporter's - representation as string if output is None.""" - if output and os.path.isdir(output): # multiple files - return cls.write_files(journal, output) - elif output: # single file - return cls.write_file(journal, output) - else: - return cls.export_journal(journal) diff --git a/jrnl/plugins/util.py b/jrnl/plugins/util.py deleted file mode 100644 index ceaa0b04..00000000 --- a/jrnl/plugins/util.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from jrnl.journals import Journal - - -def get_tags_count(journal: "Journal") -> set[tuple[int, str]]: - """Returns a set of tuples (count, tag) for all tags present in the journal.""" - # Astute reader: should the following line leave you as puzzled as me the first time - # I came across this construction, worry not and embrace the ensuing moment of enlightment. - tags = [tag for entry in journal.entries for tag in set(entry.tags)] - # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] - tag_counts = {(tags.count(tag), tag) for tag in tags} - return tag_counts - - -def oxford_list(lst: list) -> str: - """Return Human-readable list of things obeying the object comma)""" - lst = sorted(lst) - if not lst: - return "(nothing)" - elif len(lst) == 1: - return lst[0] - elif len(lst) == 2: - return lst[0] + " or " + lst[1] - else: - return ", ".join(lst[:-1]) + ", or " + lst[-1] diff --git a/jrnl/plugins/xml_exporter.py b/jrnl/plugins/xml_exporter.py deleted file mode 100644 index a0349af9..00000000 --- a/jrnl/plugins/xml_exporter.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from typing import TYPE_CHECKING -from xml.dom import minidom - -from jrnl.plugins.json_exporter import JSONExporter -from jrnl.plugins.util import get_tags_count - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class XMLExporter(JSONExporter): - """This Exporter can convert entries and journals into XML.""" - - names = ["xml"] - extension = "xml" - - @classmethod - def export_entry( - cls, entry: "Entry", doc: minidom.Document | None = None - ) -> minidom.Element | str: - """Returns an XML representation of a single entry.""" - doc_el = doc or minidom.Document() - entry_el = doc_el.createElement("entry") - for key, value in cls.entry_to_dict(entry).items(): - elem = doc_el.createElement(key) - elem.appendChild(doc_el.createTextNode(value)) - entry_el.appendChild(elem) - if not doc: - doc_el.appendChild(entry_el) - return doc_el.toprettyxml() - else: - return entry_el - - @classmethod - def entry_to_xml(cls, entry: "Entry", doc: minidom.Document) -> minidom.Element: - entry_el = doc.createElement("entry") - entry_el.setAttribute("date", entry.date.isoformat()) - if hasattr(entry, "uuid"): - entry_el.setAttribute("uuid", entry.uuid) - entry_el.setAttribute("starred", entry.starred) - tags = entry.tags - for tag in tags: - tag_el = doc.createElement("tag") - tag_el.setAttribute("name", tag) - entry_el.appendChild(tag_el) - entry_el.appendChild(doc.createTextNode(entry.fulltext)) - return entry_el - - @classmethod - def export_journal(cls, journal: "Journal") -> str: - """Returns an XML representation of an entire journal.""" - tags = get_tags_count(journal) - doc = minidom.Document() - xml = doc.createElement("journal") - tags_el = doc.createElement("tags") - entries_el = doc.createElement("entries") - for count, tag in tags: - tag_el = doc.createElement("tag") - tag_el.setAttribute("name", tag) - count_node = doc.createTextNode(str(count)) - tag_el.appendChild(count_node) - tags_el.appendChild(tag_el) - for entry in journal.entries: - entries_el.appendChild(cls.entry_to_xml(entry, doc)) - xml.appendChild(entries_el) - xml.appendChild(tags_el) - doc.appendChild(xml) - return doc.toprettyxml() diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py deleted file mode 100644 index d960ef8a..00000000 --- a/jrnl/plugins/yaml_exporter.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import os -import re -from typing import TYPE_CHECKING - -from jrnl.exception import JrnlException -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.plugins.text_exporter import TextExporter - -if TYPE_CHECKING: - from jrnl.journals import Entry - from jrnl.journals import Journal - - -class YAMLExporter(TextExporter): - """This Exporter can convert entries and journals into Markdown formatted text with YAML front matter.""" - - names = ["yaml"] - extension = "md" - - @classmethod - def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str: - """Returns a markdown representation of a single entry, with YAML front matter.""" - if to_multifile is False: - raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR)) - - date_str = entry.date.strftime(entry.journal.config["timeformat"]) - body_wrapper = "\n" if entry.body else "" - body = body_wrapper + entry.body - - tagsymbols = entry.journal.config["tagsymbols"] - # see also Entry.rag_regex - multi_tag_regex = re.compile(rf"(?u)^\s*([{tagsymbols}][-+*#/\w]+\s*)+$") - - """Increase heading levels in body text""" - newbody = "" - heading = "#" - previous_line = "" - warn_on_heading_level = False - for line in body.splitlines(True): - if re.match(r"^#+ ", line): - """ATX style headings""" - newbody = newbody + previous_line + heading + line - if re.match(r"^#######+ ", heading + line): - warn_on_heading_level = True - line = "" - elif re.match(r"^=+$", line.rstrip()) and not re.match( - r"^$", previous_line.strip() - ): - """Setext style H1""" - newbody = newbody + heading + "# " + previous_line - line = "" - elif re.match(r"^-+$", line.rstrip()) and not re.match( - r"^$", previous_line.strip() - ): - """Setext style H2""" - newbody = newbody + heading + "## " + previous_line - line = "" - elif multi_tag_regex.match(line): - """Tag only lines""" - line = "" - else: - newbody = newbody + previous_line - previous_line = line - newbody = newbody + previous_line # add very last line - - # make sure the export ends with a blank line - if previous_line not in ["\r", "\n", "\r\n", "\n\r"]: - newbody = newbody + os.linesep - - # set indentation for YAML body block - spacebody = "\t" - for line in newbody.splitlines(True): - spacebody = spacebody + "\t" + line - - if warn_on_heading_level is True: - print_msg( - Message( - MsgText.HeadingsPastH6, - MsgStyle.WARNING, - {"date": date_str, "title": entry.title}, - ) - ) - - dayone_attributes = "" - if hasattr(entry, "uuid"): - dayone_attributes += "uuid: " + entry.uuid + "\n" - if ( - hasattr(entry, "creator_device_agent") - or hasattr(entry, "creator_generation_date") - or hasattr(entry, "creator_host_name") - or hasattr(entry, "creator_os_agent") - or hasattr(entry, "creator_software_agent") - ): - dayone_attributes += "creator:\n" - if hasattr(entry, "creator_device_agent"): - dayone_attributes += f" device agent: {entry.creator_device_agent}\n" - if hasattr(entry, "creator_generation_date"): - dayone_attributes += " generation date: {}\n".format( - str(entry.creator_generation_date) - ) - if hasattr(entry, "creator_host_name"): - dayone_attributes += f" host name: {entry.creator_host_name}\n" - if hasattr(entry, "creator_os_agent"): - dayone_attributes += f" os agent: {entry.creator_os_agent}\n" - if hasattr(entry, "creator_software_agent"): - dayone_attributes += ( - f" software agent: {entry.creator_software_agent}\n" - ) - - # TODO: copy over pictures, if present - # source directory is entry.journal.config['journal'] - # output directory is...? - - return "{start}\ntitle: {title}\ndate: {date}\nstarred: {starred}\ntags: {tags}\n{dayone}body: |{body}{end}".format( - start="---", - date=date_str, - title=entry.title, - starred=entry.starred, - tags=", ".join([tag[1:] for tag in entry.tags]), - dayone=dayone_attributes, - body=spacebody, - end="...", - ) - - @classmethod - def export_journal(cls, journal: "Journal"): - """Returns an error, as YAML export requires a directory as a target.""" - raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR)) diff --git a/jrnl/prompt.py b/jrnl/prompt.py deleted file mode 100644 index e4071480..00000000 --- a/jrnl/prompt.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.output import print_msgs - - -def create_password(journal_name: str) -> str: - kwargs = { - "get_input": True, - "hide_input": True, - } - while True: - pw = print_msg( - Message( - MsgText.PasswordFirstEntry, - MsgStyle.PROMPT, - params={"journal_name": journal_name}, - ), - **kwargs - ) - - if not pw: - print_msg(Message(MsgText.PasswordCanNotBeEmpty, MsgStyle.WARNING)) - continue - - elif pw == print_msg( - Message(MsgText.PasswordConfirmEntry, MsgStyle.PROMPT), **kwargs - ): - break - - print_msg(Message(MsgText.PasswordDidNotMatch, MsgStyle.ERROR)) - - if yesno(Message(MsgText.PasswordStoreInKeychain), default=True): - from jrnl.keyring import set_keyring_password - - set_keyring_password(pw, journal_name) - - return pw - - -def prompt_password(first_try: bool = True) -> str: - if not first_try: - print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING)) - - return ( - print_msg( - Message(MsgText.Password, MsgStyle.PROMPT), - get_input=True, - hide_input=True, - ) - or "" - ) - - -def yesno(prompt: Message | str, 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) diff --git a/jrnl/templates/sample.template b/jrnl/templates/sample.template deleted file mode 100644 index 983d6af3..00000000 --- a/jrnl/templates/sample.template +++ /dev/null @@ -1,18 +0,0 @@ ---- -extension: txt ---- - -{% block journal %} -{% for entry in entries %} -{% include entry %} -{% endfor %} - -{% endblock %} - -{% block entry %} -{{ entry.title }} -{{ "-" * len(entry.title) }} - -{{ entry.body }} - -{% endblock %} diff --git a/jrnl/time.py b/jrnl/time.py deleted file mode 100644 index dd6fcb0f..00000000 --- a/jrnl/time.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import datetime - -FAKE_YEAR = 9999 -DEFAULT_FUTURE = datetime.datetime(FAKE_YEAR, 12, 31, 23, 59, 59) -DEFAULT_PAST = datetime.datetime(FAKE_YEAR, 1, 1, 0, 0) - - -def __get_pdt_calendar(): - try: - import parsedatetime.parsedatetime_consts as pdt - except ImportError: - import parsedatetime as pdt - - consts = pdt.Constants(usePyICU=False) - consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday - calendar = pdt.Calendar(consts) - - return calendar - - -def parse( - date_str: str | datetime.datetime, - inclusive: bool = False, - default_hour: int | None = None, - default_minute: int | None = None, - bracketed: bool = False, -) -> datetime.datetime | None: - """Parses a string containing a fuzzy date and returns a datetime.datetime object""" - if not date_str: - return None - elif isinstance(date_str, datetime.datetime): - return date_str - - # Don't try to parse anything with 6 or fewer characters and was parsed from the existing journal. - # It's probably a markdown footnote - if len(date_str) <= 6 and bracketed: - return None - - default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST - date = None - year_present = False - while not date: - try: - from dateutil.parser import parse as dateparse - - date = dateparse(date_str, default=default_date) - if date.year == FAKE_YEAR: - date = datetime.datetime( - datetime.datetime.now().year, date.timetuple()[1:6] - ) - else: - year_present = True - flag = 1 if date.hour == date.minute == 0 else 2 - date = date.timetuple() - except Exception as e: - if e.args[0] == "day is out of range for month": - y, m, d, H, M, S = default_date.timetuple()[:6] - default_date = datetime.datetime(y, m, d - 1, H, M, S) - else: - calendar = __get_pdt_calendar() - date, flag = calendar.parse(date_str) - - if not flag: # Oops, unparsable. - try: # Try and parse this as a single year - year = int(date_str) - return datetime.datetime(year, 1, 1) - except ValueError: - return None - except TypeError: - return None - - if flag == 1: # Date found, but no time. Use the default time. - date = datetime.datetime( - *date[:3], - hour=23 if inclusive else default_hour or 0, - minute=59 if inclusive else default_minute or 0, - second=59 if inclusive else 0 - ) - else: - date = datetime.datetime(*date[:6]) - - # Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong. - # Rather than this, we would like to see parsedatetime patched so we can tell it to prefer - # past dates - dt = datetime.datetime.now() - date - if dt.days < -28 and not year_present: - date = date.replace(date.year - 1) - return date - - -def is_valid_date(year: int, month: int, day: int) -> bool: - try: - datetime.datetime(year, month, day) - return True - except ValueError: - return False diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py deleted file mode 100644 index 1b6e500d..00000000 --- a/jrnl/upgrade.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright © 2012-2023 jrnl contributors -# License: https://www.gnu.org/licenses/gpl-3.0.html - -import logging -import os - -from jrnl import __version__ -from jrnl.config import is_config_json -from jrnl.config import load_config -from jrnl.config import scope_config -from jrnl.exception import JrnlException -from jrnl.journals import Journal -from jrnl.journals import open_journal -from jrnl.messages import Message -from jrnl.messages import MsgStyle -from jrnl.messages import MsgText -from jrnl.output import print_msg -from jrnl.output import print_msgs -from jrnl.path import expand_path -from jrnl.prompt import yesno - - -def backup(filename: str, binary: bool = False): - filename = expand_path(filename) - - try: - with open(filename, "rb" if binary else "r") as original: - contents = original.read() - - 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_msg(Message(MsgText.DoesNotExist, MsgStyle.WARNING, {"name": filename})) - cont = yesno(f"\nCreate {filename}?", default=False) - if not cont: - raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING)) - - -def check_exists(path: str) -> bool: - """ - Checks if a given path exists. - """ - return os.path.exists(path) - - -def upgrade_jrnl(config_path: str) -> None: - config = load_config(config_path) - - print_msg(Message(MsgText.WelcomeToJrnl, MsgStyle.NORMAL, {"version": __version__})) - - encrypted_journals = {} - plain_journals = {} - other_journals = {} - all_journals = [] - - for journal_name, journal_conf in config["journals"].items(): - if isinstance(journal_conf, dict): - path = expand_path(journal_conf.get("journal")) - encrypt = journal_conf.get("encrypt") - else: - encrypt = config.get("encrypt") - path = expand_path(journal_conf) - - if os.path.exists(path): - path = os.path.expanduser(path) - else: - print_msg(Message(MsgText.DoesNotExist, MsgStyle.ERROR, {"name": path})) - continue - - if encrypt: - encrypted_journals[journal_name] = path - elif os.path.isdir(path): - other_journals[journal_name] = path - else: - plain_journals[journal_name] = path - - 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, - ) - - _print_journal_summary( - journals=plain_journals, - header=Message( - MsgText.JournalsToUpgrade, - params={ - "version": __version__, - }, - ), - **kwargs, - ) - - _print_journal_summary( - journals=other_journals, - header=Message(MsgText.JournalsToIgnore), - **kwargs, - ) - - cont = yesno(Message(MsgText.ContinueUpgrade), default=False) - if not cont: - raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING)) - - for journal_name, path in encrypted_journals.items(): - print_msg( - Message( - MsgText.UpgradingJournal, - params={ - "journal_name": journal_name, - "path": path, - }, - ) - ) - - backup(path, binary=True) - old_journal = open_journal( - journal_name, scope_config(config, journal_name), legacy=True - ) - - logging.debug(f"Clearing encryption method for '{journal_name}' journal") - - # Update the encryption method - new_journal = Journal.from_journal(old_journal) - new_journal.config["encrypt"] = "jrnlv2" - new_journal._get_encryption_method() - # Copy over password (jrnlv1 only supported password-based encryption) - new_journal.encryption_method.password = old_journal.encryption_method.password - - all_journals.append(new_journal) - - for journal_name, path in plain_journals.items(): - print_msg( - Message( - MsgText.UpgradingJournal, - params={ - "journal_name": journal_name, - "path": path, - }, - ) - ) - - backup(path) - old_journal = open_journal( - journal_name, scope_config(config, journal_name), legacy=True - ) - all_journals.append(Journal.from_journal(old_journal)) - - # loop through lists to validate - failed_journals = [j for j in all_journals if not j.validate_parsing()] - - if len(failed_journals) > 0: - raise JrnlException( - Message(MsgText.AbortingUpgrade, MsgStyle.WARNING), - Message( - MsgText.JournalFailedUpgrade, - 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_msg(Message(MsgText.UpgradingConfig, MsgStyle.NORMAL)) - - backup(config_path) - - print_msg(Message(MsgText.AllDoneUpgrade, MsgStyle.NORMAL)) - - -def is_old_version(config_path: str) -> bool: - 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)