Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Alex Apolloni 2013-10-28 10:48:19 +01:00
commit 7233507ca0
40 changed files with 1102 additions and 171 deletions

1
.gitignore vendored
View file

@ -21,4 +21,5 @@ lib64
# Installer logs
pip-log.txt
.DS_Store
.travis-solo

View file

@ -2,12 +2,14 @@ language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
install: "pip install -r requirements.txt --use-mirrors"
install:
- "pip install -q -r requirements.txt --use-mirrors"
- "pip install -q behave python-dateutil"
# command to run tests
script: nosetests
script:
- python --version
- behave
matrix:
allow_failures: # python 3 support for travis is shaky....
- python: 3.2
- python: 3.3

View file

@ -1,7 +1,69 @@
Changelog
=========
#### 1.1.0
#### 1.6.0
* [Improved] Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
#### 1.5.7
* [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
#### 1.5.6
* [Fixed] Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts.
#### 1.5.5
* [Fixed] Detects DayOne journals stored in `~/Library/Mobile Data` as well.
#### 1.5.4
* [New] DayOne journals can now handle tags
#### 1.5.3
* [Fixed] DayOne integration with older DayOne Journals
#### 1.5.2
* [Improved] Soft-deprecated `-to` for filtering by time and introduces `-until` instead.
#### 1.5.1
* [Fixed] Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry
### 1.5.0
* [Improved] Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags`
#### 1.4.2
* [Fixed] Tagging works again
* Meta-info for PyPi updated
### 1.4.0
* [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`).
#### 1.3.2
* [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration
### 1.3.0
* [New] Export to multiple files
* [New] Feature to export to given output file
#### 1.1.2
* [Fixed] Timezone support for DayOne
#### 1.1.1
* [Fixed] Unicode and Python3 issues resolved.
### 1.1.0
* [New] JSON export exports tags as well.
* [Improved] Nicer error message when there is a syntactical error in your config file.

View file

@ -1,7 +1,9 @@
Copyright (c) 2012 Manuel Ebert
Copyright (c) 2013 Manuel Ebert
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
:f

View file

@ -1,6 +1,8 @@
jrnl [![Build Status](https://travis-ci.org/maebert/jrnl.png?branch=master)](https://travis-ci.org/maebert/jrnl)
====
_For news on updates or to get help, follow [@maebert](https://twitter.com/maebert) or [submit an issue](https://github.com/maebert/jrnl/issues/new) on Github._
*jrnl* is a simple journal application for your command line. Journals are stored as human readable plain text files - you can put them into a Dropbox folder for instant syncing and you can be assured that your journal will still be readable in 2050, when all your fancy iPad journal applications will long be forgotten.
*jrnl* also plays nice with the fabulous [DayOne](http://dayoneapp.com/) and can read and write directly from and to DayOne Journals.
@ -56,7 +58,7 @@ _jrnl_ has two modes: __composing__ and __viewing__.
will list you the ten latest entries,
jrnl -from "last year" -to march
jrnl -from "last year" -until march
everything that happened from the start of last year to the start of last march. If you only want to see the titles of your entries, use
@ -114,16 +116,49 @@ you'll get a list of all tags you used in your journal, sorted by most frequent.
Can do:
jrnl --json
jrnl --export json
Why not create a beautiful [timeline](http://timeline.verite.co/) of your journal?
### Markdown export
jrnl --markdown
Use:
jrnl --export markdown
or
jrnl --export md
Markdown is a simple markup language that is human readable and can be used to be rendered to other formats (html, pdf). This README for example is formatted in markdown and github makes it look nice.
### Text export
jrnl --export text
or
jrnl --export txt
Prettyprints your entire journal.
### Export to files
You can specify the output file of your exported journal using the `-o` argument:
jrnl --export md -o journal.md
The above command will generate a file named `journal.md`. If the `-o` argument is a directory, jrnl will export each entry into an individual file:
jrnl --export json -o my_entries/
The contents of `my_entries/` will then look like this:
my_entries/
|- 2013_06_03_a-beautiful-day.json
|- 2013_06_07_dinner-with-gabriel.json
|- ...
Encryption
----------
@ -150,7 +185,6 @@ The configuration file is a simple JSON file with the following options.
- `journals`: paths to your journal files
- `editor`: if set, executes this command to launch an external editor for writing your entries, e.g. `vim` or `subl -w` (note the `-w` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal).
- `encrypt`: if `true`, encrypts your journal using AES.
- `password`: you may store the password you used to encrypt your journal in plaintext here. This is useful if your journal file lives in an unsecure space (ie. your Dropbox), but the config file itself is more or less safe.
- `tagsymbols`: Symbols to be interpreted as tags. (__See note below__)
- `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time
- `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference
@ -169,11 +203,11 @@ The configuration file is a simple JSON file with the following options.
### DayOne Integration
Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, set the `"journal"` key in your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at
Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, change your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at
* `~/Library/Application Support/Day One/` by default
* `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and
* `~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're syncing with iCloud.
* `~/Library/Application Support/Day One/` by default
* `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and
* `~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're syncing with iCloud.
Instead of all entries being in a single file, each entry will live in a separate `plist` file. You can also star entries when you write them:
@ -228,3 +262,7 @@ Known Issues
- The Windows shell prior to Windows 7 has issues with unicode encoding. If you want to use non-ascii characters, change the codepage with `chcp 1252` before using `jrnl` (Thanks to Yves Pouplard for solving this!)
- _jrnl_ relies on the `PyCrypto` package to encrypt journals, which has some known problems with installing on Windows and within virtual environments. If you have trouble installing __jrnl__, [install PyCyrypto manually](https://www.dlitz.net/software/pycrypto/) first.
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/maebert/jrnl/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

36
features/core.feature Normal file
View file

@ -0,0 +1,36 @@
Feature: Basic reading and writing to a journal
Scenario: Loading a sample journal
Given we use the config "basic.json"
When we run "jrnl -n 2"
Then we should get no error
and the output should be
"""
2013-06-09 15:39 My first entry.
| Everything is alright
2013-06-10 15:40 Life is good.
| But I'm better.
"""
Scenario: Writing an entry from command line
Given we use the config "basic.json"
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
Scenario: Emoji support
Given we use the config "basic.json"
When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘"
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should contain "🌞"
and the output should contain "🐘"
Scenario: Writing an entry at the prompt
Given we use the config "basic.json"
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
Then we should get no error
and the journal should contain "2013-07-25 09:00 I saw Elvis."
and the journal should contain "He's alive."

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/simple.journal"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/dayone.dayone"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/empty_folder"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": true,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/encrypted.journal"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": true,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "bad doggie no biscuit",
"journals": {
"default": "features/journals/encrypted.journal"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,17 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/simple.journal",
"simple": "features/journals/simple.journal",
"work": "features/journals/work.journal",
"ideas": "features/journals/nothing.journal"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,14 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"password": "",
"journals": {
"default": "features/journals/tags.journal"
},
"tagsymbols": "@"
}

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Creation Date</key>
<date>2013-05-17T18:39:20Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>Macintosh/MacBookAir5,2</string>
<key>Generation Date</key>
<date>2013-08-17T18:39:20Z</date>
<key>Host Name</key>
<string>Egeria</string>
<key>OS Agent</key>
<string>Mac OS X/10.8.4</string>
<key>Software Agent</key>
<string>Day One (Mac)/1.8</string>
</dict>
<key>Entry Text</key>
<string>This entry has tags!</string>
<key>Starred</key>
<false/>
<key>Tags</key>
<array>
<string>work</string>
<string>play</string>
</array>
<key>Time Zone</key>
<string>America/Los_Angeles</string>
<key>UUID</key>
<string>044F3747A38546168B572C2E3F217FA2</string>
</dict>
</plist>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Creation Date</key>
<date>2013-06-17T18:38:29Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>Macintosh/MacBookAir5,2</string>
<key>Generation Date</key>
<date>2013-08-17T18:38:29Z</date>
<key>Host Name</key>
<string>Egeria</string>
<key>OS Agent</key>
<string>Mac OS X/10.8.4</string>
<key>Software Agent</key>
<string>Day One (Mac)/1.8</string>
</dict>
<key>Entry Text</key>
<string>This entry has a location.</string>
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>California</string>
<key>Country</key>
<string>Germany</string>
<key>Latitude</key>
<real>52.4979764</real>
<key>Locality</key>
<string>Berlin</string>
<key>Longitude</key>
<real>13.2404758</real>
<key>Place Name</key>
<string>Abandoned Spy Tower</string>
</dict>
<key>Starred</key>
<false/>
<key>Tags</key>
<array/>
<key>Time Zone</key>
<string>Europe/Berlin</string>
<key>UUID</key>
<string>0BDDD6CDA43C4A9AA2681517CC35AD9D</string>
</dict>
</plist>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Creation Date</key>
<date>2013-07-17T18:38:08Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>Macintosh/MacBookAir5,2</string>
<key>Generation Date</key>
<date>2013-08-17T18:38:08Z</date>
<key>Host Name</key>
<string>Egeria</string>
<key>OS Agent</key>
<string>Mac OS X/10.8.4</string>
<key>Software Agent</key>
<string>Day One (Mac)/1.8</string>
</dict>
<key>Entry Text</key>
<string>This entry is starred!</string>
<key>Starred</key>
<true/>
<key>Tags</key>
<array/>
<key>Time Zone</key>
<string>America/Los_Angeles</string>
<key>UUID</key>
<string>422BC895507944A291E6FC44FC6B8BFC</string>
</dict>
</plist>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Creation Date</key>
<date>2013-01-17T18:37:50Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>Macintosh/MacBookAir5,2</string>
<key>Generation Date</key>
<date>2013-08-17T18:37:50Z</date>
<key>Host Name</key>
<string>Egeria</string>
<key>OS Agent</key>
<string>Mac OS X/10.8.4</string>
<key>Software Agent</key>
<string>Day One (Mac)/1.8</string>
</dict>
<key>Entry Text</key>
<string>This is a DayOne entry without Timezone.</string>
<key>Starred</key>
<false/>
<key>Tags</key>
<array/>
<key>UUID</key>
<string>4BB1F46946AD439996C9B59DE7C4DDC1</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
Nothing to see here

Binary file not shown.

View file

@ -0,0 +1,5 @@
2013-06-09 15:39 My first entry.
Everything is alright
2013-06-10 15:40 Life is good.
But I'm better.

View file

@ -0,0 +1,7 @@
2013-04-09 15:39 I have an @idea:
(1) write a command line @journal software
(2) ???
(3) PROFIT!
2013-06-10 15:40 I met with @dan.
As alway's he shared his latest @idea on how to rule the world with me.

View file

48
features/dayone.feature Normal file
View file

@ -0,0 +1,48 @@
Feature: DayOne Ingetration
Scenario: Loading a DayOne Journal
Given we use the config "dayone.json"
When we run "jrnl -from 'feb 2013'"
Then we should get no error
and the output should be
"""
2013-05-17 11:39 This entry has tags!
2013-06-17 20:38 This entry has a location.
2013-07-17 11:38 This entry is starred!
"""
Scenario: Entries without timezone information will be intepreted in the current timezone
Given we use the config "dayone.json"
When we run "jrnl -until 'feb 2013'"
Then we should get no error
and the output should contain "2013-01-17T18:37Z" in the local time
Scenario: Writing into Dayone
Given we use the config "dayone.json"
When we run "jrnl 01 may 1979: Being born hurts."
and we run "jrnl -until 1980"
Then the output should be
"""
1979-05-01 09:00 Being born hurts.
"""
Scenario: Loading tags from a DayOne Journal
Given we use the config "dayone.json"
When we run "jrnl --tags"
Then the output should be
"""
work : 1
play : 1
"""
Scenario: Saving tags from a DayOne Journal
Given we use the config "dayone.json"
When we run "jrnl A hard day at @work"
and we run "jrnl --tags"
Then the output should be
"""
work : 2
play : 1
"""

View file

@ -0,0 +1,37 @@
Feature: Encrypted journals
Scenario: Loading an encrypted journal
Given we use the config "encrypted.json"
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Decrypting a journal
Given we use the config "encrypted.json"
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then the config for journal "default" should have "encrypt" set to "bool:False"
Then we should see the message "Journal decrypted"
and the journal should have 2 entries
Scenario: Encrypting a journal
Given we use the config "basic.json"
When we run "jrnl --encrypt" and enter "swordfish"
Then we should see the message "Journal encrypted"
and the config for journal "default" should have "encrypt" set to "bool:True"
When we run "jrnl -n 1" and enter "swordfish"
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"
Scenario: Loading an encrypted journal with password in config
Given we use the config "encrypted_with_pw.json"
When we run "jrnl -n 1"
Then the output should contain "2013-06-10 15:40 Life is good"
Scenario: Storing a password in Keychain
Given we use the config "multiple.json"
When we run "jrnl simple --encrypt" and enter "sabertooth"
When we set the keychain password of "simple" to "sabertooth"
Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1"
Then we should not see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

42
features/environment.py Normal file
View file

@ -0,0 +1,42 @@
from behave import *
import shutil
import os
from jrnl import jrnl
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
def before_scenario(context, scenario):
"""Before each scenario, backup all config and journal test data."""
context.messages = StringIO()
jrnl.util.STDERR = context.messages
jrnl.util.TEST = True
# Clean up in case something went wrong
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):
shutil.rmtree(working_dir)
for folder in ("configs", "journals"):
original = os.path.join("features", "data", folder)
working_dir = os.path.join("features", folder)
if not os.path.exists(working_dir):
os.mkdir(working_dir)
for filename in os.listdir(original):
source = os.path.join(original, filename)
if os.path.isdir(source):
shutil.copytree(source, os.path.join(working_dir, filename))
else:
shutil.copy2(source, working_dir)
def after_scenario(context, scenario):
"""After each scenario, restore all test data and remove working_dirs."""
context.messages.close()
context.messages = None
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):
shutil.rmtree(working_dir)

View file

@ -0,0 +1,22 @@
Feature: Expoting a Journal
Scenario: Exporting to json
Given we use the config "tags.json"
When we run "jrnl --export json"
Then we should get no error
and the output should be parsable as json
and "entries" in the json output should have 2 elements
and "tags" in the json output should contain "@idea"
and "tags" in the json output should contain "@journal"
and "tags" in the json output should contain "@dan"
Scenario: Exporting using filters should only export parts of the journal
Given we use the config "tags.json"
When we run "jrnl -until 'may 2013' --export json"
# Then we should get no error
Then the output should be parsable as json
and "entries" in the json output should have 1 element
and "tags" in the json output should contain "@idea"
and "tags" in the json output should contain "@journal"
and "tags" in the json output should not contain "@dan"

View file

@ -0,0 +1,36 @@
Feature: Multiple journals
Scenario: Loading a config with two journals
Given we use the config "multiple.json"
Then journal "default" should have 2 entries
and journal "work" should have 0 entries
Scenario: Write to default config by default
Given we use the config "multiple.json"
When we run "jrnl this goes to default"
Then journal "default" should have 3 entries
and journal "work" should have 0 entries
Scenario: Write to specified journal
Given we use the config "multiple.json"
When we run "jrnl work a long day in the office"
Then journal "default" should have 2 entries
and journal "work" should have 1 entry
Scenario: Tell user which journal was used
Given we use the config "multiple.json"
When we run "jrnl work a long day in the office"
Then we should see the message "Entry added to work journal"
Scenario: Write to specified journal with a timestamp
Given we use the config "multiple.json"
When we run "jrnl work 23 july 2012: a long day in the office"
Then journal "default" should have 2 entries
and journal "work" should have 1 entry
and journal "work" should contain "2012-07-23"
Scenario: Create new journals as required
Given we use the config "multiple.json"
Then journal "ideas" should not exist
When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money"
Then journal "ideas" should have 1 entry

View file

@ -0,0 +1,15 @@
Feature: Zapped bugs should stay dead.
Scenario: Writing an entry does not print the entire journal
# https://github.com/maebert/jrnl/issues/87
Given we use the config "basic.json"
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
When we run "jrnl -n 1"
Then the output should not contain "Life is good"
Scenario: Opening an folder that's not a DayOne folder gives a nice error message
Given we use the config "empty_folder.json"
When we run "jrnl Herro"
Then we should get an error
Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either"

187
features/steps/core.py Normal file
View file

@ -0,0 +1,187 @@
from behave import *
from jrnl import jrnl, Journal, util
from dateutil import parser as date_parser
import os
import sys
import json
import pytz
import keyring
keyring.set_keyring(keyring.backends.file.PlaintextKeyring())
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
def _parse_args(command):
nargs=[]
concats = []
for a in command.split()[1:]:
if a.startswith("'"):
concats.append(a.strip("'"))
elif a.endswith("'"):
concats.append(a.strip("'"))
nargs.append(u" ".join(concats))
concats = []
else:
nargs.append(a)
return nargs
def read_journal(journal_name="default"):
with open(jrnl.CONFIG_PATH) as config_file:
config = json.load(config_file)
with open(config['journals'][journal_name]) as journal_file:
journal = journal_file.read()
return journal
def open_journal(journal_name="default"):
with open(jrnl.CONFIG_PATH) as config_file:
config = json.load(config_file)
journal_conf = config['journals'][journal_name]
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
return Journal.Journal(**config)
@given('we use the config "{config_file}"')
def set_config(context, config_file):
full_path = os.path.join("features/configs", config_file)
jrnl.CONFIG_PATH = os.path.abspath(full_path)
@when('we run "{command}" and enter')
@when('we run "{command}" and enter "{inputs}"')
def run_with_input(context, command, inputs=None):
text = inputs or context.text
args = _parse_args(command)
buffer = StringIO(text.strip())
jrnl.util.STDIN = buffer
try:
jrnl.cli(args or None)
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@when('we run "{command}"')
def run(context, command):
args = _parse_args(command)
try:
jrnl.cli(args or None)
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@when('we set the keychain password of "{journal}" to "{password}"')
def set_keychain(context, journal, password):
keyring.set_password('jrnl', journal, password)
@then('we should get an error')
def has_error(context):
assert context.exit_status != 0, context.exit_status
@then('we should get no error')
def no_error(context):
assert context.exit_status is 0, context.exit_status
@then('the output should be parsable as json')
def check_output_json(context):
out = context.stdout_capture.getvalue()
assert json.loads(out), out
@then('"{field}" in the json output should have {number:d} elements')
@then('"{field}" in the json output should have 1 element')
def check_output_field(context, field, number=1):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json, [field, out_json]
assert len(out_json[field]) == number, len(out_json[field])
@then('"{field}" in the json output should not contain "{key}"')
def check_output_field_not_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key not in out_json[field]
@then('"{field}" in the json output should contain "{key}"')
def check_output_field_key(context, field, key):
out = context.stdout_capture.getvalue()
out_json = json.loads(out)
assert field in out_json
assert key in out_json[field]
@then('the output should be')
def check_output(context):
text = context.text.strip().splitlines()
out = context.stdout_capture.getvalue().strip().splitlines()
for line_text, line_out in zip(text, out):
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
@then('the output should contain "{text}" in the local time')
def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue()
local_tz = pytz.timezone(util.get_local_timezone())
utc_time = date_parser.parse(text)
date = utc_time + local_tz._utcoffset
local_date = date.strftime("%Y-%m-%d %H:%M")
assert local_date in out, local_date
@then('the output should contain "{text}"')
def check_output_inline(context, text):
out = context.stdout_capture.getvalue()
assert text in out
@then('the output should not contain "{text}"')
def check_output_not_inline(context, text):
out = context.stdout_capture.getvalue()
assert text not in out
@then('we should see the message "{text}"')
def check_message(context, text):
out = context.messages.getvalue()
assert text in out, [text, out]
@then('we should not see the message "{text}"')
def check_not_message(context, text):
out = context.messages.getvalue()
assert text not in out, [text, out]
@then('the journal should contain "{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
@then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"):
with open(jrnl.CONFIG_PATH) as config_file:
config = json.load(config_file)
journal_path = config['journals'][journal_name]
assert not os.path.exists(journal_path)
@then('the config should have "{key}" set to "{value}"')
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
def config_var(context, key, value, journal=None):
t, value = value.split(":")
value = {
"bool": lambda v: v.lower() == "true",
"int": int,
"str": str
}[t](value)
with open(jrnl.CONFIG_PATH) as config_file:
config = json.load(config_file)
if journal:
config = config["journals"][journal]
assert key in config
assert config[key] == value
@then('the journal should have {number:d} entries')
@then('the journal should have {number:d} entry')
@then('journal "{journal_name}" should have {number:d} entries')
@then('journal "{journal_name}" should have {number:d} entry')
def check_journal_content(context, number, journal_name="default"):
journal = open_journal(journal_name)
assert len(journal.entries) == number
@then('fail')
def debug_fail(context):
assert False

22
features/tagging.feature Normal file
View file

@ -0,0 +1,22 @@
Feature: Tagging
Scenario: Displaying tags
Given we use the config "tags.json"
When we run "jrnl --tags"
Then we should get no error
and the output should be
"""
@idea : 2
@journal : 1
@dan : 1
"""
Scenario: Filtering journals should also filter tags
Given we use the config "tags.json"
When we run "jrnl -from 'may 2013' --tags"
Then we should get no error
and the output should be
"""
@idea : 1
@dan : 1
"""

View file

@ -15,8 +15,9 @@ class Entry:
def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower()
tags = re.findall(ur'([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE)
self.tags = set(tags)
tags = re.findall(r'(?u)([{tags}]\w+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE)
self.tags = tags
return set(tags)
def __unicode__(self):
"""Returns a string representation of the entry to be written into a journal file."""
@ -57,7 +58,7 @@ class Entry:
)
def __repr__(self):
return str(self)
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
def to_dict(self):
return {

View file

@ -3,6 +3,8 @@
try: from . import Entry
except (SystemError, ValueError): import Entry
try: from . import util
except (SystemError, ValueError): import util
import codecs
import os
try: import parsedatetime.parsedatetime_consts as pdt
@ -10,35 +12,29 @@ except ImportError: import parsedatetime.parsedatetime as pdt
import re
from datetime import datetime
import time
try: import simplejson as json
except ImportError: import json
import sys
import glob
try:
from Crypto.Cipher import AES
from Crypto.Random import random, atfork
from Crypto import Random
crypto_installed = True
except ImportError:
crypto_installed = False
if "win32" in sys.platform: import pyreadline as readline
else: import readline
import hashlib
import getpass
try:
import colorama
colorama.init()
except ImportError:
colorama = None
import plistlib
import pytz
import uuid
from functools import partial
class Journal(object):
def __init__(self, **kwargs):
def __init__(self, name='default', **kwargs):
self.config = {
'journal': "journal.txt",
'encrypt': False,
'password': "",
'default_hour': 9,
'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M",
@ -47,12 +43,13 @@ class Journal(object):
'linewrap': 80,
}
self.config.update(kwargs)
# Set up date parser
consts = pdt.Constants(usePyICU=False)
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
self.dateparse = pdt.Calendar(consts)
self.key = None # used to decrypt and encrypt the journal
self.search_tags = None # Store tags we're highlighting
self.name = name
journal_txt = self.open()
self.entries = self.parse(journal_txt)
@ -74,53 +71,49 @@ class Journal(object):
try:
plain = crypto.decrypt(cipher[16:])
except ValueError:
print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
sys.exit(-1)
if plain[-1] != " ": # Journals are always padded
util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
sys.exit(1)
padding = " ".encode("utf-8")
if not plain.endswith(padding): # Journals are always padded
return None
else:
return plain
return plain.decode("utf-8")
def _encrypt(self, plain):
"""Encrypt a plaintext string using self.key as the key"""
if not crypto_installed:
sys.exit("Error: PyCrypto is not installed.")
atfork() # A seed for PyCrypto
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
Random.atfork() # A seed for PyCrypto
iv = Random.new().read(AES.block_size)
crypto = AES.new(self.key, AES.MODE_CBC, iv)
if len(plain) % 16 != 0:
plain += " " * (16 - len(plain) % 16)
else: # Always pad so we can detect properly decrypted files :)
plain += " " * 16
plain = plain.encode("utf-8")
plain += b" " * (AES.block_size - len(plain) % AES.block_size)
return iv + crypto.encrypt(plain)
def make_key(self, prompt="Password: "):
def make_key(self, password):
"""Creates an encryption key from the default password or prompts for a new password."""
password = self.config['password'] or getpass.getpass(prompt)
self.key = hashlib.sha256(password.encode('utf-8')).digest()
self.key = hashlib.sha256(password.encode("utf-8")).digest()
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']
journal = None
if self.config['encrypt']:
with open(filename, "rb") as f:
journal = f.read()
decrypted = None
attempts = 0
while decrypted is None:
self.make_key()
decrypted = self._decrypt(journal)
if decrypted is None:
attempts += 1
self.config['password'] = None # This password doesn't work.
if attempts < 3:
print("Wrong password, try again.")
else:
print("Extremely wrong password.")
sys.exit(-1)
journal = decrypted
journal_encrypted = f.read()
def validate_password(password):
self.make_key(password)
return self._decrypt(journal_encrypted)
# Soft-deprecated:
journal = None
if 'password' in self.config:
journal = validate_password(self.config['password'])
if not journal:
journal = util.get_password(keychain=self.name, validator=validate_password)
else:
with codecs.open(filename, "r", "utf-8") as f:
journal = f.read()
@ -149,7 +142,8 @@ class Journal(object):
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.
current_entry.body += line + "\n"
if current_entry:
current_entry.body += line + "\n"
# Append last entry
if current_entry:
@ -170,18 +164,21 @@ class Journal(object):
lambda match: self._colorize(match.group(0)),
pp, re.UNICODE)
else:
pp = re.sub(ur"(?u)([{}]\w+)".format(self.config['tagsymbols']),
pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']),
lambda match: self._colorize(match.group(0)),
pp)
return pp
def pprint(self):
return self.__unicode__()
def __repr__(self):
return "<Journal with %d entries>" % len(self.entries)
return "<Journal with {0} entries>".format(len(self.entries))
def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal']
journal = "\n".join([unicode(e) for e in self.entries])
journal = "\n".join([e.__unicode__() for e in self.entries])
if self.config['encrypt']:
journal = self._encrypt(journal)
with open(filename, 'wb') as journal_file:
@ -229,7 +226,7 @@ class Journal(object):
for m in matches:
date = e.date.strftime(self.config['timeformat'])
excerpt = e.body[m.start():min(len(e.body), m.end()+60)]
res.append('%s %s ..' % (date, excerpt))
res.append('{0} {1} ..'.format(date, excerpt))
e.body = "\n".join(res)
else:
for e in self.entries:
@ -306,11 +303,18 @@ class DayOne(Journal):
of filenames, interpret each as a plist file and create a new entry from that."""
self.entries = []
for filename in filenames:
with open(filename) as plist_entry:
with open(filename, 'rb') as plist_entry:
dict_entry = plistlib.readPlist(plist_entry)
entry = self.new_entry(raw=dict_entry['Entry Text'], date=dict_entry['Creation Date'], sort=False)
try:
timezone = pytz.timezone(dict_entry['Time Zone'])
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
timezone = pytz.timezone(util.get_local_timezone())
date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date)
entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False)
entry.starred = dict_entry["Starred"]
entry.uuid = dict_entry["UUID"]
entry.tags = dict_entry.get("Tags", [])
# We're using new_entry to create the Entry object, which adds the entry
# to self.entries already. However, in the original Journal.__init__, this
# method is expected to return a list of newly created entries, which is why
@ -324,12 +328,17 @@ class DayOne(Journal):
# that have a uuid will be old ones, and only the one that doesn't will
# have a new one!
if not hasattr(entry, "uuid"):
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
new_uuid = uuid.uuid1().hex
filename = os.path.join(self.config['journal'], "entries", new_uuid+".doentry")
entry_plist = {
'Creation Date': entry.date,
'Creation Date': utc_time,
'Starred': entry.starred if hasattr(entry, 'starred') else False,
'Entry Text': entry.title+"\n"+entry.body,
'UUID': new_uuid
'Time Zone': util.get_local_timezone(),
'UUID': new_uuid,
'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags]
}
# print entry_plist
plistlib.writePlist(entry_plist, filename)

View file

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

View file

@ -1,8 +1,15 @@
#!/usr/bin/env python
# encoding: utf-8
import os
import string
try: from slugify import slugify
except ImportError: import slugify
try: import simplejson as json
except ImportError: import json
try: from .util import u
except (SystemError, ValueError): from util import u
def get_tags_count(journal):
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
@ -17,7 +24,7 @@ def get_tags_count(journal):
return tag_counts
def to_tag_list(journal):
"""Prints a list of all tags and the number of occurances."""
"""Prints a list of all tags and the number of occurrences."""
tag_counts = get_tags_count(journal)
result = ""
if not tag_counts:
@ -25,7 +32,7 @@ def to_tag_list(journal):
elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n'
result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False))
result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result
def to_json(journal):
@ -51,4 +58,53 @@ def to_md(journal):
out.append(e.date.strftime("%B"))
out.append('-' * len(e.date.strftime("%B")) + "\n")
out.append(e.to_md())
return "\n".join(out)
result = "\n".join(out)
return result
def to_txt(journal):
"""Returns the complete text of the Journal."""
return journal.pprint()
def export(journal, format, output=None):
"""Exports the journal to various formats.
format should be one of json, txt, text, md, markdown.
If output is None, returns a unicode representation of the output.
If output is a directory, exports entries into individual files.
Otherwise, exports to the given output file.
"""
maps = {
"json": to_json,
"txt": to_txt,
"text": to_txt,
"md": to_md,
"markdown": to_md
}
if output and os.path.isdir(output): # multiple files
return write_files(journal, output, format)
else:
content = maps[format](journal)
if output:
try:
with open(output, 'w') as f:
f.write(content)
return "[Journal exported to {0}]".format(output)
except IOError as e:
return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
else:
return content
def write_files(journal, path, format):
"""Turns your journal into separate files for each entry.
Format should be either json, md or txt."""
make_filename = lambda entry: e.date.strftime("%C-%m-%d_{0}.{1}".format(slugify(u(e.title)), format))
for e in journal.entries:
full_path = os.path.join(path, make_filename(e))
if format == 'json':
content = json.dumps(e.to_dict(), indent=2) + "\n"
elif format == 'md':
content = e.to_md()
elif format == 'txt':
content = u(e)
with open(full_path, 'w') as f:
f.write(content)
return "[Journal exported individual files in {0}]".format(path)

View file

@ -1,7 +1,8 @@
#!/usr/bin/env python
# encoding: utf-8
import readline, glob
import readline
import glob
import getpass
try: import simplejson as json
except ImportError: import json
@ -25,7 +26,6 @@ default_config = {
},
'editor': "",
'encrypt': False,
'password': "",
'default_hour': 9,
'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M",
@ -35,7 +35,7 @@ default_config = {
}
def update_config(config, config_path=os.path.expanduser("~/.jrnl_conf")):
def upgrade_config(config, config_path=os.path.expanduser("~/.jrnl_conf")):
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
This essentially automatically ports jrnl installations if new config parameters are introduced in later
versions."""
@ -73,18 +73,19 @@ def install_jrnl(config_path='~/.jrnl_config'):
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
if password:
default_config['encrypt'] = True
if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain("default", password)
print("Journal will be encrypted.")
print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.")
else:
password = None
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org or with 'pip install pycrypto' and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
# Use highlighting:
if not module_exists("colorama"):
print("colorama not found. To turn on highlighting, install colorama and set highlight to true in your .jrnl_conf.")
default_config['highlight'] = False
open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there
open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there
# Write config to ~/.jrnl_conf
with open(config_path, 'w') as f:
@ -93,5 +94,3 @@ def install_jrnl(config_path='~/.jrnl_config'):
if password:
config['password'] = password
return config

View file

@ -29,7 +29,8 @@ xdg_config = os.environ.get('XDG_CONFIG_HOME')
CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
PYCRYPTO = install.module_exists("Crypto")
def parse_args():
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"')
@ -38,26 +39,26 @@ def parse_args():
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('-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('-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('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
exporting.add_argument('--json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal')
exporting.add_argument('--markdown', dest='markdown', action="store_true", help='Returns a Markdown-formated version of the Journal')
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('--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")
return parser.parse_args()
return parser.parse_args(args)
def guess_mode(args, config):
"""Guesses the mode (compose, read or export) from the given arguments"""
compose = True
export = False
if args.json or args.decrypt is not False or args.encrypt is not False or args.markdown 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 args.tags or args.delete_last:
compose = False
export = True
elif args.start_date or args.end_date or args.limit or args.strict or args.short:
@ -77,7 +78,7 @@ def get_text_from_editor(config):
raw = f.read()
os.remove(tmpfile)
else:
print('[Nothing saved to file]')
util.prompt('[Nothing saved to file]')
raw = ''
return raw
@ -85,36 +86,40 @@ def get_text_from_editor(config):
def encrypt(journal, filename=None):
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
journal.config['password'] = ""
journal.make_key(prompt="Enter new password:")
password = util.getpass("Enter new password: ")
journal.make_key(password)
journal.config['encrypt'] = True
journal.write(filename)
print("Journal encrypted to {0}.".format(filename or journal.config['journal']))
if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain(journal.name, password)
util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal']))
def decrypt(journal, filename=None):
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
journal.config['encrypt'] = False
journal.config['password'] = ""
journal.write(filename)
print("Journal decrypted to {0}.".format(filename or journal.config['journal']))
util.prompt("Journal decrypted to {0}.".format(filename or journal.config['journal']))
def touch_journal(filename):
"""If filename does not exist, touch the file"""
if not os.path.exists(filename):
print("[Journal created at {0}]".format(filename))
util.prompt("[Journal created at {0}]".format(filename))
open(filename, 'a').close()
def update_config(config, new_config, scope):
def update_config(config, new_config, scope, force_local=False):
"""Updates a config dict with new values - either global if scope is None
of config['journals'][scope] is just a string pointing to a journal file,
or config['journals'][scope] is just a string pointing to a journal file,
or within the scope"""
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
config['journals'][scope].update(new_config)
elif scope and force_local: # Convert to dict
config['journals'][scope] = {"journal": config['journals'][scope]}
config['journals'][scope].update(new_config)
else:
config.update(new_config)
def cli():
def cli(manual_args=None):
if not os.path.exists(CONFIG_PATH):
config = install.install_jrnl(CONFIG_PATH)
else:
@ -122,18 +127,18 @@ def cli():
try:
config = json.load(f)
except ValueError as e:
print("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message))
print("[Entry was NOT added to your journal]")
sys.exit(-1)
install.update_config(config, config_path=CONFIG_PATH)
util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message))
util.prompt("[Entry was NOT added to your journal]")
sys.exit(1)
install.upgrade_config(config, config_path=CONFIG_PATH)
original_config = config.copy()
# check if the configuration is supported by available modules
if config['encrypt'] and not PYCRYPTO:
print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.")
sys.exit(-1)
util.prompt("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.")
sys.exit(1)
args = parse_args()
args = parse_args(manual_args)
# If the first textual argument points to a journal file,
# use this!
@ -141,22 +146,24 @@ def cli():
if journal_name is not 'default':
args.text = args.text[1:]
journal_conf = config['journals'].get(journal_name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config['journal'] = os.path.expanduser(config['journal'])
touch_journal(config['journal'])
mode_compose, mode_export = guess_mode(args, config)
# open journal file or folder
if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \
config['journal'].endswith(".dayone/")):
journal = Journal.DayOne(**config)
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
journal = Journal.DayOne(**config)
else:
util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1)
else:
journal = Journal.Journal(**config)
journal = Journal.Journal(journal_name, **config)
if mode_compose and not args.text:
if config['editor']:
@ -171,54 +178,52 @@ def cli():
# Writing mode
if mode_compose:
raw = " ".join(args.text).strip()
unicode_raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(unicode_raw, args.date)
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
print("[Entry added to {0} journal]".format(journal_name))
util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write()
# Reading mode
elif not mode_export:
else:
journal.filter(tags=args.text,
start_date=args.start_date, end_date=args.end_date,
strict=args.strict,
short=args.short)
journal.limit(args.limit)
print(unicode(journal))
# Reading mode
if not mode_compose and not mode_export:
print(journal.pprint())
# Various export modes
elif args.tags:
print(exporters.to_tag_list(journal))
elif args.json: # export to json
print(exporters.to_json(journal))
elif args.markdown: # export to json
print(exporters.to_md(journal))
elif args.export is not False:
print(exporters.export(journal, args.export, args.output))
elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO:
print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.")
util.prompt("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.")
elif args.encrypt is not False:
encrypt(journal, filename=args.encrypt)
# Not encrypting to a separate file: update config!
if not args.encrypt:
update_config(original_config, {"encrypt": True, "password": ""}, journal_name)
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH)
elif args.decrypt is not False:
decrypt(journal, filename=args.decrypt)
# Not decrypting to a separate file: update config!
if not args.decrypt:
update_config(original_config, {"encrypt": False, "password": ""}, journal_name)
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH)
elif args.delete_last:
last_entry = journal.entries.pop()
print("[Deleted Entry:]")
print(last_entry)
util.prompt("[Deleted Entry:]")
print(last_entry.pprint())
journal.write()
if __name__ == "__main__":
cli()

View file

@ -1,10 +1,90 @@
#!/usr/bin/env python
# encoding: utf-8
import sys
import os
from tzlocal import get_localzone
import getpass as gp
import keyring
import pytz
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
STDIN = sys.stdin
STDERR = sys.stderr
STDOUT = sys.stdout
TEST = False
__cached_tz = None
def getpass(prompt="Password: "):
if not TEST:
return gp.getpass(prompt)
else:
return py23_input(prompt)
def get_password(validator, keychain=None, max_attempts=3):
pwd_from_keychain = keychain and get_keychain(keychain)
password = pwd_from_keychain or getpass()
result = validator(password)
# Password is bad:
if not result and pwd_from_keychain:
set_keychain(keychain, None)
attempt = 1
while not result and attempt < max_attempts:
prompt("Wrong password, try again.")
password = getpass()
result = validator(password)
attempt += 1
if result:
return result
else:
prompt("Extremely wrong password.")
sys.exit(1)
def get_keychain(journal_name):
return keyring.get_password('jrnl', journal_name)
def set_keychain(journal_name, password):
if password is None:
try:
keyring.delete_password('jrnl', journal_name)
except:
pass
elif not TEST:
keyring.set_password('jrnl', journal_name, password)
def u(s):
"""Mock unicode function for python 2 and 3 compatibility."""
return s if PY3 or type(s) is unicode else unicode(s, "unicode_escape")
def prompt(msg):
"""Prints a message to the std err stream defined in util."""
if not msg.endswith("\n"):
msg += "\n"
STDERR.write(u(msg))
def py23_input(msg):
if sys.version_info[0] == 3:
try: return input(msg)
except SyntaxError: return ""
else:
return raw_input(msg)
STDERR.write(u(msg))
return STDIN.readline().strip()
def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
raw = py23_input(prompt)
return {'y': True, 'n': False}.get(raw.lower(), default)
def get_local_timezone():
"""Returns the Olson identifier of the local timezone.
In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X
that prevents that right now: https://github.com/regebro/tzlocal/issues/6"""
global __cached_tz
if not __cached_tz and "darwin" in sys.platform:
__cached_tz = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip()
if not __cached_tz or __cached_tz not in pytz.all_timezones_set:
link = os.readlink("/etc/localtime")
# This is something like /usr/share/zoneinfo/America/Los_Angeles.
# Find second / from right and take the substring
__cached_tz = link[link.rfind('/', 0, link.rfind('/'))+1:]
elif not __cached_tz:
__cached_tz = str(get_localzone())
if not __cached_tz or __cached_tz not in pytz.all_timezones_set:
__cached_tz = "UTC"
return __cached_tz

View file

@ -1,4 +1,8 @@
parsedatetime >= 1.1.2
pytz >= 2013b
colorama >= 0.2.5
pycrypto >= 2.6
argparse==1.2.1
tzlocal == 1.0
slugify==0.0.1
keyring==3.0.5

View file

@ -70,7 +70,11 @@ setup(
packages = ['jrnl'],
install_requires = [
"parsedatetime>=1.1.2",
"colorama>=0.2.5"
"pytz>=2013b",
"tzlocal==1.0",
"slugify>=0.0.1",
"colorama>=0.2.5",
"keyring>=3.0.5"
] + [p for p, cond in conditional_dependencies.items() if cond],
extras_require = {
"encrypted": "pycrypto>=2.6"
@ -89,14 +93,16 @@ setup(
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Topic :: Office/Business :: News/Diary',
'Topic :: Text Processing'
],
# metadata for upload to PyPI
author = "Manuel Ebert",
author_email = "manuel@hey.com",
author_email = "manuel@1450.me",
license = "MIT License",
keywords = "journal todo todo.txt jrnl".split(),
url = "http://maebert.github.com/jrnl",
url = "http://maebert.github.io/jrnl",
)

View file

@ -1,33 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
import unittest
class TestClasses(unittest.TestCase):
"""Test the behavior of the classes.
tests related to the Journal and the Entry Classes which can
be tested withouth command-line interaction
"""
def setUp(self):
pass
def test_colon_in_textbody(self):
"""colons should not cause problems in the text body"""
pass
class TestCLI(unittest.TestCase):
"""test the command-line interaction part of the program"""
def setUp(self):
pass
def test_something(self):
"""first test"""
pass
if __name__ == '__main__':
unittest.main()