Merge pull request #104 from maebert/starring

Starring entries
This commit is contained in:
Manuel Ebert 2013-11-04 10:26:07 -08:00
commit a7a4ed06cc
7 changed files with 100 additions and 41 deletions

View file

@ -1,6 +1,10 @@
Changelog
=========
#### 1.6.2
* [Improved] Starring entries now works for plain-text journals too!
#### 1.6.1
* [Improved] Attempts to fix broken config files automatically

20
features/starring.feature Normal file
View file

@ -0,0 +1,20 @@
Feature: Starring entries
Scenario: Starring an entry will mark it in the journal file
Given we use the config "basic.json"
When we run "jrnl 20 july 2013 *: Best day of my life!"
Then we should see the message "Entry added"
and the journal should contain "2013-07-20 09:00 Best day of my life! *"
Scenario: Filtering by starred entries
Given we use the config "basic.json"
When we run "jrnl -starred"
Then the output should be
"""
"""
When we run "jrnl 20 july 2013 *: Best day of my life!"
When we run "jrnl -starred"
Then the output should be
"""
2013-07-20 09:00 Best day of my life!
"""

View file

@ -113,6 +113,7 @@ def check_output_field_key(context, field, key):
def check_output(context):
text = context.text.strip().splitlines()
out = context.stdout_capture.getvalue().strip().splitlines()
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
for line_text, line_out in zip(text, out):
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
@ -149,7 +150,7 @@ def check_not_message(context, text):
@then('journal "{journal_name}" should contain "{text}"')
def check_journal_content(context, text, journal_name="default"):
journal = read_journal(journal_name)
assert text in journal
assert text in journal, journal
@then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"):

View file

@ -6,12 +6,13 @@ import textwrap
from datetime import datetime
class Entry:
def __init__(self, journal, date=None, title="", body=""):
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.tags = self.parse_tags()
self.starred = starred
def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower()
@ -23,6 +24,8 @@ class Entry:
"""Returns a string representation of the entry to be written into a journal file."""
date_str = self.date.strftime(self.journal.config['timeformat'])
title = date_str + " " + self.title
if self.starred:
title += " *"
body = self.body.strip()
return u"{title}{sep}{body}\n".format(
@ -31,10 +34,11 @@ class Entry:
body=body
)
def pprint(self):
"""Returns a pretty-printed version of the entry."""
def pprint(self, short=False):
"""Returns a pretty-printed version of the entry.
If short is true, only print the title."""
date_str = self.date.strftime(self.journal.config['timeformat'])
if self.journal.config['linewrap']:
if not short and self.journal.config['linewrap']:
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
body = "\n".join([
textwrap.fill(line+" ",
@ -51,6 +55,9 @@ class Entry:
# 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 u"{title}{sep}{body}\n".format(
title=title,
sep="\n" if has_body else "",
@ -65,7 +72,8 @@ class Entry:
'title': self.title.strip(),
'body': self.body.strip(),
'date': self.date.strftime("%Y-%m-%d"),
'time': self.date.strftime("%H:%M")
'time': self.date.strftime("%H:%M"),
'starred': self.starred
}
def to_md(self):

View file

@ -133,12 +133,20 @@ class Journal(object):
for line in journal.splitlines():
try:
# try to parse line as date => new entry begins
line = line.strip()
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
# parsing successfull => save old entry and create new one
if new_date and current_entry:
entries.append(current_entry)
current_entry = Entry.Entry(self, date=new_date, title=line[date_length+1:])
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
current_entry = Entry.Entry(self, date=new_date, title=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.
@ -153,9 +161,12 @@ class Journal(object):
return entries
def __unicode__(self):
return self.pprint()
def pprint(self, short=False):
"""Prettyprints the journal's entries"""
sep = "\n"
pp = sep.join([e.pprint() for e in self.entries])
pp = sep.join([e.pprint(short=short) for e in self.entries])
if self.config['highlight']: # highlight tags
if self.search_tags:
for tag in self.search_tags:
@ -169,9 +180,6 @@ class Journal(object):
pp)
return pp
def pprint(self):
return self.__unicode__()
def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries))
@ -196,7 +204,7 @@ class Journal(object):
if n:
self.entries = self.entries[-n:]
def filter(self, tags=[], start_date=None, end_date=None, strict=False, short=False):
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False):
"""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
@ -204,6 +212,8 @@ class Journal(object):
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
entry is kept if any tag is present."""
self.search_tags = set([tag.lower() for tag in tags])
@ -214,6 +224,7 @@ class Journal(object):
result = [
entry for entry in self.entries
if (not tags or tagged(entry.tags))
and (not starred or entry.starred)
and (not start_date or entry.date > start_date)
and (not end_date or entry.date < end_date)
]
@ -233,16 +244,22 @@ class Journal(object):
e.body = ''
self.entries = result
def parse_date(self, date):
def parse_date(self, date_str):
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
if not date:
if not date_str:
return None
elif type(date) is datetime:
return date
elif isinstance(date_str, datetime):
return date_str
date, flag = self.dateparse.parse(date)
date, flag = self.dateparse.parse(date_str)
if not flag: # Oops, unparsable.
try: # Try and parse this as a single year
year = int(date_str)
return datetime(year, 1, 1)
except ValueError:
return None
except TypeError:
return None
if flag is 1: # Date found, but no time. Use the default time.
@ -264,7 +281,7 @@ class Journal(object):
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')
starred = False
# Split raw text into title and body
title_end = len(raw)
for separator in ["\n", ". ", "? ", "! "]:
@ -273,15 +290,22 @@ class Journal(object):
title_end = sep_pos
title = raw[:title_end+1]
body = raw[title_end+1:].strip()
starred = False
if not date:
if title.find(":") > 0:
starred = "*" in title[:title.find(":")]
date = self.parse_date(title[:title.find(":")])
if date: # Parsed successfully, strip that from the raw text
if date or starred: # Parsed successfully, strip that from the raw text
title = title[title.find(":")+1:].strip()
elif title.strip().startswith("*"):
starred = True
title = title[1:].strip()
elif title.strip().endswith("*"):
starred = True
title = title[:-1].strip()
if not date: # Still nothing? Meh, just live in the moment.
date = self.parse_date("now")
entry = Entry.Entry(self, date, title, body)
entry = Entry.Entry(self, date, title, body, starred=starred)
self.entries.append(entry)
if sort:
self.sort()

View file

@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line.
"""
__title__ = 'jrnl'
__version__ = '1.6.1-dev'
__version__ = '1.6.2-dev'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 Manuel Ebert'

View file

@ -30,22 +30,21 @@ PYCRYPTO = install.module_exists("Crypto")
def parse_args(args=None):
parser = argparse.ArgumentParser()
composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments')
composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"')
composing.add_argument('-star', dest='star', help='Stars an entry (DayOne journals only)', action="store_true")
composing.add_argument('text', metavar='text', nargs="*", help='Log entry (or tags by which to filter in viewing 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('-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', nargs="?", type=int)
reading.add_argument('-short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
exporting.add_argument('--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', help='Export your journal to Markdown, JSON or Text', nargs='?', default=False, const=None)
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', nargs='?', default=False, const=None)
exporting.add_argument('--export', metavar='TYPE', dest='export', help='Export your journal to Markdown, JSON or Text', default=False, const=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")
@ -56,13 +55,13 @@ 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 args.tags or 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.delete_last)):
compose = False
export = True
elif args.start_date or args.end_date or args.limit or args.strict or args.short:
elif any((args.start_date, args.end_date, args.limit, args.strict, args.starred)):
# Any sign of displaying stuff?
compose = False
elif not args.date and args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
# No date and only tags?
compose = False
@ -172,15 +171,15 @@ def cli(manual_args=None):
raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(raw, args.date)
entry.starred = args.star
entry = journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write()
else:
journal.filter(tags=args.text,
start_date=args.start_date, end_date=args.end_date,
strict=args.strict,
short=args.short)
short=args.short,
starred=args.starred)
journal.limit(args.limit)
# Reading mode
@ -188,6 +187,9 @@ def cli(manual_args=None):
print(journal.pprint())
# Various export modes
elif args.short:
print(journal.pprint(short=True))
elif args.tags:
print(exporters.to_tag_list(journal))