mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 08:38:32 +02:00
commit
a7a4ed06cc
7 changed files with 100 additions and 41 deletions
|
@ -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
20
features/starring.feature
Normal 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!
|
||||
"""
|
|
@ -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"):
|
||||
|
|
|
@ -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 "<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(),
|
||||
'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):
|
||||
|
|
|
@ -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,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()
|
||||
|
|
|
@ -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'
|
||||
|
|
28
jrnl/jrnl.py
28
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))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue