Merge branch 'master' into 2.0-dayone-fixes

This commit is contained in:
MinchinWeb 2019-10-01 19:16:32 -06:00 committed by GitHub
commit 3dea7d936f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 262 additions and 92 deletions

2
.github/stale.yml vendored
View file

@ -4,7 +4,7 @@ daysUntilStale: 60
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- pinned - ':star:'
- security - security
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale

View file

@ -7,7 +7,7 @@ If you use jrnl, you can totally make our day by just saying "thanks for the cod
Docs & Typos Docs & Typos
------------ ------------
If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.rst` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, and then open `docs/_build/html/index.html` in your browser. Note that this requires [lessc](http://lesscss.org/) and [Sphinx](https://pypi.python.org/pypi/Sphinx) to be installed. Changes to the CSS or Javascript should be made on `docs/_themes/jrnl/`. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that. If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.md` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, which will open a browser that hot-reloads as you change the docs. This requires [mkdocs](https://www.mkdocs.org) to be installed. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that.
Bugs Bugs
---- ----

View file

@ -9,7 +9,7 @@ Optionally, your journal can be encrypted using the [256-bit AES](http://en.wiki
### Why keep a journal? ### Why keep a journal?
Journals aren't just for angsty teenagers and people who have too much time on their summer vacation. A journal helps you to keep track of the things you get done and how you did them. Your imagination may be limitless, but your memory isn't. For personal use, make it a good habit to write at least 20 words a day. Just to reflect what made this day special, why you haven't wasted it. For professional use, consider a text-based journal to be the perfect complement to your GTD todo list - a documentation of what and how you've done it. Journals aren't just for people who have too much time on their summer vacation. A journal helps you to keep track of the things you get done and how you did them. Your imagination may be limitless, but your memory isn't. For personal use, make it a good habit to write at least 20 words a day. Just to reflect what made this day special, why you haven't wasted it. For professional use, consider a text-based journal to be the perfect complement to your GTD todo list - a documentation of what and how you've done it.
In a Nutshell In a Nutshell
------------- -------------

View file

@ -135,13 +135,11 @@ that journal.
this option will most likely result in your journal file being this option will most likely result in your journal file being
impossible to load. impossible to load.
### Known Issues ## Known Issues
- The Windows shell prior to Windows 7 has issues with unicode ### Unicode on Windows
encoding. If you want to use non-ascii characters, change the
codepage with `chcp 1252` before using The Windows shell prior to Windows 7 has issues with unicode encoding.
`jrnl` (Thanks to Yves Pouplard for To use non-ascii characters, first tweak Python to recognize the encoding by adding `'cp65001': 'utf_8'`, to `Lib/encoding/aliases.py`. Then, change the codepage with `chcp 1252` before using `jrnl`.
solving this!)
- `jrnl`relies on the PyCrypto (Related issue: [#486](https://github.com/jrnl-org/jrnl/issues/486))
package to encrypt journals, which has some known problems with
installing on Windows and within virtual environments.

View file

@ -17,7 +17,7 @@ AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
## Why keep a journal? ## Why keep a journal?
Journals aren't just for angsty teenagers and people who have too much Journals aren't just for people who have too much
time on their summer vacation. A journal helps you to keep track of the time on their summer vacation. A journal helps you to keep track of the
things you get done and how you did them. Your imagination may be things you get done and how you did them. Your imagination may be
limitless, but your memory isn't. limitless, but your memory isn't.

View file

@ -15,9 +15,8 @@ And will get something like `@melo: 9`, meaning there are 9 entries
where both `@alberto` and `@melo` are tagged. How does this work? First, where both `@alberto` and `@melo` are tagged. How does this work? First,
`jrnl @alberto` will filter the journal to only entries containing the `jrnl @alberto` will filter the journal to only entries containing the
tag `@alberto`, and then the `--tags` option will print out how often tag `@alberto`, and then the `--tags` option will print out how often
each tag occurred in this <span class="title-ref">filtered</span> each tag occurred in this filtered journal. Finally, we pipe this to
journal. Finally, we pipe this to `grep` which will only display the `grep` which will only display the line containing `@melo`.
line containing `@melo`.
### Combining filters ### Combining filters
@ -66,17 +65,19 @@ If you do that often, consider creating a function in your `.bashrc` or
``` sh ``` sh
jrnlimport () { jrnlimport () {
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
} }
``` ```
### Using templates ### Using templates
Say you always want to use the same template for creating new entries. Say you always want to use the same template for creating new entries.
If you have an `external editor <advanced>` set up, you can use this : If you have an [external editor](../advanced) set up, you can use this:
jrnl < my_template.txt ```sh
$ jrnl -1 --edit jrnl < my_template.txt
jrnl -1 --edit
```
Another nice solution that allows you to define individual prompts comes Another nice solution that allows you to define individual prompts comes
from [Jacobo de from [Jacobo de
@ -105,8 +106,10 @@ close the file to save the changes to jrnl.
To use Sublime Text, install the command line tools for Sublime Text and To use Sublime Text, install the command line tools for Sublime Text and
configure your `.jrnl_config` like this: configure your `.jrnl_config` like this:
``` javascript ``` json
"editor": "subl -w" {
"editor": "subl -w"
}
``` ```
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
@ -118,8 +121,10 @@ Similar to Sublime Text, MacVim must be started with a flag that tells
the the process to wait until the file is closed before passing control the the process to wait until the file is closed before passing control
back to journal. In the case of MacVim, this is `-f`: back to journal. In the case of MacVim, this is `-f`:
``` javascript ``` json
"editor": "mvim -f" {
"editor": "mvim -f"
}
``` ```
### iA Writer ### iA Writer
@ -128,8 +133,10 @@ On OS X, you can use the fabulous [iA
Writer](http://www.iawriter.com/mac) to write entries. Configure your Writer](http://www.iawriter.com/mac) to write entries. Configure your
`.jrnl_config` like this: `.jrnl_config` like this:
``` javascript ``` json
"editor": "open -b pro.writer.mac -Wn" {
"editor": "open -b pro.writer.mac -Wn"
}
``` ```
What does this do? `open -b ...` opens a file using the application What does this do? `open -b ...` opens a file using the application
@ -142,9 +149,7 @@ you can find the right string to use by inspecting iA Writer's
`Info.plist` file in your shell: `Info.plist` file in your shell:
``` sh ``` sh
$ grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
<key>CFBundleIdentifier</key>
<string>pro.writer.mac</string>
``` ```
### Notepad++ on Windows ### Notepad++ on Windows
@ -152,8 +157,10 @@ $ grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
the jrnl config file (`.jrnl_config`) like this: the jrnl config file (`.jrnl_config`) like this:
``` javascript ``` json
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession", {
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
}
``` ```
The double backslashes are needed so jrnl can read the file path The double backslashes are needed so jrnl can read the file path
@ -164,9 +171,9 @@ its own Notepad++ window.
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this: To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this:
```javascript ```json
{ {
"editor": "/usr/bin/code --wait", "editor": "/usr/bin/code --wait",
} }
``` ```
@ -183,7 +190,7 @@ Then you can add:
```javascript ```javascript
{ {
"editor": "code --wait", "editor": "code --wait",
} }
``` ```

View file

@ -37,7 +37,8 @@ jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
!!! note !!! note
Most shell contains a certain number of reserved characters, such as `#` Most shell contains a certain number of reserved characters, such as `#`
and `*`. Unbalanced quotes, parenthesis, and so on will also get into and `*`. Unbalanced quotes, parenthesis, and so on will also get into
the way of your editing. For writing longer entries, just enter `jrnl` the way of your editing.
For writing longer entries, just enter `jrnl`
and hit `return`. Only then enter the text of your journal entry. and hit `return`. Only then enter the text of your journal entry.
Alternatively, `use an external editor <advanced>`). Alternatively, `use an external editor <advanced>`).
@ -75,9 +76,9 @@ The following options are equivalent:
- `jrnl Best day of my life.*` - `jrnl Best day of my life.*`
!!! note !!! note
Just make sure that the asterisk sign is **not** surrounded by Just make sure that the asterisk sign is **not** surrounded by
whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the
reason being that the `*` sign has a special meaning on most shells). reason being that the `*` sign has a special meaning on most shells).
## Viewing ## Viewing
@ -126,9 +127,9 @@ You can change which symbols you'd like to use for tagging in the
configuration. configuration.
!!! note !!! note
`jrnl @pinkie @WorldDomination` will switch to viewing mode because `jrnl @pinkie @WorldDomination` will switch to viewing mode because
although **no** command line arguments are given, all the input strings although **no** command line arguments are given, all the input strings
look like tags - *jrnl* will assume you want to filter by tag. look like tags - *jrnl* will assume you want to filter by tag.
## Editing older entries ## Editing older entries
@ -164,7 +165,7 @@ DayOne journals can be edited exactly the same way, however the output
looks a little bit different because of the way DayOne stores its looks a little bit different because of the way DayOne stores its
entries: entries:
``` output ```md
# af8dbd0d43fb55458f11aad586ea2abf # af8dbd0d43fb55458f11aad586ea2abf
2013-05-02 15:30 I told everyone I built my @robot wife for sex. 2013-05-02 15:30 I told everyone I built my @robot wife for sex.
But late at night when we're alone we mostly play Battleship. But late at night when we're alone we mostly play Battleship.

View file

@ -9,6 +9,9 @@ journals:
ideas: features/journals/nothing.journal ideas: features/journals/nothing.journal
simple: features/journals/simple.journal simple: features/journals/simple.journal
work: features/journals/work.journal work: features/journals/work.journal
new_encrypted:
encrypt: true
journal: features/journals/new_encrypted.journal
linewrap: 80 linewrap: 80
password: '' password: ''
tagsymbols: '@' tagsymbols: '@'

View file

@ -1,3 +1,13 @@
2010-06-10 15:00 A life without chocolate is like a bad analogy. 2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive". 2013-06-10 15:40 He said "[this] is the best time to be alive".
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
augue et venenatis facilisis.
[2019-08-03 12:55] Some chat log or something
Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
dui.

View file

@ -29,14 +29,3 @@
When we run "jrnl simple -n 1" When we run "jrnl simple -n 1"
Then we should not see the message "Password" Then we should not see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good" and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter
"""
Y
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

View file

@ -42,11 +42,9 @@ Feature: Exporting a Journal
When we run "jrnl --export markdown" When we run "jrnl --export markdown"
Then the output should be Then the output should be
""" """
2015 # 2015
====
April ## April
-----
### 2015-04-14 13:23 Heading Test ### 2015-04-14 13:23 Heading Test

View file

@ -39,3 +39,8 @@ Feature: Multiple journals
Given we use the config "bug343.yaml" Given we use the config "bug343.yaml"
When we run "jrnl a long day in the office" When we run "jrnl a long day in the office"
Then we should see the message "No default journal configured" Then we should see the message "No default journal configured"
Scenario: Don't crash if no file exists for a configured encrypted journal
Given we use the config "multiple.yaml"
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes"
Then we should see the message "Journal 'new_encrypted' created"

View file

@ -50,3 +50,35 @@ Feature: Tagging
@foo : 1 @foo : 1
@bar : 1 @bar : 1
""" """
Scenario: Excluding a tag should filter it
Given we use the config "basic.yaml"
When we run "jrnl today: @foo came over, we went to a bar"
When we run "jrnl I have decided I did not enjoy that @bar"
When we run "jrnl --tags -not @bar"
Then the output should be
"""
@foo : 1
"""
Scenario: Excluding a tag should filter an entry, even if an unfiltered tag is in that entry
Given we use the config "basic.yaml"
When we run "jrnl today: I do @not think this will show up @thought"
When we run "jrnl today: I think this will show up @thought"
When we run "jrnl --tags -not @not"
Then the output should be
"""
@thought : 1
"""
Scenario: Excluding multiple tags should filter them
Given we use the config "basic.yaml"
When we run "jrnl today: I do @not think this will show up @thought"
When we run "jrnl today: I think this will show up @thought"
When we run "jrnl today: This should @never show up @thought"
When we run "jrnl today: What a nice day for filtering @thought"
When we run "jrnl --tags -not @not @never"
Then the output should be
"""
@thought : 2
"""

23
features/upgrade.feature Normal file
View file

@ -0,0 +1,23 @@
Feature: Upgrading Journals from 1.x.x to 2.x.x
Scenario: Upgrade and parse journals with square brackets
Given we use the config "upgrade_from_195.json"
When we run "jrnl -9" and enter "Y"
Then the output should contain
"""
2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive".
"""
Then the journal should have 2 entries
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter
"""
Y
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

View file

@ -5,7 +5,13 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib import hashlib
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
import sys
import os
import base64 import base64
import getpass
import logging
log = logging.getLogger()
def make_key(password): def make_key(password):
@ -27,6 +33,33 @@ class EncryptedJournal(Journal.Journal):
super(EncryptedJournal, self).__init__(name, **kwargs) super(EncryptedJournal, self).__init__(name, **kwargs)
self.config['encrypt'] = True self.config['encrypt'] = True
def open(self, filename=None):
"""Opens the journal file defined in the config and parses it into a list of Entries.
Entries have the form (date, title, body)."""
filename = filename or self.config['journal']
if not os.path.exists(filename):
password = util.getpass("Enter password for new journal: ")
if password:
if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain(self.name, password)
else:
util.set_keychain(self.name, None)
self.config['password'] = password
text = ""
self._store(filename, text)
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
else:
util.prompt("No password supplied for encrypted journal")
sys.exit(1)
else:
text = self._load(filename)
self.entries = self._parse(text)
self.sort()
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self
def _load(self, filename, password=None): def _load(self, filename, password=None):
"""Loads an encrypted journal from a file and tries to decrypt it. """Loads an encrypted journal from a file and tries to decrypt it.
If password is not provided, will look for password in the keychain If password is not provided, will look for password in the keychain

View file

@ -84,9 +84,20 @@ class Journal(object):
def write(self, filename=None): def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it""" """Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal'] filename = filename or self.config['journal']
text = "\n".join([e.__unicode__() for e in self.entries]) text = self._to_text()
self._store(filename, text) self._store(filename, text)
def validate_parsing(self):
"""Confirms that the jrnl is still parsed correctly after being dumped to text."""
new_entries = self._parse(self._to_text())
for i, entry in enumerate(self.entries):
if entry != new_entries[i]:
return False
return True
def _to_text(self):
return "\n".join([e.__unicode__() for e in self.entries])
def _load(self, filename): def _load(self, filename):
raise NotImplementedError raise NotImplementedError
@ -118,7 +129,6 @@ class Journal(object):
last_entry_pos = match.end() last_entry_pos = match.end()
entries.append(Entry.Entry(self, date=new_date)) entries.append(Entry.Entry(self, date=new_date))
# If no entries were found, treat all the existing text as an entry made now # If no entries were found, treat all the existing text as an entry made now
if not entries: if not entries:
entries.append(Entry.Entry(self, date=time.parse("now"))) entries.append(Entry.Entry(self, date=time.parse("now")))
@ -176,7 +186,7 @@ class Journal(object):
tag_counts = set([(tags.count(tag), tag) for tag in tags]) tag_counts = set([(tags.count(tag), tag) for tag in tags])
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)] return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False): def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]):
"""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
@ -187,19 +197,24 @@ class Journal(object):
starred limits journal to starred entries 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."""
exclude is a list of the tags which should not appear in the results.
entry is kept if any tag is present, unless they appear in exclude."""
self.search_tags = set([tag.lower() for tag in tags]) self.search_tags = set([tag.lower() for tag in tags])
excluded_tags = set([tag.lower() for tag in exclude])
end_date = time.parse(end_date, inclusive=True) end_date = time.parse(end_date, inclusive=True)
start_date = time.parse(start_date) start_date = time.parse(start_date)
# If strict mode is on, all tags have to be present in entry # If strict mode is on, all tags have to be present in entry
tagged = self.search_tags.issubset if strict else self.search_tags.intersection tagged = self.search_tags.issubset if strict else self.search_tags.intersection
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
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 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)
and (not exclude or not excluded(entry.tags))
] ]
self.entries = result self.entries = result
@ -218,7 +233,11 @@ class Journal(object):
if not date: if not date:
colon_pos = first_line.find(": ") colon_pos = first_line.find(": ")
if colon_pos > 0: if colon_pos > 0:
date = time.parse(raw[:colon_pos], default_hour=self.config['default_hour'], default_minute=self.config['default_minute']) date = time.parse(
raw[:colon_pos],
default_hour=self.config['default_hour'],
default_minute=self.config['default_minute']
)
if date: # Parsed successfully, strip that from the raw text if date: # Parsed successfully, strip that from the raw text
starred = raw[:colon_pos].strip().endswith("*") starred = raw[:colon_pos].strip().endswith("*")
raw = raw[colon_pos + 1:].strip() raw = raw[colon_pos + 1:].strip()
@ -280,6 +299,7 @@ class LegacyJournal(Journal):
# Initialise our current entry # Initialise our current entry
entries = [] entries = []
current_entry = None current_entry = None
new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)')
for line in journal_txt.splitlines(): for line in journal_txt.splitlines():
line = line.rstrip() line = line.rstrip()
try: try:
@ -299,7 +319,9 @@ class LegacyJournal(Journal):
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred) current_entry = Entry.Entry(self, date=new_date, text=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 (after some
# escaping for the new format).
line = new_date_format_regex.sub(r' \1', line)
if current_entry: if current_entry:
current_entry.text += line + u"\n" current_entry.text += line + u"\n"
@ -325,7 +347,10 @@ def open_journal(name, config, legacy=False):
from . import DayOneJournal from . import DayOneJournal
return DayOneJournal.DayOne(**config).open() return DayOneJournal.DayOne(**config).open()
else: else:
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) util.prompt(
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])
)
sys.exit(1) sys.exit(1)
if not config['encrypt']: if not config['encrypt']:

View file

@ -13,7 +13,7 @@ from . import Journal
from . import util from . import util
from . import install from . import install
from . import plugins from . import plugins
from .util import ERROR_COLOR, RESET_COLOR from .util import ERROR_COLOR, RESET_COLOR, UserAbort
import jrnl import jrnl
import argparse import argparse
import sys import sys
@ -39,6 +39,7 @@ def parse_args(args=None):
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('-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. '-n 3' and '-3' have the same effect.", nargs="?", type=int) reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
reading.add_argument('-not', dest='excluded', nargs='+', default=[], metavar="E", help="Exclude entries with these 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('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags') exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
@ -143,7 +144,12 @@ def run(manual_args=None):
print(util.py2encode(version_str)) print(util.py2encode(version_str))
sys.exit(0) sys.exit(0)
config = install.load_or_install_jrnl() try:
config = install.load_or_install_jrnl()
except UserAbort as err:
util.prompt("\n{}".format(err))
sys.exit(1)
if args.ls: if args.ls:
util.prnt(list_journals(config)) util.prnt(list_journals(config))
sys.exit(0) sys.exit(0)
@ -234,7 +240,8 @@ def run(manual_args=None):
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) starred=args.starred,
exclude=args.excluded)
journal.limit(args.limit) journal.limit(args.limit)
# Reading mode # Reading mode

View file

@ -12,8 +12,10 @@ from . import upgrade
from . import __version__ from . import __version__
from .Journal import PlainJournal from .Journal import PlainJournal
from .EncryptedJournal import EncryptedJournal from .EncryptedJournal import EncryptedJournal
from .util import UserAbort
import yaml import yaml
import logging import logging
import sys
DEFAULT_CONFIG_NAME = 'jrnl.yaml' DEFAULT_CONFIG_NAME = 'jrnl.yaml'
DEFAULT_JOURNAL_NAME = 'journal.txt' DEFAULT_JOURNAL_NAME = 'journal.txt'
@ -85,12 +87,26 @@ def load_or_install_jrnl():
if os.path.exists(config_path): if os.path.exists(config_path):
log.debug('Reading configuration from file %s', config_path) log.debug('Reading configuration from file %s', config_path)
config = util.load_config(config_path) config = util.load_config(config_path)
upgrade.upgrade_jrnl_if_necessary(config_path)
try:
upgrade.upgrade_jrnl_if_necessary(config_path)
except upgrade.UpgradeValidationException:
util.prompt("Aborting upgrade.")
util.prompt("Please tell us about this problem at the following URL:")
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException")
util.prompt("Exiting.")
sys.exit(1)
upgrade_config(config) upgrade_config(config)
return config return config
else: else:
log.debug('Configuration file not found, installing jrnl...') log.debug('Configuration file not found, installing jrnl...')
return install() try:
config = install()
except KeyboardInterrupt:
raise UserAbort("Installation aborted")
return config
def install(): def install():

View file

@ -3,6 +3,7 @@
from __future__ import absolute_import, unicode_literals, print_function from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter from .text_exporter import TextExporter
import os
import re import re
import sys import sys
from ..util import WARNING_COLOR, RESET_COLOR from ..util import WARNING_COLOR, RESET_COLOR
@ -30,17 +31,17 @@ class MarkdownExporter(TextExporter):
previous_line = '' previous_line = ''
warn_on_heading_level = False warn_on_heading_level = False
for line in body.splitlines(True): for line in body.splitlines(True):
if re.match(r"#+ ", line): if re.match(r"^#+ ", line):
"""ATX style headings""" """ATX style headings"""
newbody = newbody + previous_line + heading + line newbody = newbody + previous_line + heading + line
if re.match(r"#######+ ", heading + line): if re.match(r"^#######+ ", heading + line):
warn_on_heading_level = True warn_on_heading_level = True
line = '' line = ''
elif re.match(r"=+$", line) and not re.match(r"^$", previous_line): elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
"""Setext style H1""" """Setext style H1"""
newbody = newbody + heading + "# " + previous_line newbody = newbody + heading + "# " + previous_line
line = '' line = ''
elif re.match(r"-+$", line) and not re.match(r"^$", previous_line): elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
"""Setext style H2""" """Setext style H2"""
newbody = newbody + heading + "## " + previous_line newbody = newbody + heading + "## " + previous_line
line = '' line = ''
@ -68,12 +69,12 @@ class MarkdownExporter(TextExporter):
for e in journal.entries: for e in journal.entries:
if not e.date.year == year: if not e.date.year == year:
year = e.date.year year = e.date.year
out.append(str(year)) out.append("# " + str(year))
out.append("=" * len(str(year)) + "\n") out.append("")
if not e.date.month == month: if not e.date.month == month:
month = e.date.month month = e.date.month
out.append(e.date.strftime("%B")) out.append("## " + e.date.strftime("%B"))
out.append('-' * len(e.date.strftime("%B")) + "\n") out.append("")
out.append(cls.export_entry(e, False)) out.append(cls.export_entry(e, False))
result = "\n".join(out) result = "\n".join(out)
return result return result

View file

@ -3,6 +3,7 @@
from __future__ import absolute_import, unicode_literals, print_function from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter from .text_exporter import TextExporter
import os
import re import re
import sys import sys
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
@ -38,14 +39,14 @@ class YAMLExporter(TextExporter):
if re.match(r"#+ ", line): if re.match(r"#+ ", line):
"""ATX style headings""" """ATX style headings"""
newbody = newbody + previous_line + heading + line newbody = newbody + previous_line + heading + line
if re.match(r"#######+ ", heading + line): if re.match(r"^#######+ ", heading + line):
warn_on_heading_level = True warn_on_heading_level = True
line = '' line = ''
elif re.match(r"=+$", line) and not re.match(r"^$", previous_line): elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
"""Setext style H1""" """Setext style H1"""
newbody = newbody + heading + "# " + previous_line newbody = newbody + heading + "# " + previous_line
line = '' line = ''
elif re.match(r"-+$", line) and not re.match(r"^$", previous_line): elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
"""Setext style H2""" """Setext style H2"""
newbody = newbody + heading + "## " + previous_line newbody = newbody + heading + "## " + previous_line
line = '' line = ''

View file

@ -4,7 +4,7 @@ from . import __version__
from . import Journal from . import Journal
from . import util from . import util
from .EncryptedJournal import EncryptedJournal from .EncryptedJournal import EncryptedJournal
import sys from .util import UserAbort
import os import os
import codecs import codecs
@ -44,6 +44,7 @@ older versions of jrnl anymore.
encrypted_journals = {} encrypted_journals = {}
plain_journals = {} plain_journals = {}
other_journals = {} other_journals = {}
all_journals = []
for journal_name, journal_conf in config['journals'].items(): for journal_name, journal_conf in config['journals'].items():
if isinstance(journal_conf, dict): if isinstance(journal_conf, dict):
@ -76,28 +77,44 @@ older versions of jrnl anymore.
for journal, path in other_journals.items(): for journal, path in other_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
cont = util.yesno("\nContinue upgrading jrnl?", default=False) try:
if not cont: cont = util.yesno("\nContinue upgrading jrnl?", default=False)
util.prompt("jrnl NOT upgraded, exiting.") if not cont:
sys.exit(1) raise KeyboardInterrupt
except KeyboardInterrupt:
raise UserAbort("jrnl NOT upgraded, exiting.")
for journal_name, path in encrypted_journals.items(): for journal_name, path in encrypted_journals.items():
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path)) util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
backup(path, binary=True) backup(path, binary=True)
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
new_journal = EncryptedJournal.from_journal(old_journal) all_journals.append(EncryptedJournal.from_journal(old_journal))
new_journal.write()
util.prompt(" Done.")
for journal_name, path in plain_journals.items(): for journal_name, path in plain_journals.items():
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path)) util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
backup(path) backup(path)
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
new_journal = Journal.PlainJournal.from_journal(old_journal) all_journals.append(Journal.PlainJournal.from_journal(old_journal))
new_journal.write()
util.prompt(" Done.") # loop through lists to validate
failed_journals = [j for j in all_journals if not j.validate_parsing()]
if len(failed_journals) > 0:
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format(
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals))
)
raise UpgradeValidationException
# write all journals - or - don't
for j in all_journals:
j.write()
util.prompt("\nUpgrading config...") util.prompt("\nUpgrading config...")
backup(config_path) backup(config_path)
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.") util.prompt("\nWe're all done here and you can start enjoying jrnl 2.")
class UpgradeValidationException(Exception):
"""Raised when the contents of an upgraded journal do not match the old journal"""
pass

View file

@ -47,6 +47,10 @@ SENTENCE_SPLITTER = re.compile(r"""
)""", re.UNICODE | re.VERBOSE) )""", re.UNICODE | re.VERBOSE)
class UserAbort(Exception):
pass
def getpass(prompt="Password: "): def getpass(prompt="Password: "):
if not TEST: if not TEST:
return gp.getpass(bytes(prompt)) return gp.getpass(bytes(prompt))

View file

@ -85,7 +85,7 @@ setup(
"six>=1.10.0", "six>=1.10.0",
"cryptography>=1.4", "cryptography>=1.4",
"tzlocal>=1.2", "tzlocal>=1.2",
"pyyaml>=3.11", "pyyaml>=5.1",
"keyring>=7.3", "keyring>=7.3",
"passlib>=1.6.2", "passlib>=1.6.2",
"pyxdg>=0.25", "pyxdg>=0.25",