mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-13 18:08:30 +02:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
7233507ca0
40 changed files with 1102 additions and 171 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,4 +21,5 @@ lib64
|
|||
# Installer logs
|
||||
pip-log.txt
|
||||
.DS_Store
|
||||
.travis-solo
|
||||
|
||||
|
|
10
.travis.yml
10
.travis.yml
|
@ -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
|
||||
|
|
64
CHANGELOG.md
64
CHANGELOG.md
|
@ -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.
|
||||
|
|
6
LICENSE
6
LICENSE
|
@ -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
|
||||
|
|
54
README.md
54
README.md
|
@ -1,6 +1,8 @@
|
|||
jrnl [](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.
|
||||
|
||||
|
||||
|
||||
[](https://bitdeli.com/free "Bitdeli Badge")
|
||||
|
||||
|
|
36
features/core.feature
Normal file
36
features/core.feature
Normal 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."
|
14
features/data/configs/basic.json
Normal file
14
features/data/configs/basic.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/dayone.json
Normal file
14
features/data/configs/dayone.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/empty_folder.json
Normal file
14
features/data/configs/empty_folder.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/encrypted.json
Normal file
14
features/data/configs/encrypted.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/encrypted_with_pw.json
Normal file
14
features/data/configs/encrypted_with_pw.json
Normal 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": "@"
|
||||
}
|
17
features/data/configs/multiple.json
Normal file
17
features/data/configs/multiple.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/tags.json
Normal file
14
features/data/configs/tags.json
Normal 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": "@"
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
1
features/data/journals/empty_folder/empty.txt
Normal file
1
features/data/journals/empty_folder/empty.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Nothing to see here
|
BIN
features/data/journals/encrypted.journal
Normal file
BIN
features/data/journals/encrypted.journal
Normal file
Binary file not shown.
5
features/data/journals/simple.journal
Normal file
5
features/data/journals/simple.journal
Normal 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.
|
7
features/data/journals/tags.journal
Normal file
7
features/data/journals/tags.journal
Normal 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.
|
0
features/data/journals/work.journal
Normal file
0
features/data/journals/work.journal
Normal file
48
features/dayone.feature
Normal file
48
features/dayone.feature
Normal 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
|
||||
"""
|
37
features/encryption.feature
Normal file
37
features/encryption.feature
Normal 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
42
features/environment.py
Normal 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)
|
22
features/exporting.feature
Normal file
22
features/exporting.feature
Normal 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"
|
||||
|
36
features/multiple_journals.feature
Normal file
36
features/multiple_journals.feature
Normal 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
|
15
features/regression.feature
Normal file
15
features/regression.feature
Normal 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
187
features/steps/core.py
Normal 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
22
features/tagging.feature
Normal 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
|
||||
"""
|
|
@ -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 {
|
||||
|
|
107
jrnl/Journal.py
107
jrnl/Journal.py
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
107
jrnl/jrnl.py
107
jrnl/jrnl.py
|
@ -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()
|
||||
|
||||
|
|
90
jrnl/util.py
90
jrnl/util.py
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
12
setup.py
12
setup.py
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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()
|
Loading…
Add table
Reference in a new issue