mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-12 09:28:31 +02:00
* Update and modularize exception handling cc #1024 #1141 - Stack traces are no longer shown to users unless the --debug flag is being used - Errors, warnings, and other messages contain color as needed - Converted error messages to Enum - Adds print_msg function to centralize output (this should replace all other output in other modules) Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com> * format with black * add message to catch-all exception block * Unskip some tests (#1399) * remove skip_editor test and tag * remove useless test * unskip blank input test * formatting * rename test so it doesn't overwrite other test * unskip some dayone tests that now work * Bump ipython from 7.28.0 to 7.31.1 (#1401) Bumps [ipython](https://github.com/ipython/ipython) from 7.28.0 to 7.31.1. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/7.28.0...7.31.1) --- updated-dependencies: - dependency-name: ipython dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update changelog [ci skip] * Bump asteval from 0.9.25 to 0.9.26 (#1400) Bumps [asteval](https://github.com/newville/asteval) from 0.9.25 to 0.9.26. - [Release notes](https://github.com/newville/asteval/releases) - [Commits](https://github.com/newville/asteval/compare/0.9.25...0.9.26) --- updated-dependencies: - dependency-name: asteval dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update changelog [ci skip] * Bump black from 21.12b0 to 22.1.0 (#1404) * Bump black from 21.12b0 to 22.1.0 Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.1.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/commits/22.1.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Run make format Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com> * Update changelog [ci skip] * Add reference documentation to docs site and separate out "Tips and Tricks" and "External Editors" from "Recipes" (#1332) * First draft of command line reference, mostly pulled from help screen * Add first draft of config file reference, mostly pulled from advanced.md * Clean up config file doc for readability * Add --config-file and remove examples from CLI reference * Add warning about time zone in timeformat * More small changes, and adding template config keyword * Cleaning up and re-ordering config file reference * Clean up reference and anything else from advanced documentation that can live elsewhere and linking to config file reference wherever config file is mentioned * Fix syntax highlighting in command line reference, clean up content a bit, include --diagnostic * Mention version config key * Apply minor changes suggested in PR review * Rename "recipes" to "Tips and Tricks", pull "External Editors" out of it into its own page, and redirect old recipes link to tips-and-tricks * Revert broken mkdocs-redirects usage from last commit * Update changelog [ci skip] * Add --co alias for --config-override (#1397) * Add hash as a default tag symbol (#1398) * Update changelog [ci skip] * Increment version to v2.8.4-beta2 * Update changelog [ci skip] * Increment version to v2.8.4 * Update changelog [ci skip] * Bump pytest from 6.2.5 to 7.0.0 (#1407) Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update changelog [ci skip] * Drop support for Python 3.7 and 3.8 (#1412) * Remove Python 3.7 and 3.8 from github actions workflows * Update lockfile after running poetry update a couple times * Update poetry lock * Remove Python 3.7 and 3.8 from pyproject.toml and run poetry lock * Update changelog [ci skip] * Tidy up git ignore (#1414) * cleaned gitignore and add comments * removed colon for readbility * alphabetize files in sections Co-authored-by: nelnog <nel.nogales@gmail.com> * fix behavior that was confusing pytest * update test to match new message * whitespace change * clean up error for manually stopping the inline editor * udpate error to use new exception handling * move some exceptions and errors to the new exception handling * add line breaks to keyboard interrupt so it looks more like other exceptions * add handling for exceptions that happen earlier in the flow * add new 'NothingToDelete' error to replace old behavior * get rid of old exception * add new exception handling to 'nothing saved to file' errors * move exception for no editor configured into new handling * move exception for no alt config to new handling * get rid of old exception handling for encrypted journal * Move error for too many wrong passwords into new handling * fix merge errors * replace sys.exit call with new exception handling * replace sys.exit call with new exception handling * replace sys.exit call with new exception handling * reformat with black * clean up old code * clean up old code * clean up linting issue * update uncaught exception for new handling * update test * fix mangled lock file Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jrnl Bot <jrnl.bot@gmail.com> Co-authored-by: Nelson <35701520+nelnog@users.noreply.github.com> Co-authored-by: nelnog <nel.nogales@gmail.com>
340 lines
9.3 KiB
Python
340 lines
9.3 KiB
Python
# Copyright (C) 2012-2021 jrnl contributors
|
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
import logging
|
|
import sys
|
|
|
|
from . import install
|
|
from . import plugins
|
|
from .Journal import open_journal
|
|
from .config import get_journal_name
|
|
from .config import scope_config
|
|
from .config import get_config_path
|
|
from .editor import get_text_from_editor
|
|
from .editor import get_text_from_stdin
|
|
from . import time
|
|
from .override import apply_overrides
|
|
|
|
from jrnl.exception import JrnlException
|
|
from jrnl.messages import Message
|
|
from jrnl.messages import MsgText
|
|
from jrnl.messages import MsgType
|
|
|
|
|
|
def run(args):
|
|
"""
|
|
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 write mode, or search mode
|
|
6. 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,
|
|
}
|
|
|
|
if _is_write_mode(**kwargs):
|
|
write_mode(**kwargs)
|
|
else:
|
|
search_mode(**kwargs)
|
|
|
|
|
|
def _is_write_mode(args, config, **kwargs):
|
|
"""Determines if we are in write mode (as opposed to search mode)"""
|
|
write_mode = True
|
|
|
|
# Are any search filters present? If so, then search mode.
|
|
write_mode = not any(
|
|
(
|
|
args.contains,
|
|
args.delete,
|
|
args.edit,
|
|
args.export,
|
|
args.end_date,
|
|
args.today_in_history,
|
|
args.month,
|
|
args.day,
|
|
args.year,
|
|
args.limit,
|
|
args.on_date,
|
|
args.short,
|
|
args.starred,
|
|
args.start_date,
|
|
args.strict,
|
|
args.tags,
|
|
)
|
|
)
|
|
|
|
# Might be writing and want to move to editor part of the way through
|
|
if args.edit and args.text:
|
|
write_mode = True
|
|
|
|
# If the text is entirely tags, then we are also searching (not writing)
|
|
if (
|
|
write_mode
|
|
and args.text
|
|
and all(word[0] in config["tagsymbols"] for word in " ".join(args.text).split())
|
|
):
|
|
write_mode = False
|
|
|
|
return write_mode
|
|
|
|
|
|
def write_mode(args, config, journal, **kwargs):
|
|
"""
|
|
Gets input from the user to write to the journal
|
|
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("Write mode: starting")
|
|
|
|
if args.text:
|
|
logging.debug("Write mode: cli text detected: %s", args.text)
|
|
raw = " ".join(args.text).strip()
|
|
if args.edit:
|
|
raw = _write_in_editor(config, raw)
|
|
|
|
elif not sys.stdin.isatty():
|
|
logging.debug("Write mode: receiving piped text")
|
|
raw = sys.stdin.read()
|
|
|
|
else:
|
|
raw = _write_in_editor(config)
|
|
|
|
if not raw:
|
|
logging.error("Write mode: couldn't get raw text")
|
|
raise JrnlException(
|
|
Message(MsgText.JrnlExceptionMessage.NoTextReceived, MsgType.ERROR)
|
|
)
|
|
|
|
logging.debug(
|
|
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
|
)
|
|
journal.new_entry(raw)
|
|
print(f"[Entry added to {args.journal_name} journal]", file=sys.stderr)
|
|
journal.write()
|
|
logging.debug("Write mode: completed journal.write()", args.journal_name, raw)
|
|
|
|
|
|
def search_mode(args, journal, **kwargs):
|
|
"""
|
|
Search for entries in a journal, then either:
|
|
1. Send them to configured editor for user manipulation
|
|
2. Delete them (with confirmation for each entry)
|
|
3. Display them (with formatting options)
|
|
"""
|
|
kwargs = {
|
|
**kwargs,
|
|
"args": args,
|
|
"journal": journal,
|
|
"old_entries": journal.entries,
|
|
}
|
|
|
|
# Filters the journal entries in place
|
|
_search_journal(**kwargs)
|
|
|
|
# Where do the search results go?
|
|
if args.edit:
|
|
_edit_search_results(**kwargs)
|
|
|
|
elif args.delete:
|
|
_delete_search_results(**kwargs)
|
|
|
|
else:
|
|
_display_search_results(**kwargs)
|
|
|
|
|
|
def _write_in_editor(config, template=None):
|
|
if config["editor"]:
|
|
logging.debug("Write mode: opening editor")
|
|
if not template:
|
|
template = _get_editor_template(config)
|
|
raw = get_text_from_editor(config, template)
|
|
|
|
else:
|
|
raw = get_text_from_stdin()
|
|
|
|
return raw
|
|
|
|
|
|
def _get_editor_template(config, **kwargs):
|
|
logging.debug("Write mode: loading template for entry")
|
|
|
|
if not config["template"]:
|
|
logging.debug("Write mode: no template configured")
|
|
return ""
|
|
|
|
try:
|
|
template = open(config["template"]).read()
|
|
logging.debug("Write mode: template loaded: %s", template)
|
|
except OSError:
|
|
logging.error("Write mode: template not loaded")
|
|
raise JrnlException(
|
|
Message(
|
|
MsgText.CantReadTemplate,
|
|
MsgType.ERROR,
|
|
{"template": config["template"]},
|
|
)
|
|
)
|
|
|
|
return template
|
|
|
|
|
|
def _search_journal(args, journal, **kwargs):
|
|
"""Search the journal with the given 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,
|
|
exclude=args.excluded,
|
|
contains=args.contains,
|
|
)
|
|
journal.limit(args.limit)
|
|
|
|
|
|
def _edit_search_results(config, journal, old_entries, **kwargs):
|
|
"""
|
|
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,
|
|
MsgType.ERROR,
|
|
{"config_file": get_config_path()},
|
|
)
|
|
)
|
|
|
|
# separate entries we are not editing
|
|
other_entries = [e for e in old_entries if e not in journal.entries]
|
|
|
|
# Get stats now for summary later
|
|
old_stats = _get_predit_stats(journal)
|
|
|
|
# Send user to the editor
|
|
edited = get_text_from_editor(config, journal.editable_str())
|
|
journal.parse_editable_str(edited)
|
|
|
|
# Print summary if available
|
|
_print_edited_summary(journal, old_stats)
|
|
|
|
# Put back entries we separated earlier, sort, and write the journal
|
|
journal.entries += other_entries
|
|
journal.sort()
|
|
journal.write()
|
|
|
|
|
|
def _print_edited_summary(journal, old_stats, **kwargs):
|
|
stats = {
|
|
"added": len(journal) - old_stats["count"],
|
|
"deleted": old_stats["count"] - len(journal),
|
|
"modified": len([e for e in journal.entries if e.modified]),
|
|
}
|
|
|
|
prompts = []
|
|
|
|
if stats["added"] > 0:
|
|
prompts.append(f"{stats['added']} {_pluralize_entry(stats['added'])} added")
|
|
stats["modified"] -= stats["added"]
|
|
|
|
if stats["deleted"] > 0:
|
|
prompts.append(
|
|
f"{stats['deleted']} {_pluralize_entry(stats['deleted'])} deleted"
|
|
)
|
|
|
|
if stats["modified"]:
|
|
prompts.append(
|
|
f"{stats['modified']} {_pluralize_entry(stats['modified'])} modified"
|
|
)
|
|
|
|
if prompts:
|
|
print(f"[{', '.join(prompts).capitalize()}]", file=sys.stderr)
|
|
|
|
|
|
def _get_predit_stats(journal):
|
|
return {"count": len(journal)}
|
|
|
|
|
|
def _pluralize_entry(num):
|
|
return "entry" if num == 1 else "entries"
|
|
|
|
|
|
def _delete_search_results(journal, old_entries, **kwargs):
|
|
if not journal.entries:
|
|
raise JrnlException(Message(MsgText.NothingToDelete, MsgType.ERROR))
|
|
|
|
entries_to_delete = journal.prompt_delete_entries()
|
|
|
|
if entries_to_delete:
|
|
journal.entries = old_entries
|
|
journal.delete_entries(entries_to_delete)
|
|
|
|
journal.write()
|
|
|
|
|
|
def _display_search_results(args, journal, **kwargs):
|
|
if args.short or args.export == "short":
|
|
print(journal.pprint(short=True))
|
|
|
|
elif args.export == "pretty":
|
|
print(journal.pprint())
|
|
|
|
elif args.tags:
|
|
print(plugins.get_exporter("tags").export(journal))
|
|
|
|
elif args.export:
|
|
exporter = plugins.get_exporter(args.export)
|
|
print(exporter.export(journal, args.filename))
|
|
elif kwargs["config"].get("display_format"):
|
|
exporter = plugins.get_exporter(kwargs["config"]["display_format"])
|
|
print(exporter.export(journal, args.filename))
|
|
else:
|
|
print(journal.pprint())
|