mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-07-08 01:06:12 +02:00
Updated docs from master
This commit is contained in:
parent
82f9a713dc
commit
7d577baf66
54 changed files with 451 additions and 156 deletions
|
@ -9,10 +9,11 @@ class Entry:
|
|||
def __init__(self, journal, date=None, title="", body="", starred=False):
|
||||
self.journal = journal # Reference to journal mainly to access it's config
|
||||
self.date = date or datetime.now()
|
||||
self.title = title.strip()
|
||||
self.body = body.strip()
|
||||
self.title = title.strip("\n ")
|
||||
self.body = body.strip("\n ")
|
||||
self.tags = self.parse_tags()
|
||||
self.starred = starred
|
||||
self.modified = False
|
||||
|
||||
def parse_tags(self):
|
||||
fulltext = " ".join([self.title, self.body]).lower()
|
||||
|
@ -67,6 +68,18 @@ class Entry:
|
|||
def __repr__(self):
|
||||
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Entry) \
|
||||
or self.title.strip() != other.title.strip() \
|
||||
or self.body.strip() != other.body.strip() \
|
||||
or self.date != other.date \
|
||||
or self.starred != other.starred:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'title': self.title.strip(),
|
||||
|
|
BIN
jrnl/Entry.pyc
BIN
jrnl/Entry.pyc
Binary file not shown.
160
jrnl/Journal.py
160
jrnl/Journal.py
|
@ -11,6 +11,7 @@ try: import parsedatetime.parsedatetime_consts as pdt
|
|||
except ImportError: import parsedatetime.parsedatetime as pdt
|
||||
import re
|
||||
from datetime import datetime
|
||||
import dateutil
|
||||
import time
|
||||
import sys
|
||||
try:
|
||||
|
@ -50,9 +51,11 @@ class Journal(object):
|
|||
self.search_tags = None # Store tags we're highlighting
|
||||
self.name = name
|
||||
|
||||
journal_txt = self.open()
|
||||
self.entries = self.parse(journal_txt)
|
||||
self.sort()
|
||||
self.open()
|
||||
|
||||
def __len__(self):
|
||||
"""Returns the number of entries"""
|
||||
return len(self.entries)
|
||||
|
||||
def _colorize(self, string):
|
||||
if colorama:
|
||||
|
@ -115,9 +118,10 @@ class Journal(object):
|
|||
else:
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
journal = f.read()
|
||||
return journal
|
||||
self.entries = self._parse(journal)
|
||||
self.sort()
|
||||
|
||||
def parse(self, journal):
|
||||
def _parse(self, journal_txt):
|
||||
"""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
|
||||
|
@ -128,7 +132,7 @@ class Journal(object):
|
|||
entries = []
|
||||
current_entry = None
|
||||
|
||||
for line in journal.splitlines():
|
||||
for line in journal_txt.splitlines():
|
||||
try:
|
||||
# try to parse line as date => new entry begins
|
||||
line = line.strip()
|
||||
|
@ -184,7 +188,7 @@ class Journal(object):
|
|||
def write(self, filename=None):
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
filename = filename or self.config['journal']
|
||||
journal = "\n".join([e.__unicode__() for e in self.entries])
|
||||
journal = u"\n".join([e.__unicode__() for e in self.entries])
|
||||
if self.config['encrypt']:
|
||||
journal = self._encrypt(journal)
|
||||
with open(filename, 'wb') as journal_file:
|
||||
|
@ -249,7 +253,12 @@ class Journal(object):
|
|||
elif isinstance(date_str, datetime):
|
||||
return date_str
|
||||
|
||||
date, flag = self.dateparse.parse(date_str)
|
||||
try:
|
||||
date = dateutil.parser.parse(date_str)
|
||||
flag = 1 if date.hour == 0 and date.minute == 0 else 2
|
||||
date = date.timetuple()
|
||||
except:
|
||||
date, flag = self.dateparse.parse(date_str)
|
||||
|
||||
if not flag: # Oops, unparsable.
|
||||
try: # Try and parse this as a single year
|
||||
|
@ -281,20 +290,15 @@ class Journal(object):
|
|||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||
starred = False
|
||||
# Split raw text into title and body
|
||||
title_end = len(raw)
|
||||
for separator in ["\n", ". ", "? ", "! "]:
|
||||
sep_pos = raw.find(separator)
|
||||
if 1 < sep_pos < title_end:
|
||||
title_end = sep_pos
|
||||
title = raw[:title_end+1]
|
||||
body = raw[title_end+1:].strip()
|
||||
sep = re.search("[\n!?.]+", raw)
|
||||
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
|
||||
starred = False
|
||||
if not date:
|
||||
if title.find(":") > 0:
|
||||
starred = "*" in title[:title.find(":")]
|
||||
date = self.parse_date(title[:title.find(":")])
|
||||
if title.find(": ") > 0:
|
||||
starred = "*" in title[:title.find(": ")]
|
||||
date = self.parse_date(title[:title.find(": ")])
|
||||
if date or starred: # Parsed successfully, strip that from the raw text
|
||||
title = title[title.find(":")+1:].strip()
|
||||
title = title[title.find(": ")+1:].strip()
|
||||
elif title.strip().startswith("*"):
|
||||
starred = True
|
||||
title = title[1:].strip()
|
||||
|
@ -304,25 +308,36 @@ class Journal(object):
|
|||
if not date: # Still nothing? Meh, just live in the moment.
|
||||
date = self.parse_date("now")
|
||||
entry = Entry.Entry(self, date, title, body, starred=starred)
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
if sort:
|
||||
self.sort()
|
||||
return entry
|
||||
|
||||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
manually and later be parsed with eslf.parse_editable_str."""
|
||||
return u"\n".join([e.__unicode__() for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""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.entries = mod_entries
|
||||
|
||||
class DayOne(Journal):
|
||||
"""A special Journal handling DayOne files"""
|
||||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
self._deleted_entries = []
|
||||
super(DayOne, self).__init__(**kwargs)
|
||||
|
||||
def open(self):
|
||||
files = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
||||
return files
|
||||
|
||||
def parse(self, filenames):
|
||||
"""Instead of parsing a string into an entry, this method will take a list
|
||||
of filenames, interpret each as a plist file and create a new entry from that."""
|
||||
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
||||
self.entries = []
|
||||
for filename in filenames:
|
||||
with open(filename, 'rb') as plist_entry:
|
||||
|
@ -333,34 +348,97 @@ class DayOne(Journal):
|
|||
timezone = pytz.timezone(util.get_local_timezone())
|
||||
date = dict_entry['Creation Date']
|
||||
date = date + timezone.utcoffset(date)
|
||||
entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False)
|
||||
entry.starred = dict_entry["Starred"]
|
||||
raw = dict_entry['Entry Text']
|
||||
sep = re.search("[\n!?.]+", raw)
|
||||
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
|
||||
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
|
||||
entry.uuid = dict_entry["UUID"]
|
||||
entry.tags = dict_entry.get("Tags", [])
|
||||
# We're using new_entry to create the Entry object, which adds the entry
|
||||
# to self.entries already. However, in the original Journal.__init__, this
|
||||
# method is expected to return a list of newly created entries, which is why
|
||||
# we're returning the obvious.
|
||||
return self.entries
|
||||
self.entries.append(entry)
|
||||
self.sort()
|
||||
|
||||
def write(self):
|
||||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
# Assumption: since jrnl can not manipulate existing entries, all entries
|
||||
# that have a uuid will be old ones, and only the one that doesn't will
|
||||
# have a new one!
|
||||
if not hasattr(entry, "uuid"):
|
||||
if entry.modified:
|
||||
if not hasattr(entry, "uuid"):
|
||||
entry.uuid = uuid.uuid1().hex
|
||||
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
||||
new_uuid = uuid.uuid1().hex
|
||||
filename = os.path.join(self.config['journal'], "entries", new_uuid+".doentry")
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid+".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': util.get_local_timezone(),
|
||||
'UUID': new_uuid,
|
||||
'UUID': entry.uuid,
|
||||
'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags]
|
||||
}
|
||||
# print entry_plist
|
||||
|
||||
plistlib.writePlist(entry_plist, filename)
|
||||
for entry in self._deleted_entries:
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid+".doentry")
|
||||
os.remove(filename)
|
||||
|
||||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
manually and later be parsed with eslf.parse_editable_str."""
|
||||
return u"\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates it's 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.
|
||||
date_length = len(datetime.today().strftime(self.config['timeformat']))
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
current_entry = None
|
||||
|
||||
for line in edited.splitlines():
|
||||
# try to parse line as UUID => new entry begins
|
||||
line = line.strip()
|
||||
m = re.match("# *([a-f0-9]+) *$", line.lower())
|
||||
if m:
|
||||
if current_entry:
|
||||
entries.append(current_entry)
|
||||
current_entry = Entry.Entry(self)
|
||||
current_entry.modified = False
|
||||
current_entry.uuid = m.group(1).lower()
|
||||
else:
|
||||
try:
|
||||
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
||||
if line.endswith("*"):
|
||||
current_entry.starred = True
|
||||
line = line[:-1]
|
||||
current_entry.title = line[date_length+1:]
|
||||
current_entry.date = new_date
|
||||
except ValueError:
|
||||
if current_entry:
|
||||
current_entry.body += line + "\n"
|
||||
|
||||
# Append last entry
|
||||
if current_entry:
|
||||
entries.append(current_entry)
|
||||
|
||||
# Now, update our current entries if they changed
|
||||
for entry in entries:
|
||||
entry.parse_tags()
|
||||
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
|
||||
if matched_entries:
|
||||
# This entry is an existing entry
|
||||
match = matched_entries[0]
|
||||
if match != entry:
|
||||
self.entries.remove(match)
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
else:
|
||||
# This entry seems to be new... save it.
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
# Remove deleted entries
|
||||
edited_uuids = [e.uuid for e in entries]
|
||||
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]
|
||||
return entries
|
||||
|
||||
|
|
BIN
jrnl/Journal.pyc
BIN
jrnl/Journal.pyc
Binary file not shown.
|
@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line.
|
|||
"""
|
||||
|
||||
__title__ = 'jrnl'
|
||||
__version__ = '1.6.6'
|
||||
__version__ = '1.7.2'
|
||||
__author__ = 'Manuel Ebert'
|
||||
__license__ = 'MIT License'
|
||||
__copyright__ = 'Copyright 2013 Manuel Ebert'
|
||||
|
|
Binary file not shown.
44
jrnl/cli.py
44
jrnl/cli.py
|
@ -20,8 +20,6 @@ except (SystemError, ValueError):
|
|||
import install
|
||||
import jrnl
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
@ -51,7 +49,7 @@ def parse_args(args=None):
|
|||
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', 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('--delete-last', dest='delete_last', help='Deletes the last entry from your journal file.', action="store_true")
|
||||
exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
|
||||
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
@ -59,7 +57,7 @@ def guess_mode(args, config):
|
|||
"""Guesses the mode (compose, read or export) from the given arguments"""
|
||||
compose = True
|
||||
export = False
|
||||
if args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.delete_last)):
|
||||
if 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.limit, args.strict, args.starred)):
|
||||
|
@ -71,20 +69,6 @@ def guess_mode(args, config):
|
|||
|
||||
return compose, export
|
||||
|
||||
def get_text_from_editor(config):
|
||||
tmpfile = os.path.join(tempfile.gettempdir(), "jrnl")
|
||||
subprocess.call(config['editor'].split() + [tmpfile])
|
||||
if os.path.exists(tmpfile):
|
||||
with open(tmpfile) as f:
|
||||
raw = f.read()
|
||||
os.remove(tmpfile)
|
||||
else:
|
||||
util.prompt('[Nothing saved to file]')
|
||||
raw = ''
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def encrypt(journal, filename=None):
|
||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
password = util.getpass("Enter new password: ")
|
||||
|
@ -164,11 +148,10 @@ def run(manual_args=None):
|
|||
else:
|
||||
journal = Journal.Journal(journal_name, **config)
|
||||
|
||||
# How to quit writing?
|
||||
if "win32" in sys.platform:
|
||||
# for Windows systems
|
||||
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
|
||||
else:
|
||||
# for *nix systems (and others?)
|
||||
_exit_multiline_code = "press Ctrl+D"
|
||||
|
||||
if mode_compose and not args.text:
|
||||
|
@ -176,7 +159,7 @@ def run(manual_args=None):
|
|||
# Piping data into jrnl
|
||||
raw = util.py23_read()
|
||||
elif config['editor']:
|
||||
raw = get_text_from_editor(config)
|
||||
raw = util.get_text_from_editor(config)
|
||||
else:
|
||||
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
|
||||
if raw:
|
||||
|
@ -193,6 +176,7 @@ def run(manual_args=None):
|
|||
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
||||
journal.write()
|
||||
else:
|
||||
old_entries = journal.entries
|
||||
journal.filter(tags=args.text,
|
||||
start_date=args.start_date, end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
|
@ -231,10 +215,20 @@ def run(manual_args=None):
|
|||
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
||||
install.save_config(original_config, config_path=CONFIG_PATH)
|
||||
|
||||
elif args.delete_last:
|
||||
last_entry = journal.entries.pop()
|
||||
util.prompt("[Deleted Entry:]")
|
||||
print(last_entry.pprint())
|
||||
elif args.edit:
|
||||
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} entries deleted".format(num_deleted))
|
||||
if num_edited: prompts.append("{0} entries modified".format(num_edited))
|
||||
if prompts:
|
||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
||||
journal.entries += other_entries
|
||||
journal.write()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
BIN
jrnl/cli.pyc
BIN
jrnl/cli.pyc
Binary file not shown.
Binary file not shown.
BIN
jrnl/install.pyc
BIN
jrnl/install.pyc
Binary file not shown.
BIN
jrnl/jrnl.pyc
BIN
jrnl/jrnl.pyc
Binary file not shown.
19
jrnl/util.py
19
jrnl/util.py
|
@ -9,6 +9,9 @@ import pytz
|
|||
try: import simplejson as json
|
||||
except ImportError: import json
|
||||
import re
|
||||
import tempfile
|
||||
import subprocess
|
||||
import codecs
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
@ -121,3 +124,19 @@ def load_and_fix_json(json_path):
|
|||
prompt("[Entry was NOT added to your journal]")
|
||||
sys.exit(1)
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
tmpfile = os.path.join(tempfile.gettempdir(), "jrnl")
|
||||
if template:
|
||||
with codecs.open(tmpfile, 'w', "utf-8") as f:
|
||||
f.write(template)
|
||||
subprocess.call(config['editor'].split() + [tmpfile])
|
||||
if os.path.exists(tmpfile):
|
||||
with codecs.open(tmpfile, "r", "utf-8") as f:
|
||||
raw = f.read()
|
||||
os.remove(tmpfile)
|
||||
else:
|
||||
prompt('[Nothing saved to file]')
|
||||
raw = ''
|
||||
|
||||
return raw
|
||||
|
||||
|
|
BIN
jrnl/util.pyc
BIN
jrnl/util.pyc
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue