diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ceb6ab4..143dc87b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/features/starring.feature b/features/starring.feature new file mode 100644 index 00000000..0f984896 --- /dev/null +++ b/features/starring.feature @@ -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! + """ diff --git a/features/steps/core.py b/features/steps/core.py index c66a921f..f97d7ae1 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -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"): diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 32c76b12..4948cc1d 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -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,11 +55,14 @@ 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) - return u"{title}{sep}{body}\n".format( - title=title, - sep="\n" if has_body else "", - body=body if has_body else "", - ) + if short: + return title + 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): return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) @@ -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): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index ecc9a4d7..71f1e397 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -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 "".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,17 +244,23 @@ 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. - 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. 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.""" 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() diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 0e06627b..409b3969 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -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' diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 7b98a42e..901507c2 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -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))