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 Changelog
========= =========
#### 1.6.2
* [Improved] Starring entries now works for plain-text journals too!
#### 1.6.1 #### 1.6.1
* [Improved] Attempts to fix broken config files automatically * [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): def check_output(context):
text = context.text.strip().splitlines() text = context.text.strip().splitlines()
out = context.stdout_capture.getvalue().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): for line_text, line_out in zip(text, out):
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()] 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}"') @then('journal "{journal_name}" should contain "{text}"')
def check_journal_content(context, text, journal_name="default"): def check_journal_content(context, text, journal_name="default"):
journal = read_journal(journal_name) journal = read_journal(journal_name)
assert text in journal assert text in journal, journal
@then('journal "{journal_name}" should not exist') @then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"): def journal_doesnt_exist(context, journal_name="default"):

View file

@ -6,12 +6,13 @@ import textwrap
from datetime import datetime from datetime import datetime
class Entry: 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.journal = journal # Reference to journal mainly to access it's config
self.date = date or datetime.now() self.date = date or datetime.now()
self.title = title.strip() self.title = title.strip()
self.body = body.strip() self.body = body.strip()
self.tags = self.parse_tags() self.tags = self.parse_tags()
self.starred = starred
def parse_tags(self): def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower() 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.""" """Returns a string representation of the entry to be written into a journal file."""
date_str = self.date.strftime(self.journal.config['timeformat']) date_str = self.date.strftime(self.journal.config['timeformat'])
title = date_str + " " + self.title title = date_str + " " + self.title
if self.starred:
title += " *"
body = self.body.strip() body = self.body.strip()
return u"{title}{sep}{body}\n".format( return u"{title}{sep}{body}\n".format(
@ -31,10 +34,11 @@ class Entry:
body=body body=body
) )
def pprint(self): def pprint(self, short=False):
"""Returns a pretty-printed version of the entry.""" """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']) 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']) title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
body = "\n".join([ body = "\n".join([
textwrap.fill(line+" ", textwrap.fill(line+" ",
@ -51,11 +55,14 @@ class Entry:
# Suppress bodies that are just blanks and new lines. # 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) has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
return u"{title}{sep}{body}\n".format( if short:
title=title, return title
sep="\n" if has_body else "", else:
body=body if has_body else "", return u"{title}{sep}{body}\n".format(
) title=title,
sep="\n" if has_body else "",
body=body if has_body else "",
)
def __repr__(self): def __repr__(self):
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
@ -65,7 +72,8 @@ class Entry:
'title': self.title.strip(), 'title': self.title.strip(),
'body': self.body.strip(), 'body': self.body.strip(),
'date': self.date.strftime("%Y-%m-%d"), '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): def to_md(self):

View file

@ -133,12 +133,20 @@ class Journal(object):
for line in journal.splitlines(): for line in journal.splitlines():
try: try:
# try to parse line as date => new entry begins # try to parse line as date => new entry begins
line = line.strip()
new_date = datetime.strptime(line[:date_length], self.config['timeformat']) new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
# parsing successfull => save old entry and create new one # parsing successfull => save old entry and create new one
if new_date and current_entry: if new_date and current_entry:
entries.append(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: except ValueError:
# Happens when we can't parse the start of the line as an date. # Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body. # In this case, just append line to our body.
@ -153,9 +161,12 @@ class Journal(object):
return entries return entries
def __unicode__(self): def __unicode__(self):
return self.pprint()
def pprint(self, short=False):
"""Prettyprints the journal's entries""" """Prettyprints the journal's entries"""
sep = "\n" 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.config['highlight']: # highlight tags
if self.search_tags: if self.search_tags:
for tag in self.search_tags: for tag in self.search_tags:
@ -169,9 +180,6 @@ class Journal(object):
pp) pp)
return pp return pp
def pprint(self):
return self.__unicode__()
def __repr__(self): def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries)) return "<Journal with {0} entries>".format(len(self.entries))
@ -196,7 +204,7 @@ class Journal(object):
if n: if n:
self.entries = self.entries[-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. """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 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. 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 If strict is True, all tags must be present in an entry. If false, the
entry is kept if any tag is present.""" entry is kept if any tag is present."""
self.search_tags = set([tag.lower() for tag in tags]) self.search_tags = set([tag.lower() for tag in tags])
@ -214,6 +224,7 @@ class Journal(object):
result = [ result = [
entry for entry in self.entries entry for entry in self.entries
if (not tags or tagged(entry.tags)) if (not tags or tagged(entry.tags))
and (not starred or entry.starred)
and (not start_date or entry.date > start_date) and (not start_date or entry.date > start_date)
and (not end_date or entry.date < end_date) and (not end_date or entry.date < end_date)
] ]
@ -233,17 +244,23 @@ class Journal(object):
e.body = '' e.body = ''
self.entries = result 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""" """Parses a string containing a fuzzy date and returns a datetime.datetime object"""
if not date: if not date_str:
return None return None
elif type(date) is datetime: elif isinstance(date_str, datetime):
return date return date_str
date, flag = self.dateparse.parse(date) date, flag = self.dateparse.parse(date_str)
if not flag: # Oops, unparsable. if not flag: # Oops, unparsable.
return None 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. if flag is 1: # Date found, but no time. Use the default time.
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute']) date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
@ -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.""" 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') raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
starred = False
# Split raw text into title and body # Split raw text into title and body
title_end = len(raw) title_end = len(raw)
for separator in ["\n", ". ", "? ", "! "]: for separator in ["\n", ". ", "? ", "! "]:
@ -273,15 +290,22 @@ class Journal(object):
title_end = sep_pos title_end = sep_pos
title = raw[:title_end+1] title = raw[:title_end+1]
body = raw[title_end+1:].strip() body = raw[title_end+1:].strip()
starred = False
if not date: if not date:
if title.find(":") > 0: if title.find(":") > 0:
starred = "*" in title[:title.find(":")]
date = self.parse_date(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() 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. if not date: # Still nothing? Meh, just live in the moment.
date = self.parse_date("now") date = self.parse_date("now")
entry = Entry.Entry(self, date, title, body, starred=starred)
entry = Entry.Entry(self, date, title, body)
self.entries.append(entry) self.entries.append(entry)
if sort: if sort:
self.sort() self.sort()

View file

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

View file

@ -30,22 +30,21 @@ PYCRYPTO = install.module_exists("Crypto")
def parse_args(args=None): def parse_args(args=None):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') 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('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') composing.add_argument('text', metavar='', nargs="*")
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)')
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') 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('-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('-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('-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('-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 = 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('--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('--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', 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', 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('--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('--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('--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""" """Guesses the mode (compose, read or export) from the given arguments"""
compose = True compose = True
export = False 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 compose = False
export = True 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? # Any sign of displaying stuff?
compose = False 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? # No date and only tags?
compose = False compose = False
@ -172,15 +171,15 @@ def cli(manual_args=None):
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode: if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding()) raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(raw, args.date) entry = journal.new_entry(raw)
entry.starred = args.star
util.prompt("[Entry added to {0} journal]".format(journal_name)) util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write() journal.write()
else: else:
journal.filter(tags=args.text, journal.filter(tags=args.text,
start_date=args.start_date, end_date=args.end_date, start_date=args.start_date, end_date=args.end_date,
strict=args.strict, strict=args.strict,
short=args.short) short=args.short,
starred=args.starred)
journal.limit(args.limit) journal.limit(args.limit)
# Reading mode # Reading mode
@ -188,6 +187,9 @@ def cli(manual_args=None):
print(journal.pprint()) print(journal.pprint())
# Various export modes # Various export modes
elif args.short:
print(journal.pprint(short=True))
elif args.tags: elif args.tags:
print(exporters.to_tag_list(journal)) print(exporters.to_tag_list(journal))