mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
285 lines
12 KiB
Python
285 lines
12 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
"""
|
|
jrnl
|
|
|
|
license: MIT, see LICENSE for more details.
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
|
from __future__ import absolute_import
|
|
from . import Journal
|
|
from . import util
|
|
from . import install
|
|
from . import plugins
|
|
from .util import ERROR_COLOR, RESET_COLOR
|
|
import jrnl
|
|
import argparse
|
|
import sys
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
|
|
|
|
|
def parse_args(args=None):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
|
|
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
|
|
parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
|
|
|
|
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
|
|
composing.add_argument('text', metavar='', nargs="*")
|
|
|
|
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
|
|
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
|
|
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
|
|
reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date')
|
|
reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)')
|
|
reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries')
|
|
reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
|
|
|
|
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
|
|
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
|
|
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
|
|
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
|
|
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
|
|
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
|
|
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None)
|
|
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None)
|
|
exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None)
|
|
exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
|
|
|
|
return parser.parse_args(args)
|
|
|
|
|
|
def guess_mode(args, config):
|
|
"""Guesses the mode (compose, read or export) from the given arguments"""
|
|
compose = True
|
|
export = False
|
|
import_ = False
|
|
if args.import_ is not False:
|
|
compose = False
|
|
export = False
|
|
import_ = True
|
|
elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)):
|
|
compose = False
|
|
export = True
|
|
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
|
|
# Any sign of displaying stuff?
|
|
compose = False
|
|
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
|
|
# No date and only tags?
|
|
compose = False
|
|
|
|
return compose, export, import_
|
|
|
|
|
|
def encrypt(journal, filename=None):
|
|
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
|
from . import EncryptedJournal
|
|
|
|
journal.config['password'] = util.getpass("Enter new password: ")
|
|
journal.config['encrypt'] = True
|
|
|
|
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
|
|
new_journal.entries = journal.entries
|
|
new_journal.write(filename)
|
|
|
|
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
|
util.set_keychain(journal.name, journal.config['password'])
|
|
|
|
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal']))
|
|
|
|
|
|
def decrypt(journal, filename=None):
|
|
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
|
journal.config['encrypt'] = False
|
|
journal.config['password'] = ""
|
|
|
|
new_journal = Journal.PlainJournal(filename, **journal.config)
|
|
new_journal.entries = journal.entries
|
|
new_journal.write(filename)
|
|
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
|
|
|
|
|
def list_journals(config):
|
|
"""List the journals specified in the configuration file"""
|
|
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
|
|
ml = min(max(len(k) for k in config['journals']), 20)
|
|
for journal, cfg in config['journals'].items():
|
|
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
|
return result
|
|
|
|
|
|
def update_config(config, new_config, scope, force_local=False):
|
|
"""Updates a config dict with new values - either global if scope is None
|
|
or config['journals'][scope] is just a string pointing to a journal file,
|
|
or within the scope"""
|
|
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
|
config['journals'][scope].update(new_config)
|
|
elif scope and force_local: # Convert to dict
|
|
config['journals'][scope] = {"journal": config['journals'][scope]}
|
|
config['journals'][scope].update(new_config)
|
|
else:
|
|
config.update(new_config)
|
|
|
|
|
|
def configure_logger(debug=False):
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if debug else logging.INFO,
|
|
format='%(levelname)-8s %(name)-12s %(message)s'
|
|
)
|
|
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
|
|
|
|
|
|
def run(manual_args=None):
|
|
args = parse_args(manual_args)
|
|
configure_logger(args.debug)
|
|
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
|
|
if args.version:
|
|
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
|
|
print(util.py2encode(version_str))
|
|
sys.exit(0)
|
|
|
|
config = install.load_or_install_jrnl()
|
|
if args.ls:
|
|
util.prnt(list_journals(config))
|
|
sys.exit(0)
|
|
|
|
log.debug('Using configuration "%s"', config)
|
|
original_config = config.copy()
|
|
|
|
# If the first textual argument points to a journal file,
|
|
# use this!
|
|
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
|
|
|
if journal_name is not 'default':
|
|
args.text = args.text[1:]
|
|
elif "default" not in config['journals']:
|
|
util.prompt("No default journal configured.")
|
|
util.prompt(list_journals(config))
|
|
sys.exit(1)
|
|
|
|
config = util.scope_config(config, journal_name)
|
|
|
|
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter
|
|
if not args.limit and args.text and args.text[0].startswith("-"):
|
|
try:
|
|
args.limit = int(args.text[0].lstrip("-"))
|
|
args.text = args.text[1:]
|
|
except:
|
|
pass
|
|
|
|
log.debug('Using journal "%s"', journal_name)
|
|
mode_compose, mode_export, mode_import = guess_mode(args, config)
|
|
|
|
# How to quit writing?
|
|
if "win32" in sys.platform:
|
|
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
|
|
else:
|
|
_exit_multiline_code = "press Ctrl+D"
|
|
|
|
if mode_compose and not args.text:
|
|
if not sys.stdin.isatty():
|
|
# Piping data into jrnl
|
|
raw = util.py23_read()
|
|
elif config['editor']:
|
|
template = ""
|
|
if config['template']:
|
|
try:
|
|
template = open(config['template']).read()
|
|
except:
|
|
util.prompt("[Could not read template at '']".format(config['template']))
|
|
sys.exit(1)
|
|
raw = util.get_text_from_editor(config, template)
|
|
else:
|
|
try:
|
|
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
|
|
except KeyboardInterrupt:
|
|
util.prompt("[Entry NOT saved to journal.]")
|
|
sys.exit(0)
|
|
if raw:
|
|
args.text = [raw]
|
|
else:
|
|
mode_compose = False
|
|
|
|
# This is where we finally open the journal!
|
|
journal = Journal.open_journal(journal_name, config)
|
|
|
|
# Import mode
|
|
if mode_import:
|
|
plugins.get_importer(args.import_).import_(journal, args.input)
|
|
|
|
# Writing mode
|
|
elif mode_compose:
|
|
raw = " ".join(args.text).strip()
|
|
if util.PY2 and type(raw) is not unicode:
|
|
raw = raw.decode(sys.getfilesystemencoding())
|
|
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
|
journal.new_entry(raw)
|
|
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
|
journal.write()
|
|
|
|
if not mode_compose:
|
|
old_entries = journal.entries
|
|
if args.on_date:
|
|
args.start_date = args.end_date = args.on_date
|
|
journal.filter(tags=args.text,
|
|
start_date=args.start_date, end_date=args.end_date,
|
|
strict=args.strict,
|
|
short=args.short,
|
|
starred=args.starred)
|
|
journal.limit(args.limit)
|
|
|
|
# Reading mode
|
|
if not mode_compose and not mode_export and not mode_import:
|
|
print(util.py2encode(journal.pprint()))
|
|
|
|
# Various export modes
|
|
elif args.short:
|
|
print(util.py2encode(journal.pprint(short=True)))
|
|
|
|
elif args.tags:
|
|
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
|
|
|
elif args.export is not False:
|
|
exporter = plugins.get_exporter(args.export)
|
|
print(exporter.export(journal, args.output))
|
|
|
|
elif args.encrypt is not False:
|
|
encrypt(journal, filename=args.encrypt)
|
|
# Not encrypting to a separate file: update config!
|
|
if not args.encrypt:
|
|
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
|
|
install.save_config(original_config)
|
|
|
|
elif args.decrypt is not False:
|
|
decrypt(journal, filename=args.decrypt)
|
|
# Not decrypting to a separate file: update config!
|
|
if not args.decrypt:
|
|
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
|
install.save_config(original_config)
|
|
|
|
elif args.edit:
|
|
if not config['editor']:
|
|
util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR))
|
|
sys.exit(1)
|
|
other_entries = [e for e in old_entries if e not in journal.entries]
|
|
# Edit
|
|
old_num_entries = len(journal)
|
|
edited = util.get_text_from_editor(config, journal.editable_str())
|
|
journal.parse_editable_str(edited)
|
|
num_deleted = old_num_entries - len(journal)
|
|
num_edited = len([e for e in journal.entries if e.modified])
|
|
prompts = []
|
|
if num_deleted:
|
|
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
|
if num_edited:
|
|
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
|
if prompts:
|
|
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
|
journal.entries += other_entries
|
|
journal.sort()
|
|
journal.write()
|