Compare commits

...

644 commits

Author SHA1 Message Date
MinchinWeb
5dc724dd4d Don't break when system timezone is UTC 2017-11-13 20:20:11 -07:00
MinchinWeb
779a8f4ddf [Travis] Add newest versions of Python 2017-11-13 20:09:34 -07:00
MinchinWeb
9441998ecf Close file handle before passing edit's tempfile to editor.
PR #416
2017-11-13 17:57:54 -07:00
MinchinWeb
126e188c0f Make jrnl not depend on itself 2017-11-13 17:44:43 -07:00
MinchinWeb
89eba3e274 [Prjct Exporter] remove tag only lines 2017-11-13 16:23:18 -07:00
MinchinWeb
1c244da79b Add 'prjct' exporter. 2017-11-13 16:23:18 -07:00
MinchinWeb
b84bc9b856 [Export] remove extra spaces from Markdown, YAML exports 2017-11-13 16:21:44 -07:00
MinchinWeb
14ef6e3dcb [Travis-CI] Python 3.5 has been released 2017-11-13 16:21:44 -07:00
MinchinWeb
b11cbd9e6f [Dayone] fix typo 2017-11-13 16:21:44 -07:00
MinchinWeb
5a98089522 [Dayone] maintain metadata on edited entries;
Fixes #358, See also #159
2017-11-13 16:21:44 -07:00
MinchinWeb
43df14f18c [Dayone] reverse fd46ffea23
Brings back extended Dayone attributes
2017-11-13 16:21:44 -07:00
MinchinWeb
8721ff819f [Dayone] maintain existing tags stored in Dayone metadata 2017-11-13 16:21:44 -07:00
MinchinWeb
ccaca2d135 [Upgrade] remove unused string parameter 2017-11-13 16:19:16 -07:00
MinchinWeb
a31eb9628a [YAML Export] style improvements 2017-11-13 16:19:16 -07:00
MinchinWeb
cccdc157fb [Dayone] remove extra spaces from the titles of edited DayOne entries
Otherwise, a leading space was being introduced
2017-11-13 16:19:16 -07:00
Radomír Bosák
8615679beb Remove --use-mirrors switch from travis setup (#477)
The --use-mirrors pip switch does not work since pip 7.0.0. This caused
travis builds to fail.

This commit removes the --use-mirrors switch.
2017-03-10 09:47:43 -08:00
Radomír Bosák
a95859a292 Catch keyboard interrupt when opening journal (#476)
If the journal is encrypted, jrnl asks for a password using the
getpass.getpass function. If user presses the keyboard interrupt (e.g.
ctrl+c) during the password prompt, jrnl crashes and prints a stack
trace. This however is not very user friendly.

This commit adds a check for KeyboardInterrupt Exception and prints a
message whenever this exception occurs and exits. Stack is no longer
printed.

Fixes #450
2017-03-10 08:54:02 -08:00
Radomír Bosák
d65a9779ca Fix python2 error in template_explorer.py (#471)
If template_explorer.py was imported in python2, it would fail because
it uses unicode_literals which would ultimately cause passing an unicode
string as the first argument to the 'type' built-in function.

This commit fixes this by converting the unicode to string before it is
passed to the 'type' function.

Fixes #456
2017-03-09 11:59:03 -08:00
Radomír Bosák
6d7bd18d52 Fix behave tests by adding asteval dependency (#469) 2017-03-09 11:56:44 -08:00
Manuel Ebert
2031902f1c Clean up 2016-08-19 23:23:14 +00:00
Manuel Ebert
4b3dc38e05 Template exporting FTW 2016-08-19 23:20:31 +00:00
Manuel Ebert
ce113af4e0 Fix JSON export 2016-08-19 21:44:12 +00:00
Manuel Ebert
dab853555e Indent character optional 2016-08-19 21:44:01 +00:00
Manuel Ebert
b3f5c092de Fix Keyring tests 2016-08-19 21:43:27 +00:00
Manuel Ebert
49a374e552 Upgrade to cryptography 1.4 2016-08-17 13:41:54 +00:00
Manuel Ebert
7776b3d24c Update Cryptography
Fixes SSL issues described in
https://github.com/pyca/cryptography/issues/2750
2016-08-16 11:00:08 +02:00
Manuel Ebert
8b64cc95d9 Exports 2016-07-17 13:35:35 -07:00
Philip Douglass
3580154f7d User defined indent character (#419)
Make indent character configurable.
2016-04-15 23:00:49 -07:00
Manuel Ebert
5921cd63f8 Merge pull request #417 from philipsd6/2.0-strip_entry_body
Fully strip title and body after SENTENCE_SPLITTER search
2016-04-07 13:15:12 -07:00
Manuel Ebert
37f6e3c8b4 Merge pull request #420 from philipsd6/2.0-better_textwrap
Clean up text wrapping once and for all --- Please!
2016-04-07 13:13:54 -07:00
Philip Douglass
1ac5444051 Fully strip title and body after SENTENCE_SPLITTER search 2016-04-07 14:31:16 -04:00
Philip Douglass
a4783109b3 Clean up text wrapping once and for all 2016-04-07 14:29:28 -04:00
Manuel Ebert
4a871aff32 Merge pull request #399 from philipsd6/2.0-upgrade_unicode_and_tildes
Support unicode and userdir expansion in config during 2.0 upgrade
2016-02-10 10:41:06 -08:00
Manuel Ebert
e9d856b039 Update dependencies and lock versions on keyring and cryptography. Fixes #407 2016-02-10 10:24:52 -08:00
Philip Douglass
91cf1c911f Open config_path as utf-8 2016-01-24 21:16:50 -05:00
Philip Douglass
1aef13d757 Expand vars and user tilde for upgrade backup 2016-01-24 21:09:17 -05:00
Manuel Ebert
659502adf5 Docs on Alfred integration
Fixes #364
2016-01-11 12:17:59 -08:00
Manuel Ebert
155be26840 Doc update on HISTIGNORE
Fixes #394
2016-01-11 12:11:56 -08:00
Manuel Ebert
06e3f0c101 Byte encoding for getpass
Fixes #392 and #393
2016-01-11 12:08:05 -08:00
Manuel Ebert
7a0e056130 Unicode literals in util 2015-12-28 22:07:26 -08:00
Manuel Ebert
38682b922f Remove references to jrnl[encrypted] 2015-12-28 21:56:08 -08:00
Manuel Ebert
5805cef008 Drop whitespace in print
Fixes #222 and #376
2015-12-28 21:53:23 -08:00
flight16
06a9ed2026 Merged PR 2015-12-28 21:36:17 -08:00
Manuel Ebert
79fa9c5434 Seperation of title and entry is now purely virtual.
Fixes #360
2015-12-28 21:24:39 -08:00
Manuel Ebert
76bc803776 --export text doesn't indent
Fixes #373
2015-12-28 13:06:26 -08:00
Manuel Ebert
168f63c657 Fix XML export 2015-12-28 13:01:33 -08:00
Manuel Ebert
d0f86d398f Update tests for templates 2015-07-01 22:23:13 -07:00
Manuel Ebert
dc39a25d5c Support for templates 2015-07-01 22:23:13 -07:00
Manuel Ebert
8a6563d958 Merge pull request #349 from MinchinWeb/2.0-yaml-export-fixes
2.0 -- yaml export fixes, dayone fixes
2015-05-06 08:56:03 +02:00
MinchinWeb
549f8501d6 [CLI] display instructions to enter an entry on the command line when no editor is set 2015-05-05 15:30:18 -06:00
MinchinWeb
78bae0476c Colorize error and warning messages 2015-05-05 15:12:02 -06:00
MinchinWeb
c8b8fab684 [Dayone] clean up date code 2015-05-05 14:25:53 -06:00
MinchinWeb
fd46ffea23 [Dayone] cut extended attributes for now 2015-05-05 13:54:38 -06:00
MinchinWeb
e4249bb00c [Dayone] allow variable length dates on edited Dayone entries 2015-05-05 13:21:46 -06:00
MinchinWeb
4c7d37a153 [Dayone] fix reading dates off edited dayone entries 2015-05-05 13:07:25 -06:00
MinchinWeb
281b74da74 Fixes #348
Also fixes #322 on windows.
2015-05-05 11:58:15 -06:00
MinchinWeb
6444b6b137 code formatting 2015-05-04 12:08:05 -06:00
MinchinWeb
f7cadfc9dd [Dayone] Add Creator section to dayone import and YAML export 2015-05-04 00:21:33 -06:00
MinchinWeb
72cbafac55 [Util] delete old, unused code 2015-05-03 22:12:10 -06:00
MinchinWeb
2141bcacd4 [CLI] colour code 'no editor' error message 2015-05-03 21:59:20 -06:00
MinchinWeb
06dbe2fb30 [YAML export] remove lines that simply list tags 2015-05-03 21:36:16 -06:00
MinchinWeb
586ce47c68 [YAML export] only increase headings by one level 2015-05-03 19:56:16 -06:00
Manuel Ebert
ea6060d75c Fix that goddamn timezone test 2015-04-21 16:34:27 +02:00
Manuel Ebert
bedab7c121 Fix for #332 2015-04-21 15:57:44 +02:00
Manuel Ebert
eb0af8f47c Fix for #322 2015-04-21 15:49:27 +02:00
Manuel Ebert
924037e2b0 Merge branch 'pr/326' into 2.0-rc1 2015-04-20 14:59:47 +02:00
Gregory Crosswhite
70c1f8c2ed Now the list of import types in --help is generated from the plugins. 2015-04-20 14:57:33 +02:00
Gregory Crosswhite
61f1d866b3 Add jrnl.plugins to the list of installed packages.
Without this the installed jrnl will not run.

Conflicts:
	setup.py
2015-04-20 14:56:18 +02:00
Josh Davis
308fe0d478 Add documentation on advanced editor config 2015-04-20 14:45:24 +02:00
Josh Davis
61cc792ad7 Allow editor config value to be either list or str 2015-04-20 14:45:15 +02:00
Manuel Ebert
5a3e3320a7 Merge pull request #346 from ametheus/patch-2
Fix improper charset handling in PY2 path of u(x)
2015-04-20 14:42:41 +02:00
Manuel Ebert
e88fa16bbc Remove ; 2015-04-20 14:36:53 +02:00
MinchinWeb
d17558f3c6 Fix weird character at the front of the line... 2015-04-20 14:36:35 +02:00
MinchinWeb
425ecf791d Name file handle if we're going to us it
Conflicts:
	jrnl/util.py
2015-04-20 14:36:12 +02:00
Craig Moyer
ee782cc5c5 Close temp file before deleting it. 2015-04-20 14:35:31 +02:00
Manuel Ebert
94d6ebeb2d Merge pull request #347 from MinchinWeb/2.0-rc1-markdown-export-fix
2.0 rc1 markdown export fix
2015-04-20 14:29:17 +02:00
Manuel Ebert
4b70709248 Warn user if attempting to write to default journal but not configured
Fixes #343
2015-04-20 14:25:44 +02:00
Manuel Ebert
978d735351 Tests for #343 2015-04-20 14:25:20 +02:00
Manuel Ebert
57ef796ccb Merge branch 'pr/342' into 2.0-rc1 2015-04-20 14:15:57 +02:00
Manuel Ebert
10eb36076f Consolidate requirements 2015-04-20 14:15:48 +02:00
Manuel Ebert
08fbd0d960 Split tests 2015-04-20 14:13:34 +02:00
Manuel Ebert
bdfe588a01 Fix test arg parsing 2015-04-20 14:12:34 +02:00
MinchinWeb
a2c75e180c Include plugins in eggs 2015-04-19 23:19:35 -06:00
MinchinWeb
162e33089e grr...fix typo 2015-04-14 16:55:06 -06:00
MinchinWeb
7d23945df7 Make the tests pass 2015-04-14 16:49:49 -06:00
MinchinWeb
2caaa039b1 Merge branch '2.0-rc1-markdown-export-fix' of https://github.com/MinchinWeb/jrnl into 2.0-rc1-markdown-export-fix 2015-04-14 16:06:57 -06:00
MinchinWeb
0a730f2a94 Update .gitignore 2015-04-14 15:58:56 -06:00
MinchinWeb
087b45f0bf Check for heading level error 2015-04-14 15:58:55 -06:00
MinchinWeb
dbfc61b437 Check for heading level error 2015-04-14 15:52:18 -06:00
MinchinWeb
f182dae6eb Add YAML export 2015-04-14 15:49:58 -06:00
MinchinWeb
2d49aaa1a0 Add testing for increasing Headings on Markdown export 2015-04-14 15:13:00 -06:00
MinchinWeb
442e27a5d1 Don't relay on colorama for ANSI color codes 2015-04-14 14:07:08 -06:00
MinchinWeb
8e44ce0845 Warn if increasing headings goes past H6
Only warn once per entry.
2015-04-14 13:45:50 -06:00
MinchinWeb
c10a4d4fed Increase heading levels on Markdown export 2015-04-14 13:28:03 -06:00
MinchinWeb
429c8283e0 Change heading levels on Markdown export depending on if it is to a consolidated file or to individual files. 2015-04-14 12:25:22 -06:00
MinchinWeb
f1641d8084 Removes leading 'b' on slugs in Python 3 2015-04-14 12:04:36 -06:00
MinchinWeb
141eb2a647 Update Tag exporter code documentation. 2015-04-14 11:59:15 -06:00
MinchinWeb
bbbc2de1b4 Update Markdown exporter documentation. 2015-04-14 11:38:24 -06:00
MinchinWeb
3b85121f51 Unable to run cli.py as a standalone script due to relative imports 2015-04-14 11:07:56 -06:00
MinchinWeb
873356b623 Don't pin to a specific version of cryptography 2015-04-14 10:41:28 -06:00
Thijs van Dijk
c68a60f6dc Fix improper charset handling in PY2 path of u(x)
Knowing fully that I may have just added another layer of impropriety, the above line fixed the charset errors I was getting.

I'll illustrate this change with an example string `Charšet`, entered (e.g. through stdin) in UTF-8 encoding.
To the best of my knowledge, the previous version would first have encoded this string to `Char\xc5\xa1et` (i.e., it encoded each byte not in ascii range as a hex escape code), and then have parsed this string to `Charšet` (i.e. after "r" it sees Unicode code point U+00c5 and U+00a1).

My version simply takes this str for what it is: an UTF-8 representation of the unicode string `Charšet`.
2015-04-14 11:52:10 +02:00
flight16
35b9827eb1 Fix breaking test submitted in b2a7d1f30e. 2015-04-13 23:23:54 +09:00
flight16
b2a7d1f30e Add single letter alias -s for option --short
--short was being used in a test for date logic, but I added a second
test for explicitness.
2015-04-13 09:08:43 +09:00
flight16
00a5657c1b Fix: dateutil conflict because python-dateutils was already added to the conditional section. 2015-04-13 09:01:10 +09:00
flight16
6bcd5be7d4 Merge remote-tracking branch 'maebert/2.0-rc1' into 2.0-rc1
Conflicts:
	setup.py
2015-04-13 08:46:43 +09:00
flight16
41bec199a2 Add missing dependencies pyyaml and pyxdg 2015-04-11 13:03:15 +09:00
Manuel Ebert
8e627b01c0 TXT extension for temp files 2015-04-05 23:55:04 +02:00
Manuel Ebert
b41b5d3376 Tests for parsing issue fix 2015-04-05 18:38:37 +02:00
Manuel Ebert
cca630059f Fix parsing issue 2015-04-05 18:38:30 +02:00
Manuel Ebert
c720c15787 More encoding madness 2015-04-05 06:38:38 +04:00
Manuel Ebert
109a11d18e Encryption reads and writes in binary mode 2015-04-05 06:14:18 +04:00
Manuel Ebert
94964dc31c Python 3 fixes :) 2015-04-05 06:05:46 +04:00
Manuel Ebert
b7d8e1a9a5 Bilingual meta classes 2015-04-05 05:50:30 +04:00
Manuel Ebert
f459dd5bc2 Tests for #333 2015-04-05 03:27:09 +04:00
Manuel Ebert
1543a93799 Export DayOne UUID in json
Fixes #333
2015-04-05 03:26:52 +04:00
Manuel Ebert
812d289740 Update all tests to new time format 2015-04-05 03:25:39 +04:00
Manuel Ebert
4010d38af2 Fix creating non-existent journals 2015-04-05 03:25:17 +04:00
Manuel Ebert
7dc2ccbd3a Cleanup and more logging 2015-04-05 02:28:04 +04:00
Manuel Ebert
367b13b849 Fix conflicting requirements
Fixes #296
2015-04-05 02:27:47 +04:00
Manuel Ebert
24ada92787 Upgrade old time stamps to fix #317 2015-04-05 02:23:10 +04:00
Manuel Ebert
9c7d5af152 Legacy Journal with old timestamps 2015-04-05 02:23:09 +04:00
Manuel Ebert
f68ca1221e Cuddle timestamp in brackets to fix #318 2015-04-05 02:22:49 +04:00
Manuel Ebert
c31e62d9d6 Introduce legacy classes 2015-04-05 07:25:22 +10:00
Manuel Ebert
75ecc0dbbd Properly initialise journal files after creating them 2015-04-04 18:58:33 +11:00
Manuel Ebert
4955902702 Return config after install
Fixes #297
2015-04-04 18:47:09 +11:00
Manuel Ebert
b90a4b16bb Make backup of config when upgrading
Fixes #307
2015-04-04 18:36:34 +11:00
Manuel Ebert
dd79639095 Hardcoded salt to fix crypto 2015-04-04 17:50:44 +11:00
Manuel Ebert
003fb507ae Merge pull request #306 from pcarranza/2.0-r1-logging
Added basic logging feature to understand how is configuration loaded
2014-11-10 11:53:14 +01:00
Pablo Carranza
cc22fd8e60 Added basic logging feature to understand how is configuration loaded
Conflicts:

	jrnl/cli.py
	jrnl/util.py
2014-11-09 15:55:52 -05:00
Manuel Ebert
6b8cbe52b5 Merge pull request #300 from gcgoogle/add-import-plugin-support
Added support for importing entries for 2.0rc1.
2014-10-30 15:03:47 +01:00
Manuel Ebert
1771ff4e27 Merge pull request #299 from gcgoogle/fix-hard-coded-export-type-list
Now the list of export types in --help is generated from the plugins.
2014-10-30 15:01:32 +01:00
Gregory Crosswhite
cfd8856989 Now the importer reads from standard input using util.py23_read(). 2014-10-29 13:02:11 -07:00
Gregory Crosswhite
5685952a73 Made Journal.import_ more concise. 2014-10-29 13:02:05 -07:00
Gregory Crosswhite
584f0a969e Added support for importing entries. 2014-10-24 15:21:10 -07:00
Gregory Crosswhite
9e766d7ad0 Now the list of export types in --help is generated from the plugins. 2014-10-24 14:56:24 -07:00
Manuel Ebert
5e68b97e59 Plugin architecture 2014-09-27 15:07:22 -07:00
Manuel Ebert
359eabd56d Work on cryptography 2014-09-27 13:15:46 -07:00
Manuel Ebert
be009b08b4 Converts tests to YAML 2014-09-12 15:28:12 -07:00
Manuel Ebert
79f87abd76 Use 'version' in configs 2014-09-12 15:27:48 -07:00
Manuel Ebert
c5c6676bab Uses PBKDF2-SHA256 instead of plain SHA256
Fixes #192
2014-09-12 14:28:37 -07:00
Manuel Ebert
dc03c47da8 Upgrade prompt for encrypted jrnls 2014-09-12 14:28:03 -07:00
Manuel Ebert
0a28a9807b Doc changes 2014-09-12 14:28:03 -07:00
Manuel Ebert
84d886d0ef Basic support for cryptography.io
Still needs better hash function
2014-09-12 14:28:03 -07:00
Manuel Ebert
4ea5ae80b5 Upgrade scaffolding 2014-09-12 14:28:03 -07:00
Manuel Ebert
293204040b Remove deprecated tests 2014-09-12 14:28:03 -07:00
Manuel Ebert
9651adcdd0 Deprecate config.password 2014-09-12 14:28:03 -07:00
Matthias Vogelgesang
044accff6d Fix failing behave tests 2014-09-12 14:27:34 -07:00
Matthias Vogelgesang
84002574e6 Remove circular dependency 2014-09-12 14:26:23 -07:00
Matthias Vogelgesang
37d3f93b8a Move and remove imports in utils 2014-09-12 14:26:22 -07:00
Matthias Vogelgesang
953c18dff2 Make Journal a general interface
This change moves the loading and saving mechanisms into its own
Plain- and EncryptedJournal subclasses for easier maintenance and lazy loading
of all the crypto modules.
2014-09-12 14:26:22 -07:00
yarko
4f05c2bc07 fix call to entry_to_md() 2014-09-12 14:22:45 -07:00
Manuel Ebert
37ebc473c8 Fix config writing for python 3 2014-09-12 14:22:45 -07:00
Manuel Ebert
cbab596b43 Fixes config tests 2014-09-12 14:22:45 -07:00
Manuel Ebert
9c16ad4577 YAML configs
See #184, #230, #197
2014-09-12 14:22:30 -07:00
Manuel Ebert
bdcdce343f Docs 2014-09-12 14:22:15 -07:00
Manuel Ebert
487c902d37 Cleans up entry 2014-09-12 14:22:15 -07:00
Manuel Ebert
99c2884343 Fixes unicode errors in XML export 2014-09-12 14:21:48 -07:00
Manuel Ebert
76710d06f7 Better unicode mock support 2014-09-12 14:21:48 -07:00
B Krishna Chaitanya
dbb3fe40df Export to xml 2014-09-12 14:21:48 -07:00
Matthias Vogelgesang
b77a579ab5 Use PyXDG to save config and journal in XDG dirs
This change also hides the CONFIG_PATH state information entirely within
the install module. Therefore, the cli module does not have to care about
checking existence of files and paths.
2014-09-12 14:20:05 -07:00
Matthias Vogelgesang
f5a44487c1 Require config arguments 2014-09-12 14:18:52 -07:00
Manuel Ebert
411b9eb844 Version bump 2014-09-02 13:27:16 -07:00
Manuel Ebert
391269335f Multi-Word tags for DayOne Journals
Closes #274
2014-09-02 13:27:11 -07:00
Manuel Ebert
182e51b92d Use unicode literals
Fixes #273
2014-09-02 13:26:50 -07:00
Manuel Ebert
050eea7560 Merge pull request #261 from kneufeld/master
using a common tag regex for searching and highlighting
2014-09-02 13:07:33 -07:00
Kurt Neufeld
6d783a80ac using a common tag regex for searching and highlighting 2014-08-30 10:02:59 -06:00
Manuel Ebert
a43ebd7b08 Version bump 2014-08-12 10:11:18 +02:00
Manuel Ebert
4390e1f10c Sort journal entries after editing
Fixes #269
2014-08-12 10:10:15 +02:00
Manuel Ebert
ccd4cca9e6 Merge pull request #267 from maebert/issue-265
Tags at the beginning of entries
2014-08-07 13:31:25 +02:00
Manuel Ebert
0d981241ac typo 2014-08-07 13:27:48 +02:00
Manuel Ebert
b851941574 Version bump 2014-08-07 13:26:10 +02:00
Manuel Ebert
2780a0d9df Tags at the beginning of lines 2014-08-07 13:25:50 +02:00
Manuel Ebert
bf49f31d22 Manifest update 2014-07-27 12:43:26 +09:00
Manuel Ebert
2150b5b3e7 Merge pull request #252 from mjhoffman65/issue237
There should be a whitespace character in front of a tag.  Fixes issue #...
2014-07-27 12:32:30 +09:00
U-NA\cle1413
4d6cdc835e Add assertion to scenario that verifies a tag on a new line is recoginized as a tag. 2014-07-23 11:19:47 -04:00
Matt Hoffman
b8a3b6d9ad There should be a whitespace character in front of a tag. Fixes issue #237. 2014-07-22 22:14:26 -04:00
Manuel Ebert
112f626d09 Merge pull request #248 from maebert/1-9-1
Fix parsing of  future timestmpas
2014-07-21 17:50:06 +09:00
Manuel Ebert
8d1f5fe862 Version bump 2014-07-21 17:33:44 +09:00
Manuel Ebert
4b4d29bf08 Regression test for future timestamps 2014-07-21 17:33:39 +09:00
Manuel Ebert
929896e53e Don't correct future timestamps if year is present in input 2014-07-21 17:33:30 +09:00
Manuel Ebert
340c510a8b Docs update
Closes #176
2014-07-21 14:58:01 +09:00
Manuel Ebert
30c30812a9 Merge pull request #247 from maebert/1-9-0
Greatly improved date parsing
2014-07-21 14:52:10 +09:00
Manuel Ebert
6837c46351 Conditional dateutil for python 3 2014-07-21 14:22:16 +09:00
Manuel Ebert
7f9cd675fe Fix bug when omitting day in parsing 2014-07-21 14:22:07 +09:00
Manuel Ebert
22b5119cb0 Version bump 2014-07-21 13:30:39 +09:00
Manuel Ebert
41d093cc19 Tests for date parsing and -on 2014-07-21 13:30:36 +09:00
Manuel Ebert
2d46b0250b Introduces -on option
Fixes #246
2014-07-21 13:30:25 +09:00
Manuel Ebert
49f67df064 Improves dateutil parsing
Closes #133
Fixes #183, #185, #228
2014-07-21 13:30:10 +09:00
Manuel Ebert
f604862488 Merge pull request #239 from StevenMaude/patch-1
Minor typo in installation.rst
2014-07-17 10:42:21 +09:00
Steven Maude
811167704b Minor typo in installation.rst
Fix "PyCyrypto" to "PyCrypto".
2014-07-12 00:06:11 +01:00
Manuel Ebert
ad2d278ae5 Merge pull request #232 from grplyler/fromfix
jrnl -from and -to times do not include the time themselves
2014-07-03 09:13:51 +02:00
Manuel Ebert
fd529a485c Version bump to 1.8.7 2014-07-03 09:13:07 +02:00
Ryan Plyler
85826ca77b 'jrnl -from' times now match greater/less than or equal to start and end times 2014-07-03 09:11:29 +02:00
Ryan Plyler
f3871aab02 Added PyCharm project files to .gitignore 2014-07-03 09:11:29 +02:00
Ryan Plyler
6f4ab856d3 'jrnl -from' times now match greater/less than or equal to start and end times 2014-07-02 21:53:28 +08:00
Ryan Plyler
0618082aae Merge branch 'master' of https://github.com/maebert/jrnl into fromfix 2014-07-02 21:36:49 +08:00
Manuel Ebert
53e947ac18 Version bump 2014-07-02 15:36:04 +02:00
Manuel Ebert
c05fdfdad7 Tests for tagging 2014-07-02 15:35:05 +02:00
Ryan Plyler
4df3571b47 Added PyCharm project files to .gitignore 2014-07-02 21:34:31 +08:00
Manuel Ebert
919b9ae3e4 Merge pull request #217 from chaitan94/tag-regex
Changed Tag regex to accept special characters
2014-07-02 15:29:02 +02:00
Manuel Ebert
8be672a1d9 Merge pull request #226 from grplyler/master
Fixed typo in CONTRIBUTING.md
2014-06-30 21:11:47 +02:00
Manuel Ebert
15b7d2603c Notes on encryption 2014-06-30 18:00:38 +02:00
grplyler
9871221fb2 Fixed typo in CONTRIBUTING.md
Changed "sooner or later get implement it" to
"sooner or later get implemented."
2014-06-30 18:39:06 +08:00
Manuel Ebert
2332fca6fe New make options 2014-06-30 11:14:25 +02:00
B Krishna Chaitanya
a2be5d25fc Unescaped -, + and * in regex 2014-06-30 14:38:00 +05:30
Manuel Ebert
3a65ce227d Version bump & new recipes 2014-06-30 10:26:54 +02:00
Manuel Ebert
07f2e2be34 Merge pull request #225 from jdevera/patch-1
Include the year in exported file name
2014-06-30 10:06:02 +02:00
Jacobo de Vera
b423bfc963 Include the year in exported file name 2014-06-30 07:01:33 +02:00
Manuel Ebert
3f5b5ff3c3 Restrict --export argument
Fixes #187
2014-06-28 17:55:10 +02:00
B Krishna Chaitanya
8b45458f90 Include [-+/*#] in tag characters 2014-06-27 21:12:42 +05:30
Manuel Ebert
8314c85463 Merge pull request #218 from maebert/readline
Installs readline on non-windows if not available
2014-06-27 16:08:25 +02:00
Manuel Ebert
f5a3d7328c Installs readline on non-windows if not available
Fixes #208
2014-06-27 15:56:12 +02:00
Manuel Ebert
fa243065d9 Merge branch 'pr/213' 2014-06-27 14:11:54 +02:00
Manuel Ebert
b4944c808d Link to advanced usage 2014-06-27 14:11:42 +02:00
Manuel Ebert
a593ed80b9 Merge branch 'pr/198' 2014-06-27 14:01:53 +02:00
Jonathon Scanes
3f4b873ec9 Printed message aborted on ctrl+c. 2014-06-27 14:01:16 +02:00
Jonathon Scanes
948d031777 Prevent printing stack trace in compose mode on ctrl+c. 2014-06-27 14:00:20 +02:00
B Krishna Chaitanya
f9af744a29 Changed regex to accept symbols 2014-06-27 11:58:59 +05:30
karl
6f8755ab55 Note for editing constraints on one line. FIX #199 2014-06-27 09:12:04 +09:00
Manuel Ebert
8eeba1481d Version bump 2014-06-26 15:30:39 +02:00
Manuel Ebert
fe41e41a6f Merge pull request #204 from chrissexton/parallel_edits
Allow parallel edits
2014-06-26 15:19:38 +02:00
Manuel Ebert
828ea4d427 Fixes error when exporting txt files
Fixes #202
2014-06-26 15:03:27 +02:00
Manuel Ebert
872cab6cb4 Allow 'text' and 'markdown' aliases in export to dir
Fixes #201
2014-06-26 15:00:38 +02:00
Chris Sexton
9276a92a0b Allow parallel edits
* Wait to open the journal until after getting text
* Use mktemp so that editors like vim get separate files for editing
2014-06-26 08:29:42 -04:00
Manuel Ebert
b8b928f277 Dynamically load 3L library 2014-06-25 16:40:27 +02:00
Manuel Ebert
cabaa6a832 Merge pull request #188 from igniteflow/master
Add export options to help message
2014-06-25 12:39:59 +02:00
Phil Tysoe
9878b6e350 Add export options to help message 2014-06-25 10:24:39 +01:00
Manuel Ebert
6b2df88d29 Move 3L into vendors dir 2014-06-25 08:31:52 +02:00
Manuel Ebert
38ebf7920f Merge pull request #181 from jenmontes/patch-1
Typo fix
2014-06-25 08:29:14 +02:00
Manuel Ebert
466a9d1f84 Merge pull request #180 from mason-bially/master
Update recipies.rst with correct notepad++ options.
2014-06-25 08:28:25 +02:00
Jen Montes
2a0d9333fc Typo fix
A quick spelling correction and I swapped the position of 2 words for clarity.
2014-06-24 22:03:30 -04:00
Mason Bially
7e80071088 Update recipies.rst with correct notepad++ options. 2014-06-24 17:43:33 -07:00
Manuel Ebert
c95f8b5a1f Merge pull request #178 from matze/use-env-editor
Try $VISUAL and $EDITOR for config['editor']
2014-06-25 00:49:02 +02:00
Manuel Ebert
cfefb287db Merge pull request #175 from jonjonsonjr/master
Fix terminal carousel on landing page of docs
2014-06-25 00:47:48 +02:00
Matthias Vogelgesang
562f700615 Try $VISUAL and $EDITOR for config['editor']
On UNIX derivates they expand to the default or preferred editor and avoid
unnecessary first time edits of the config file.
2014-06-24 22:52:04 +02:00
Jon Johnson
32203f95d8 Fix terminal carousel on landing page of docs
This isn't perfect, but it is a step in the right direction. This fixes
the prev button not working at all and the next button having some
undesirable behavior.

This seems to work for the most part, but I noticed that if I leave this
open and go to a different tab in my browser, there's some strange
behavior when I come back to the landing page: there are multiple
outputs playing at once. This is similar to the current behavior, where
clicking the next button multiple times does the same thing.
2014-06-24 14:04:22 -04:00
Manuel Ebert
57b272febf Typo 2014-06-24 18:42:45 +02:00
Manuel Ebert
9b2f05c9d7 Clarifications on writing docs 2014-06-24 18:41:49 +02:00
Manuel Ebert
e60ff1faf6 Merge pull request #167 from runiq/env-vars-in-config-paths
Allow use of environment variables in config paths
2014-06-24 18:17:54 +02:00
Patrice Peterson
3639e00183 Allow use of environment variables in config paths
This patch simply adds several calls to `os.expandvars` to functions
that parse paths in the config file.
2014-06-23 16:12:41 +02:00
Manuel Ebert
632d6990ec Update README.md 2014-06-05 16:01:18 +02:00
Manuel Ebert
db5bb6366c Update README.md 2014-06-05 16:01:07 +02:00
Manuel Ebert
98a9d53217 Update README.md 2014-06-05 15:59:12 +02:00
Manuel Ebert
97f19614ce Update CONTRIBUTING.md 2014-05-22 14:19:24 -07:00
Manuel Ebert
8a4401b670 Mark jrnl as 3.4 compatible 2014-05-22 14:18:15 -07:00
Manuel Ebert
e889e7d7a3 Merge pull request #164 from maebert/1.8
Official support for python 3.4
2014-05-22 13:49:31 -07:00
Manuel Ebert
7b2c47cbbe Catch proper exceptions in python3.4 2014-05-22 13:24:19 -07:00
Manuel Ebert
378baf17a1 Fixes xml header 2014-05-22 13:24:04 -07:00
Manuel Ebert
73ee4054d6 add six to requirements 2014-05-22 12:18:00 -07:00
Manuel Ebert
db99510720 Split DayOne into separate file 2014-05-22 12:17:54 -07:00
Manuel Ebert
a0333e60d5 Fix encoding in tests 2014-05-22 12:16:26 -07:00
Manuel Ebert
6953e340c4 Official support for python 3.4 2014-05-22 11:56:28 -07:00
Manuel Ebert
6a60cac4b5 Merge pull request #163 from maebert/fix-162
Fix 162
2014-05-19 14:47:44 -07:00
Manuel Ebert
c2692d88fe Changelog & version bump to 1.7.22 2014-05-19 14:43:56 -07:00
Manuel Ebert
b9685a55b0 Export to files using UTF-8
Fixes #162
2014-05-19 14:43:38 -07:00
Manuel Ebert
689f5b7461 Merge pull request #161 from notbalanced/master
Re-fix Issue #56 by changing Regex for title.
2014-04-29 09:42:47 -07:00
Craig Moyer
4442829306 Re-fix Issue #56 by changing Regex to discriminate between title and entry.
Added BDD tests to cover this feature.
2014-04-24 21:34:30 -04:00
Manuel Ebert
88a38efdc1 Merge pull request #158 from maebert/pkcs7
PKCS#7 Padding
2014-04-16 17:37:54 -04:00
Manuel Ebert
16e09a8095 byte2int for PY3 2014-04-16 17:14:57 -04:00
Manuel Ebert
91f677d825 Version bump & docs 2014-04-16 17:03:29 -04:00
Manuel Ebert
0b678ce69c Use PKCS#7 for encryption
Closes #156
2014-04-16 17:03:24 -04:00
Manuel Ebert
2b6768e427 Merge pull request #157 from maebert/1.7.20
1.7.20
2014-04-16 15:34:05 -04:00
Manuel Ebert
181c36163a Tests for ignoring invalid DayOne entries 2014-04-16 14:35:03 -04:00
Manuel Ebert
8604bf8455 Ignore unreadably DayOne entries 2014-04-16 14:34:39 -04:00
Manuel Ebert
a5d345173a version bump 2014-04-16 14:28:21 -04:00
Manuel Ebert
0161d834c0 Tests for #153 2014-04-16 14:26:02 -04:00
Manuel Ebert
b7479860a1 Assumes no DST in case of ambiguous dates
Fixes #153
2014-04-16 14:25:56 -04:00
Manuel Ebert
db4d2ddc54 Warns if using --edit without editor
Closes #151
2014-04-16 13:14:16 -04:00
Manuel Ebert
666bab572e Merge pull request #154 from maebert/1.7.19
Create path to journal during installation
2014-04-10 16:49:21 -04:00
Manuel Ebert
60b49f8c37 No 3.4 on Travis yet :-( 2014-04-10 16:18:36 -04:00
Manuel Ebert
a78d8d6337 Version bump 2014-04-10 15:58:06 -04:00
Manuel Ebert
69ded73304 Test on Python 3.4 2014-04-10 15:57:35 -04:00
Manuel Ebert
052f571e0b Create path to journal during installation
Fixes #152
2014-04-10 15:57:24 -04:00
Manuel Ebert
1a37161853 Improved docs for day one 2014-04-10 14:43:36 -04:00
Manuel Ebert
915bb99607 Small update to parsing regex 2014-04-01 20:14:49 -07:00
Manuel Ebert
c4c44d74eb Merge pull request #150 from maebert/fix-newlines
Fixes writing new lines between entries
2014-03-31 13:01:02 -07:00
Manuel Ebert
b8c1360f2d Fixes writing new lines between entries 2014-03-31 12:37:37 -07:00
Manuel Ebert
da827952b0 Merge pull request #149 from maebert/unicode-fixes
Even more unicode fixes
2014-03-28 14:52:53 -07:00
Manuel Ebert
e9e9b1611b Don't decode unicode 2014-03-28 14:40:58 -07:00
Manuel Ebert
9d4c1a23b1 Even more unicode fixes 2014-03-28 13:43:58 -07:00
Manuel Ebert
599c5a9ea1 More unicode fixes for #119 2014-03-27 16:12:03 -07:00
Manuel Ebert
f72b10700d Merge pull request #146 from maebert/fix-whitespaces
Fix whitespaces
2014-03-24 11:07:09 -07:00
Manuel Ebert
040c09dbd4 Changelog & version bump 2014-03-24 10:48:57 -07:00
Manuel Ebert
aa70bb3274 Fixes white spaces at the beginning of lines, fixes #134 2014-03-24 10:48:51 -07:00
Manuel Ebert
e2111ff209 Merge pull request #145 from maebert/unicode-fix
Unicode fix for DayOne journals
2014-03-24 09:42:16 -07:00
Manuel Ebert
9ba6b71a61 Changelog & version bump 2014-03-24 08:46:20 -07:00
Manuel Ebert
4d4a85b4b8 Small fix for unicode in DayOne, fixes #119 2014-03-24 08:45:48 -07:00
Manuel Ebert
bdb77d488d Makefile update 2014-03-19 15:24:33 -07:00
Manuel Ebert
ed43ae9bc4 Version bump 2014-03-19 15:21:52 -07:00
Manuel Ebert
4f55e895bc Merge pull request #143 from maebert/dayone-tag-search
Adds the tag symbol to day one entries on load
2014-03-19 15:20:02 -07:00
Manuel Ebert
d5496467cf Tests for filtering by tags 2014-03-19 15:00:56 -07:00
Manuel Ebert
f03d7a3bf9 Adds the tag symbol to day one entries on load 2014-03-19 14:44:47 -07:00
Manuel Ebert
887c7027b2 Merge pull request #138 from jtan189/master
Added ability to list accessible journals.
2014-02-23 21:02:35 -08:00
Josh Tan
36601cba39 Simplified syntax for joining journal names with new lines. 2014-02-23 22:46:28 -06:00
Josh Tan
f1ea01ff9a Added ability to list accessible journals.
Previously, if a user forgot the exact name of a journal, they could check the configuration file to retrieve this information. This patch adds support for performing this task using the jrnl interface.

The newly added '-ls' option retrieves the available journals from the jrnl configuration files and displays each journal name on a separate line.

This patch also updates the jrnl help display and the usage documentation to reflect these changes.
2014-02-23 22:21:54 -06:00
Manuel Ebert
f504e2a61c Merge pull request #139 from maebert/argparse-fix
Flexible argparse for 2.6
2014-02-23 20:13:47 -08:00
Manuel Ebert
27c261943c Flexible argparse for 2.6 2014-02-23 20:10:31 -08:00
Manuel Ebert
ec3751ccc0 Merge pull request #132 from MinchinWeb/line-wrap
Linewrap fixes
2014-02-17 11:27:42 -08:00
William Minchin
24a5712c4d Fix linewrap
If a line was exactly the length of `linewrap`, the extra space was
causing the next line to be blank. But if the line is empty, we need the
space to maintain the `| ` sidebar.
2014-02-07 08:24:30 -07:00
William Minchin
cac0b722cd Don't replace all double spaces
want to maintain the ability to format with the use of just text.
2014-02-07 08:21:28 -07:00
Manuel Ebert
18d5348529 Download count 2014-02-05 12:21:27 -08:00
Manuel Ebert
3931e40cd1 Merge pull request #131 from maebert/limit-shortcut
Limit shortcut
2014-02-05 10:35:52 -08:00
Manuel Ebert
1a6c9aa74a Changelog and docs 2014-02-05 10:19:01 -08:00
Manuel Ebert
5b5a5d9f57 Allows -3 as a shortcut for -n 3 2014-02-05 10:16:40 -08:00
Manuel Ebert
1d574c7407 Clean up 2014-01-29 16:54:53 -08:00
Manuel Ebert
d900b239a9 Merge pull request #130 from MinchinWeb/update-tzlocal
Update tzlocal
2014-01-29 16:49:14 -08:00
William Minchin
ca7a804b09 Convert from local util.get_local_timezone() to tzlocal.get_localzone()
one side effect is that a `pytz` object is returned rather than a
string, and so conversion to a string must be done explicitly at run
time where needed.
2014-01-29 09:57:15 -07:00
William Minchin
9e5664ba46 Fix typos in test files 2014-01-29 09:43:29 -07:00
William Minchin
a20381ce68 require tzlocal version 1.1 or better
jrnl needs the OSX fixes in version 1.1
2014-01-29 09:15:46 -07:00
William Minchin
a26fd3d1d7 [Travis-CI] manually install PyCrypto 2014-01-28 16:17:13 -07:00
William Minchin
3a4607f671 Use Travis-CI without requirements.txt 2014-01-28 16:08:22 -07:00
William Minchin
6f302ad664 Just use setup.py
(don't keep a second copy in `requirements.txt`)
2014-01-28 15:20:13 -07:00
William Minchin
f1e5c88b23 allow newer versions of tzlocal 2014-01-28 15:02:49 -07:00
William Minchin
5ff185b2df documentation typo fix 2014-01-28 14:37:15 -07:00
Manuel Ebert
81b684afdf Merge pull request #129 from maebert/fix-argparse
Fix a logic bug so that jrnl -h and jrnl -v are possible even if jrnl no...
2014-01-26 10:22:40 -08:00
Manuel Ebert
fc72514434 Fix a logic bug so that jrnl -h and jrnl -v are possible even if jrnl not configured yet. 2014-01-26 14:15:46 +01:00
Manuel Ebert
1e4506803b Merge pull request #127 from maebert/pdt1.2
Upgrade to parsedatetime 1.2
2014-01-26 03:47:59 -08:00
Manuel Ebert
cebe33574d Upgrade to parsedatetime 1.2 2014-01-26 12:37:41 +01:00
Manuel Ebert
7b630fb65e Statistics ideas 2014-01-26 12:24:54 +01:00
Manuel Ebert
60bcac8d55 Fix copyright 2014-01-26 12:05:46 +01:00
Manuel Ebert
b88180fbce Updated docs from master 2014-01-26 12:05:46 +01:00
Manuel Ebert
ea70ac713f Fixed references to jrnl_conf 2014-01-26 12:05:45 +01:00
Manuel Ebert
7599688dba Merge pull request #126 from maebert/absolute-imports
Cleaned up imports to work with Python2.6 and Python 3.3
2014-01-23 06:21:42 -08:00
Manuel Ebert
6c51374f08 More unicode support 2014-01-23 15:14:20 +01:00
Manuel Ebert
ab9a3b5192 Cleaned up imports to work with Python2.6 and Python 3.3 2014-01-23 14:33:14 +01:00
Manuel Ebert
382917b8c4 Merge pull request #125 from maebert/remove-slugify
Port slugify to Python3
2014-01-19 03:13:13 -08:00
Manuel Ebert
816aa66356 Clean up docs build folder 2014-01-19 12:12:14 +01:00
Manuel Ebert
4f7827fc91 Remove css from repo 2014-01-19 12:07:04 +01:00
Manuel Ebert
6c9687110c Port slugify to Python3 2014-01-19 11:58:13 +01:00
Manuel Ebert
92867a2f84 Merge pull request #124 from MinchinWeb/colorama-edit-fixes
Colorama edit fixes (v2)
2014-01-04 06:10:26 -08:00
William Minchin
447fb4d59f colorama fixes 2014-01-03 10:42:32 -07:00
Manuel Ebert
d4a1a10880 Changelog 2014-01-03 16:02:39 +01:00
Manuel Ebert
a0828ef151 Move docs on Windows to FAQ 2014-01-03 15:59:38 +01:00
William Minchin
57e926aaaa Windows documentation 2014-01-03 15:55:24 +01:00
Manuel Ebert
26878883dd Make editing work with templates 2014-01-03 15:52:59 +01:00
Manuel Ebert
f0aab36e19 Only install colorama on windows 2014-01-03 15:51:27 +01:00
Manuel Ebert
b050f2b030 Include dateutil in setup 2014-01-02 23:42:01 +01:00
Manuel Ebert
fcd8c4d214 Gets rid of colorama 2014-01-02 23:41:53 +01:00
Manuel Ebert
c3e8f59fe0 Doc fix 2014-01-02 14:53:32 +01:00
Manuel Ebert
77b669aedf Touch tmpfiles before opening 2014-01-02 07:48:57 +01:00
Manuel Ebert
860d74bc0e Travis fix 2013-12-22 18:50:57 +01:00
Manuel Ebert
cdf312de28 Makefile update 2013-12-22 18:47:09 +01:00
Manuel Ebert
a4448b0c34 Dateutil added to requirements. 2013-12-22 18:46:26 +01:00
Manuel Ebert
8c9ae82b98 Merge pull request #118 from maebert/develop
Develop
2013-12-22 09:42:14 -08:00
Manuel Ebert
e0ef258a83 Smal doc fixes (fixes #115) 2013-12-22 18:26:57 +01:00
Manuel Ebert
9df688d6f0 Changelog, dogs 2013-12-22 17:11:32 +01:00
Manuel Ebert
d34874947f Tests for #117 2013-12-22 14:52:13 +01:00
Manuel Ebert
077bff7e88 Fix for #117 2013-12-22 14:52:06 +01:00
Manuel Ebert
fa130143b5 DayOne support for --edit 2013-12-21 15:00:26 +01:00
Manuel Ebert
88db6fca23 Cleaner parsing 2013-12-21 14:59:24 +01:00
Manuel Ebert
c06bfd781f Modified flag for entries 2013-12-20 16:16:48 +01:00
Manuel Ebert
410e4299ea Get rid of --delete 2013-12-20 16:16:48 +01:00
Manuel Ebert
c6bde5e9ac Line break 2013-12-02 22:39:34 -08:00
Manuel Ebert
4d6b22861e Version bump 2013-11-30 15:14:53 -08:00
Manuel Ebert
abe2281361 Subheaders 2013-11-30 15:13:48 -08:00
Manuel Ebert
7fd210de3c Docs update 2013-11-30 15:10:34 -08:00
Manuel Ebert
bc75d4031b Deleting the last entry 2013-11-30 14:55:37 -08:00
Manuel Ebert
e1f31a23ae Merge pull request #112 from maebert/version-info
Version info
2013-11-29 11:23:22 -08:00
Manuel Ebert
c6e620d9cb py2.6 2013-11-29 11:13:12 -08:00
Manuel Ebert
1c7f0b351d Tests for new structure 2013-11-29 10:23:16 -08:00
Manuel Ebert
f9ea3e69ff jrnl.py:cli -> cli.py:run 2013-11-29 10:23:09 -08:00
Manuel Ebert
91b4257f7c Merge pull request #110 from MinchinWeb/help-windows
Help windows
2013-11-28 19:02:49 -08:00
William Minchin
b49e868f93 Set default linewrap to 79 globally 2013-11-27 12:52:56 -07:00
William Minchin
b6016611fd Add MS Visual Studio to GitIgnore 2013-11-27 10:37:14 -07:00
William Minchin
fd1fc05bd0 Windows: wraps lines at 79 chars by default
trying to wrap lines at 80 char in Windows' CMD results in blank lines
following lines that are exactly 80 char long
2013-11-27 10:25:17 -07:00
William Minchin
006361d963 Change break msg for Windows
for multiline input on Windows, you need Ctrl+Z to exit
2013-11-27 10:07:06 -07:00
William Minchin
e5262067a2 your chance to make a programmer happy :)
fix small typo in CONTRIBUTING.md
2013-11-27 09:11:01 -07:00
Manuel Ebert
f2b3f56993 Merge pull request #109 from maebert/piping
Piping data into jrnl
2013-11-24 11:34:56 -08:00
Manuel Ebert
0911c2e470 Docs & Version bump 2013-11-24 11:29:55 -08:00
Manuel Ebert
a88459dc1c Piping data into jrnl and multiline composing 2013-11-24 11:29:45 -08:00
Manuel Ebert
8821158835 Merge pull request #108 from maebert/#100
Squash of @alapolloni 's PR
2013-11-22 12:35:45 -08:00
Manuel Ebert
421d27f021 Squash of @alapolloni 's PR
Fixes #100 and #102
2013-11-22 12:27:52 -08:00
Manuel Ebert
37fad66942 ixed a bug that caused creating encrypted journals to fail 2013-11-20 16:55:41 -08:00
Manuel Ebert
7097ed03a2 Merge pull request #106 from multani/master
Typo on home page
2013-11-20 08:16:58 -08:00
Jonathan Ballet
afa1b2bc4d Typo in documentation 2013-11-20 11:18:52 +01:00
Manuel Ebert
7195c545ae Change to Makefile 2013-11-06 00:01:22 -08:00
Manuel Ebert
c582ac2af9 Fixes to docs 2013-11-05 23:54:54 -08:00
Manuel Ebert
7247af2195 Use RTFD's theme on their site 2013-11-05 13:26:42 -08:00
Manuel Ebert
bc9b8600a7 Fixes logo path on docs and introduces makefile 2013-11-05 11:59:32 -08:00
Manuel Ebert
bb5f8141ee Merge pull request #105 from maebert/docs
Docs
2013-11-05 11:32:08 -08:00
Manuel Ebert
b7afaa355c Contributing 2013-11-05 10:23:26 -08:00
Manuel Ebert
e0655d894a Changelog update 2013-11-05 10:23:26 -08:00
Manuel Ebert
73d0969d20 Finished docs 2013-11-05 10:23:26 -08:00
Manuel Ebert
04567ff5a7 Starring entries and looking at the stars 2013-11-05 10:23:26 -08:00
Manuel Ebert
89afb5ba36 Gloriously mobile-retina-ready docs 2013-11-05 10:21:46 -08:00
Manuel Ebert
1dafa66ef3 Glorious. 2013-11-05 10:21:46 -08:00
Manuel Ebert
241dac087a More design work 2013-11-05 10:21:45 -08:00
Manuel Ebert
840425d3a4 Doc stubs 2013-11-05 10:21:45 -08:00
Manuel Ebert
07e8f19e21 Icons 2013-11-05 10:21:45 -08:00
Manuel Ebert
7965e1b4ef Doc init 2013-11-05 10:21:45 -08:00
Manuel Ebert
84641ee62f Merge pull request #104 from maebert/starring
Starring entries
2013-11-04 10:26:07 -08:00
Manuel Ebert
e6592ddd5c Merge pull request #103 from maebert/tolerant-json
Tolerant json parsing
2013-11-04 09:39:39 -08:00
Manuel Ebert
630aaefffd Parses -from 1980 as Jan 1 1980 2013-11-04 00:36:46 -08:00
Manuel Ebert
e37119b1d5 -short becomes --short 2013-11-04 00:36:10 -08:00
Manuel Ebert
a8c232b310 Version bump 2013-11-03 20:02:28 -08:00
Manuel Ebert
9c6db60cc4 Tests for starring 2013-11-03 20:02:28 -08:00
Manuel Ebert
a64d1a2350 Deprecates -date and -star arguments 2013-11-03 20:02:28 -08:00
Manuel Ebert
6ec4cb10c1 "Starred" in json export 2013-11-03 20:02:28 -08:00
Manuel Ebert
c201e56713 Starring entries and looking at the stars 2013-11-03 20:02:18 -08:00
Manuel Ebert
6a88f78d9a Version bump 2013-11-03 14:39:35 -10:00
Manuel Ebert
967e888936 Tests for fixing errors automatically 2013-11-03 14:38:41 -10:00
Manuel Ebert
b60605567e Attempts to fix common errors in jrnl_config automatically 2013-11-03 14:38:32 -10:00
Manuel Ebert
f9f20fb4ad Better prompts for y/n questions 2013-10-21 14:36:31 -07:00
Manuel Ebert
0d451be033 Delete tmp_log.bak 2013-10-20 14:03:27 -07:00
Manuel Ebert
2b4afeb635 Merge pull request #99 from maebert/96-keychain
Saves passwords to keyring
2013-10-20 13:45:15 -07:00
Manuel Ebert
36b375831a Tests for soft-deprecating passwords in config 2013-10-20 13:43:04 -07:00
Manuel Ebert
aac68db995 Only soft-deprecate passwords in config 2013-10-20 13:42:55 -07:00
Manuel Ebert
360580dbc7 Updates readme 2013-10-20 13:30:45 -07:00
Manuel Ebert
7aa252c0ab User plain text keyring for testing 2013-10-20 13:30:42 -07:00
Manuel Ebert
ed66d76db2 Tests for storing password in keychain 2013-10-17 16:26:49 -07:00
Manuel Ebert
50036e2ee1 Only updates config locally on encrypting and decrypting 2013-10-17 16:04:34 -07:00
Manuel Ebert
68cd41de40 Remove tests for having password in config 2013-10-17 16:00:45 -07:00
Manuel Ebert
7e3faeef45 Disambiguates jrnl.update_config and install.update_config 2013-10-17 16:00:27 -07:00
Manuel Ebert
be2c511ea4 Saves password to keyring
Closes #96 and deprecates password field in config
2013-10-17 14:55:59 -07:00
Manuel Ebert
d9f01fd753 Merge pull request #97 from bitdeli-chef/master
Add a Bitdeli Badge to README
2013-10-08 10:57:02 -07:00
Bitdeli Chef
d588b31a71 Add a Bitdeli badge to README 2013-10-08 17:57:54 +00:00
Manuel Ebert
f5826fb56b Version bump 2013-09-23 17:24:29 -07:00
Manuel Ebert
8f5082a919 Fixes #91 2013-09-23 17:24:23 -07:00
Manuel Ebert
dd36b1faa3 Merge pull request #94 from maebert/timezone-fix
Fixes timezone detection for non-admin accounts on OS X
2013-09-16 11:10:02 -07:00
Manuel Ebert
08d260f3b2 Treat non-OS X separately 2013-09-16 10:57:37 -07:00
Manuel Ebert
7315aa8c06 Version bump 2013-09-16 10:46:02 -07:00
Manuel Ebert
1246fb2bbb Fixes #93 2013-09-16 10:45:57 -07:00
Manuel Ebert
fd95c6a83f Merge pull request #92 from maebert/bugfix-dayone-mobile
Bugfix dayone mobile
2013-08-28 13:55:26 -07:00
Manuel Ebert
2235a2f974 Python 2.6 compatibility 2013-08-28 13:48:18 -07:00
Manuel Ebert
d4aa08e18a Adds empty folder for testing 2013-08-28 13:37:18 -07:00
Manuel Ebert
3d49937335 Better checking for failed runs 2013-08-28 12:36:00 -07:00
Manuel Ebert
e37395ffd9 Failures return exit code 1 2013-08-28 12:29:34 -07:00
Manuel Ebert
8f85b97942 Version bump 2013-08-28 12:19:03 -07:00
Manuel Ebert
a7afed7c8b Tests for error messages on empty directories 2013-08-28 12:18:58 -07:00
Manuel Ebert
79b61dfbab Detectsa DayOne journals that are stored in the Mobile Documents folder 2013-08-28 12:18:39 -07:00
Manuel Ebert
30692efc6c Merge pull request #90 from maebert/#89-dayone
#89 dayone
2013-08-17 18:25:20 -07:00
Manuel Ebert
d36711b36b dateutil -> python-dateutil 2013-08-17 18:14:06 -07:00
Manuel Ebert
e796f18d6e Updates travis build 2013-08-17 13:24:51 -07:00
Manuel Ebert
81e8e26b0a Tests for parsing DayOne entries without Timezone 2013-08-17 13:19:54 -07:00
Manuel Ebert
575026b51d Some testing goodies 2013-08-17 12:18:48 -07:00
Manuel Ebert
19d342d6f2 Version bump and doc fixes 2013-08-17 12:09:57 -07:00
Manuel Ebert
59de6351bc Tests for DayOne Tagging 2013-08-17 12:06:59 -07:00
Manuel Ebert
f3e1f94f55 Support for DayOne tagging
Closes #83
2013-08-17 12:06:52 -07:00
Manuel Ebert
8446908139 Initial support for DayOne integration testing 2013-08-17 11:59:51 -07:00
Manuel Ebert
58ea57f62b Fixes #87 2013-08-17 11:54:21 -07:00
Manuel Ebert
887f765495 Merge pull request #88 from maebert/bugfix-87
Bugfix 87
2013-08-14 14:18:51 -07:00
Manuel Ebert
24ff65d6fc Readme + changelog update 2013-08-14 14:08:05 -07:00
Manuel Ebert
2593b23d26 Version bump 2013-08-14 14:07:57 -07:00
Manuel Ebert
93813ef040 Updates tests 2013-08-14 14:06:53 -07:00
Manuel Ebert
2e44a29c33 Soft-deprecates -to and introduces -until 2013-08-14 14:06:39 -07:00
Manuel Ebert
6dae28f196 Version bump 2013-08-14 14:01:13 -07:00
Manuel Ebert
ad98031c7f Adds regression test for #87 2013-08-14 14:00:42 -07:00
Manuel Ebert
619285fb7f Prevents journal from printing when composing an entry
This fixes #87
2013-08-14 14:00:32 -07:00
Manuel Ebert
0e2f63a5b1 Hello 2013 🎆 2013-08-09 11:45:23 -07:00
Manuel Ebert
9ba3e447de Merge pull request #86 from maebert/export-fix
Allows filtering the journal before exporting
2013-08-06 18:17:45 -07:00
Manuel Ebert
ff0b85e28b Better tests for exporting to json 2013-08-06 18:10:42 -07:00
Manuel Ebert
1889cee6c3 Version bump 2013-08-06 17:57:36 -07:00
Manuel Ebert
3ef6095b41 Tests for filtering 2013-08-06 17:57:31 -07:00
Manuel Ebert
978d9db072 Filters before exporting 2013-08-06 17:57:21 -07:00
Manuel Ebert
9dda1b39bd Updates distribution info 2013-07-26 19:40:39 -07:00
Manuel Ebert
08d6f5fa06 Links for getting help 2013-07-24 15:03:54 -07:00
Manuel Ebert
9443fb3b5f Merge pull request #85 from maebert/tests
New unit testing suit using behave and several smaller fixes
2013-07-23 21:14:06 -07:00
Manuel Ebert
6bcf83ed78 Tests for tagging 2013-07-23 21:02:03 -07:00
Manuel Ebert
13a0e7ce86 Tagging fixes 2013-07-23 21:01:57 -07:00
Manuel Ebert
601e574d91 Test for decrypting journals when password is saved in config 2013-07-22 21:26:21 +02:00
Manuel Ebert
bf69e0042e Fixes encryption tests 2013-07-22 21:24:25 +02:00
Manuel Ebert
e78786e19a Unifies encryption between python versions 2013-07-22 21:24:19 +02:00
Manuel Ebert
9c3841dded Changes cleaning strategy 2013-07-22 21:19:30 +02:00
Manuel Ebert
b1266dcc4e Tests for encryption 2013-07-22 20:08:53 +02:00
Manuel Ebert
95f68a5109 Allows getpass to get bypassed by reading from stdin 2013-07-22 20:08:41 +02:00
Manuel Ebert
0b7b88dcf0 Tests for using stderr prompts 2013-07-22 12:04:32 +02:00
Manuel Ebert
7cf4fd701e Uses stderr for prompts instead stdout 2013-07-22 12:04:01 +02:00
Manuel Ebert
91638fd429 Tests for multiple journals 2013-07-22 10:11:37 +02:00
Manuel Ebert
c303ea8455 Python 3 improvements 2013-07-19 13:24:18 +02:00
Manuel Ebert
7f680f6260 Python 3 compatibility for tests 2013-07-19 13:09:33 +02:00
Manuel Ebert
608cc04897 Better Python2.6 compatibility 2013-07-19 13:03:27 +02:00
Manuel Ebert
6abd7d5b68 Mock stdin 2013-07-19 12:44:39 +02:00
Manuel Ebert
d4cbd4f944 Update travis 2013-07-19 12:43:58 +02:00
Manuel Ebert
3a4c87ca4c Emoji support 2013-07-19 11:46:02 +02:00
Manuel Ebert
6521d55a2b Tests for writing entries from the command line 2013-07-19 11:36:39 +02:00
Manuel Ebert
89423bdccb Backup and restore config and journal files every time 2013-07-19 11:36:29 +02:00
Manuel Ebert
783603fb1c Core testing 2013-07-18 22:49:22 +02:00
Manuel Ebert
c5cd42028d Ability to parse in args manually 2013-07-18 22:48:46 +02:00
Manuel Ebert
02ce7dfbd3 Adds slugify to setup.py 2013-07-18 09:02:37 +02:00
Manuel Ebert
b773c0d7f5 Version bump 2013-07-17 18:29:50 +02:00
Manuel Ebert
edc41dab20 Updated Readme 2013-07-17 18:24:53 +02:00
Manuel Ebert
0c374a839c Simplified exporting logic 2013-07-17 18:19:14 +02:00
Manuel Ebert
d56f4140fb Fixes bug in tag export 2013-07-17 18:13:51 +02:00
Manuel Ebert
9309ec391b Merges requirements 2013-07-17 17:18:40 +02:00
Manuel Ebert
39a8b3a4cd Oh Python 3 imports... 2013-07-15 13:55:34 +02:00
Manuel Ebert
a7e64dd756 Version bump 2013-07-15 13:37:35 +02:00
Manuel Ebert
de8caf08a6 Caches local timezone 2013-07-15 13:37:30 +02:00
Manuel Ebert
d81f263e8c Uses local timezone if timezone could not be parsed 2013-07-15 13:37:13 +02:00
Manuel Ebert
f6be2f13a5 Workaround for get_localzone on OS X 2013-07-15 13:32:23 +02:00
dejay
42daea2dd6 Better Day One Timestamps 2013-07-13 11:57:34 -06:00
Aniket Pant
bce370174b Update installation after removal of default folder config
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 13:09:22 +05:30
Aniket Pant
6fff395701 Update readme in accordance to changes
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 13:05:24 +05:30
Aniket Pant
659f9c4d98 Improve export functionality
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 13:05:13 +05:30
Aniket Pant
bdc0b4c5f5 Remove default folder config
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 13:04:50 +05:30
Aniket Pant
9e969786f5 Fix changelog
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 11:20:58 +05:30
Aniket Pant
5e448270d2 Merge master and fix merge conflict
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-26 11:19:10 +05:30
Manuel Ebert
d7dfba008c Addresses unicode issues in Python 3
Fixes #79
2013-06-24 15:54:04 +02:00
Aniket Pant
cf4592fbe3 Update changelog
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:23:54 +05:30
Aniket Pant
99dd9a05aa Update version to 1.1.1
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:23:46 +05:30
Aniket Pant
d48f5137e5 Update readme
Information on new export options added

Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:23:27 +05:30
Aniket Pant
65760eabff Add slugify to requirements
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:22:28 +05:30
Aniket Pant
5408f60db3 Add new export option
Support for json and md retained
All export types moved to --export except tags

Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:22:02 +05:30
Aniket Pant
a959ed49fd Add default export folder config
Signed-off-by: Aniket Pant <me@aniketpant.com>
2013-06-21 19:20:57 +05:30
Manuel Ebert
868d29a92e Allow python 3 to fail on travis 2013-06-09 16:04:17 -07:00
Manuel Ebert
cd2eebab34 Typo 2013-06-09 15:59:49 -07:00
Manuel Ebert
b4a28d3c7b Changelog cleanup 2013-06-09 15:59:11 -07:00
Manuel Ebert
5bb8f9c567 Better Unicode support
Closes #72
2013-06-09 15:55:03 -07:00
Manuel Ebert
cf7581c0c0 Moves tag export to exporters 2013-06-09 15:07:27 -07:00
Manuel Ebert
32f3d23b90 Nicer error message when there is a syntactical error in your config file. 2013-06-09 14:49:02 -07:00
Manuel Ebert
48c5dd8aa1 JSON exports tags
Closes #78. This changes the export format to

{
tags: {tag: count},
entries: [{…}, {…}, …]
}
2013-06-09 14:48:56 -07:00
Manuel Ebert
0bf2354bc2 Disables PyICU use for now until proper localization is implemented.
Fixes #52
2013-05-08 10:08:48 -07:00
Manuel Ebert
fde308e4ba Backwards compatibility with parsedatetime 0.8.7
Closes #76
2013-05-08 09:56:49 -07:00
Manuel Ebert
aef9a0ee7e Fixes double readline import
Should finally fix #71
2013-04-23 16:29:32 -07:00
Manuel Ebert
1417305c2e Formatting 2013-04-23 09:13:06 -07:00
Manuel Ebert
4528d9f612 Conditional import for pyreadline
Fixes #71
2013-04-23 09:13:00 -07:00
Manuel Ebert
56a9c0bbcc pycrypto back to extras.
Fixes 68
2013-04-22 09:58:45 +02:00
Manuel Ebert
2790f250e6 Merge pull request #70 from okin/master
Make the cli work again.
2013-04-22 00:14:34 -07:00
Niko Wenselowski
dab192db86 Make the cli work again. 2013-04-21 22:03:49 +02:00
Manuel Ebert
dbddb7d6c9 Build status 2013-04-19 17:58:07 +02:00
Manuel Ebert
3abe14bab1 Better utf8 support 2013-04-19 17:32:32 +02:00
Manuel Ebert
8865816d60 Updated readme and changelog and changed config path according to discussion 2013-04-19 16:58:19 +02:00
Manuel Ebert
62a48d7b02 Merge branch 'patch-1' of git://github.com/evaryont/jrnl into evaryont-patch-1 2013-04-19 16:50:27 +02:00
Manuel Ebert
9f33c9c9c4 Fixes for Python 3 Support 2013-04-19 15:19:21 +02:00
Manuel Ebert
8200ebb6fe Python 3 compatibility 2013-04-19 14:46:05 +02:00
Manuel Ebert
52b5b4848d Experimental build settings for travis 2013-04-18 11:52:53 +02:00
Manuel Ebert
80da7326b8 Python 2.6 compatibility
Closes #67
2013-04-18 11:51:42 +02:00
Manuel Ebert
41706c3cb5 Fixes install routine 2013-04-17 12:02:26 +02:00
Manuel Ebert
9b0bee0637 Fixes readline support on windows
Closes #60
2013-04-17 10:50:03 +02:00
Manuel Ebert
36fe783e5e Merge pull request #61 from alapolloni/master
Update jrnl.py so that a trailing / for a DayOne journal config is seen as a DayOne journal.
2013-04-17 01:32:56 -07:00
Manuel Ebert
8ed2e36669 Fixes bug when showing tags when no tags are defined.
Closes #63
2013-04-17 10:27:17 +02:00
Manuel Ebert
b2a5b7b219 Automatic versioning 2013-04-17 10:19:38 +02:00
Manuel Ebert
24f769cf66 Makes PyCrypto an automatic install 2013-04-17 10:19:25 +02:00
Manuel Ebert
fe521147c0 Uses colorama instead of clint 2013-04-17 10:19:02 +02:00
Alex Apolloni
38d2734c96 allow trailing \ for DayOne journals
https://github.com/maebert/jrnl#dayone-integration
sort of infers that it in the .jrnl_config it should end in a /

but prior to this, if there was a trailing / then jrnl thought it was
looking at a text type journal.
2013-04-15 14:05:04 +02:00
No GUI
080bfd513f Support a custom location for the config
Export $JRNL_CONFIG to override the location instead of using $HOME. Too many dotfiles are literring it, ya' know?

Linux desktop users, you might like if you follow the XDG standards:

    export JRNL_CONFIG=$XDG_CONFIG_HOME/jrnl/config
2013-04-14 13:19:03 -07:00
Manuel Ebert
9a55b2c81b Merge pull request #56 from notbalanced/master
Fixes issue where entry is split into title and body at decimal points in numbers.
2013-03-29 13:47:59 -07:00
Craig Moyer
1fc6bc2dd7 Fix issue where embedded decimal in title causes title to get cut off. 2013-03-28 21:31:08 -04:00
Manuel Ebert
ba2b4a592a Fixes typos in readme 2013-03-13 00:51:24 -07:00
Manuel Ebert
e3613b8628 Version bump 2013-03-12 13:30:24 -07:00
Manuel Ebert
3d31e31eb5 Requires current version of parsedatime when using setuptools
Closes #52
2013-03-12 13:29:10 -07:00
Manuel Ebert
ba9e39f050 Changelog updated 2013-03-04 15:49:59 -08:00
Manuel Ebert
dd47a94b61 Version bump to 1.0.0 2013-03-04 14:58:02 -08:00
Manuel Ebert
fe5f318c16 Merge pull request #51 from maebert/dayone
Dayone support
2013-03-04 14:57:10 -08:00
Manuel Ebert
caad0bdbae Merges pull request #49 from dedan/remove_single_tags
* remove_single_tags:
  Cleaner syntax and messages
  remove tags that were used only once.
2013-03-04 14:55:30 -08:00
Manuel Ebert
05198b65b7 Cleaner syntax and messages 2013-03-04 14:54:29 -08:00
Stephan Gabler
a8777b747a remove tags that were used only once.
I sometimes look at the overview of tags with `jrnl --tags` and there
I found it annoying to see all the tags that appeared only once. Those
are now filtered out and I print a comment saying that they were filtered out

Also changed the order (sorted by counts) in which the tags are printed. For
a CLI application I think it is nicer to see the more important version at the end
2013-03-04 14:53:46 -08:00
Manuel Ebert
123000ce41 Fixes issue where now date was given 2013-03-04 14:44:04 -08:00
Manuel Ebert
5236ca0c44 Compatibility with parsedatetime 1.x 2013-03-04 14:43:55 -08:00
Manuel Ebert
1c0916a8f3 Removes 3.2 support from travis 2013-02-19 14:53:43 -08:00
Manuel Ebert
918a46a2fa Fixes requirements and setup 2012-12-14 15:18:30 -08:00
Manuel Ebert
8418c2f696 Travis CI integration 2012-12-14 14:58:46 -08:00
Manuel Ebert
370e703d11 Documentation on DayOne integration 2012-08-09 18:43:57 +02:00
Manuel Ebert
fd0d35e151 Smarter UUID creation for DayOne journals 2012-08-09 18:42:31 +02:00
Manuel Ebert
9476d3f115 Experimental DayOne Support
Just point your jrnl file to a folder ending with `.dayone` and magic.
Supports both reading and creation (and hence all other export magic,
although this is untested).
2012-08-09 18:33:00 +02:00
Manuel Ebert
ac266fd3b1 New style class, new_entry returns entry 2012-08-09 17:54:17 +02:00
Manuel Ebert
386c574815 Removed unnecessary print statement 2012-07-25 17:59:59 +02:00
Manuel Ebert
911ec2eb3f bump to 1.0.0-rc1 2012-07-23 13:09:46 +02:00
Manuel Ebert
61d73281c4 Notes on 1.0.0-rc1 2012-07-23 13:09:46 +02:00
Manuel Ebert
a56b395b7a Fixes a bug where jrnl would not go into compose mode if the entry comes from command line, is wrapped into quotes and starts with a tag symbol 2012-07-23 13:09:46 +02:00
Manuel Ebert
25ea115eca Notes on individual settings for multiple journals 2012-07-23 13:09:45 +02:00
Manuel Ebert
cb6ee5889e Better support for individual configs 2012-07-23 13:09:45 +02:00
Manuel Ebert
33195829c4 Saves changes in config (ie after decrypting) to the right scope 2012-07-23 13:09:45 +02:00
Manuel Ebert
a04675e33e Allows multiple journals to override default configuration 2012-07-23 13:09:45 +02:00
Manuel Ebert
c1bfd8b451 Refactored install.py 2012-07-23 13:09:45 +02:00
Manuel Ebert
9a39c8962d Journal class brings it's own default config. 2012-07-23 13:09:45 +02:00
Manuel Ebert
9796f5b0b0 Merge pull request #42 from dedan/strip_whitespace
remove the leading whitespace introduced by textwrap
2012-07-17 04:20:59 -07:00
Stephan Gabler
907a75d41e remove the leading whitespace introduced by textwrap 2012-07-17 12:12:47 +02:00
Manuel Ebert
144785d2b5 Version bump to 0.3.2 2012-07-05 12:47:48 +02:00
Manuel Ebert
fb3d7abc2d Converts \n to new lines 2012-07-05 12:44:40 +02:00
Manuel Ebert
7e59e8ad3b Refractored exporting 2012-07-05 12:31:50 +02:00
Manuel Ebert
3cbef42e2b Feedback on which journal was used 2012-07-05 12:27:57 +02:00
Manuel Ebert
80bb29bf0e Formatting issues. 2012-06-20 20:57:12 +03:00
Manuel Ebert
1903749901 Merge pull request #39 from maebert/multiple-journals
Multiple journals
2012-06-16 10:51:56 -07:00
Manuel Ebert
017843b948 [Improved] Supports multiple journal files. 2012-06-11 16:17:25 +02:00
Manuel Ebert
4898759b8b Help on multiple journals. 2012-06-11 16:16:26 +02:00
Manuel Ebert
9439c294da Installation modifies journals.default, not journal 2012-06-11 16:08:38 +02:00
Manuel Ebert
41c26d6115 Adds support for multiple journals 2012-06-11 16:02:21 +02:00
Manuel Ebert
482e579242 Adds support for multiple journals 2012-06-11 15:58:25 +02:00
Manuel Ebert
46ba622aa1 Adds feedback on updating jrnl_conf 2012-06-11 15:39:45 +02:00
Manuel Ebert
c289aaeb8c Merge pull request #34 from maebert/new-style-display
New style display of posts
2012-06-01 04:52:51 -07:00
Manuel Ebert
4cca754ba5 Deleting last entry 2012-06-01 11:07:57 +02:00
Manuel Ebert
3935681365 Allows deleting the last entry in the journal. 2012-06-01 11:06:53 +02:00
Manuel Ebert
3b9610f5f4 Fixes a bug where encryption or decryption without target file would just bring up writing mode. 2012-05-31 11:43:20 +02:00
Manuel Ebert
2f8a95f553 Strips blank lines at the end of Entry bodies when prettyprinting. 2012-05-31 11:37:02 +02:00
Manuel Ebert
6c1e3f2351 Suppress empty bodies in prettyprinting 2012-05-31 11:33:28 +02:00
Manuel Ebert
084e956d16 Use Entry.pprint for display, str(Entry) for writing. 2012-05-31 11:22:54 +02:00
Manuel Ebert
cc786e5376 Refactors prettyprinting into the pprint method, __str__ will only produce journal-file-ready strings again. 2012-05-31 11:22:34 +02:00
Manuel Ebert
08b7624513 New style display of posts 2012-05-30 18:12:10 +02:00
Manuel Ebert
0d19f1790f Fixes highlighting 2012-05-30 17:58:26 +02:00
Manuel Ebert
dd824c4f8d Using string.format instead of % 2012-05-30 17:50:37 +02:00
Manuel Ebert
bfcdb2928f Linewrap support documented 2012-05-30 17:45:52 +02:00
Manuel Ebert
f7eee0fa76 Adds word wrap support. 2012-05-30 17:43:55 +02:00
Manuel Ebert
56c974f1dd Version bump to 0.3.0 2012-05-24 18:18:05 +02:00
Manuel Ebert
4a1312a70b Fixes time parsing, closes #32 2012-05-24 18:14:12 +02:00
Manuel Ebert
4850828e07 Error message when journal is corrupted, closes #22 2012-05-24 17:04:10 +02:00
Manuel Ebert
8ec7c51d89 Metavars on encrypt and decrypt options 2012-05-24 17:03:35 +02:00
Manuel Ebert
3c05edde25 New Changelog format 2012-05-24 16:58:17 +02:00
Manuel Ebert
386d813eeb Opens journal file before allowing user to compose entry, closes #21 2012-05-24 16:51:09 +02:00
Manuel Ebert
99e8679b48 Merge pull request #33 from maebert/folders
Restructures folders
2012-05-24 07:44:45 -07:00
Manuel Ebert
e8f346242e Version bump to 0.2.5 2012-05-24 13:43:48 +02:00
Manuel Ebert
32b691f938 Added an entry point so we don't have to make an bash alias anymore. 2012-05-24 13:30:37 +02:00
Manuel Ebert
4b354bd40e removed necessity to create bash alias 2012-05-24 13:28:50 +02:00
Manuel Ebert
19ed9a6cf8 Experimental new directory structure 2012-05-24 13:23:46 +02:00
Manuel Ebert
5bee8ff1e1 Added note for Windows users, closes #29 2012-05-22 17:36:47 +03:00
Manuel Ebert
945ee4145b Merge pull request #30 from dedan/fix_dateparsing
Fix dateparsing and add testing scaffold
2012-05-22 04:21:27 -07:00
Stephan Gabler
59049a87f0 add testing scaffold, #28 2012-05-22 09:02:17 +02:00
Stephan Gabler
19a1db7038 fix date parsing when text body contains colon.
The problem was appeared when no date was specified and the text body contained a colon. All the text before the colon was discarded and lost.

This commit changes the code to first extract the first line as a title and then only look whether this title contains a valid date.
2012-05-22 09:02:17 +02:00
Manuel Ebert
cd48d4e41d License, readme and changelog renamed 2012-05-21 12:25:54 +02:00
Manuel Ebert
5d3070ddd7 License, readme and changelog renamed 2012-05-21 12:25:02 +02:00
Manuel Ebert
44333a5330 Makes pycrypto optional, closes #23 2012-05-21 12:20:24 +02:00
Manuel Ebert
4ec53e1b69 Makes encryption optional. 2012-05-21 12:14:36 +02:00
Manuel Ebert
4e39ab2821 Adds version string and more 2012-05-21 11:50:49 +02:00
Manuel Ebert
f8a8cdf24b Print-statement Python 3-compatible 2012-05-21 11:42:52 +02:00
Manuel Ebert
7dcd78f3ff Decrypt and Encrypt into new files, closes #23 2012-05-21 11:41:28 +02:00
Manuel Ebert
e9063e1336 Version bump to 0.2.4 2012-05-21 11:25:28 +02:00
Manuel Ebert
3e07ef5fe4 Uses \n instead of os.linesep (http://stackoverflow.com/questions/454725/python-get-proper-line-ending), closes #27 #26 2012-05-21 11:21:42 +02:00
Manuel Ebert
432997969b Merge pull request #26 from dedan/fix_parsing
Fix parsing of newlines, closes #15
2012-05-15 02:58:03 -07:00
Stephan Gabler
7c3a6d0222 don't ignore the blank lines #15 2012-05-14 21:57:58 +02:00
Stephan Gabler
448f56473d simplify date parsing
and remove some whitespace
2012-05-14 21:44:30 +02:00
Manuel Ebert
d4bd93779a Updated .gitignore 2012-05-10 14:26:43 +02:00
Manuel Ebert
e4eef0fa81 Formatted tag count line 2012-05-10 11:27:02 +02:00
Manuel Ebert
e238d88225 Commented on a nested list comprehension, following f96d73d2 2012-05-09 21:49:55 +03:00
Manuel Ebert
f96d73d2e1 Modernised tag count syntax, Python 3 idioms galore! 2012-05-08 13:04:35 +02:00
Manuel Ebert
cfbfcbc50b Bump to 2.3 2012-05-04 16:39:19 +02:00
Manuel Ebert
bd834e7863 Added readme on -short option 2012-05-03 14:27:24 +03:00
Manuel Ebert
b14493eea5 Update CHANGELOG.md 2012-05-03 14:26:04 +03:00
Manuel Ebert
0710411cde Merge pull request #24 from dedan/short_option
add short option like discussed in #20, closes #20
2012-05-03 04:24:16 -07:00
Stephan Gabler
d5b931c4a1 add short option like discussed in #20
a -short option, which only outputs the titles of the entries, not the entire entries. So jrnl @maebert will display the full entries, but jrnl -short @maebert will output all lines containing the tag. jrnl -short without a search tag will display all titles
2012-05-03 11:42:24 +02:00
Manuel Ebert
46a7bea4b4 Merge pull request #20 from dedan/tag_search
highlight only the tags someone searched for in the tag search #16
2012-05-03 02:30:51 -07:00
Stephan Gabler
a79f094fd8 highlight only the tags someone searched for in the tag search #16 2012-04-26 17:29:53 +02:00
Manuel Ebert
0cc57bab49 Update CHANGELOG.md 2012-04-26 14:55:51 +03:00
Manuel Ebert
5a096ccd54 Merge pull request #19 from dedan/fix_editor_not_saved
don't crash if editor file was not saved (treat it as read mode) close #12
2012-04-26 04:52:16 -07:00
Manuel Ebert
d550932b98 Merge pull request #17 from dedan/fix_highlighting
automatically add 'highlight' to config after upgrade from earlier versiion
2012-04-26 04:51:10 -07:00
Stephan Gabler
0a56658c43 don't crash if editor file was not saved (treat it as read mode) close #12 2012-04-26 10:03:04 +02:00
Stephan Gabler
3ca6ef89f4 make update of new settings a bit more general and remove some whitespace
* config.update(default_config) does not work, would overwrite all values
2012-04-26 09:43:09 +02:00
Stephan Gabler
6a23b54c36 automatically add 'highlight' to config after upgrade from earlier version #16 2012-04-24 22:05:33 +02:00
Manuel Ebert
f2e207de85 Adds support for tag highlighting as proposed by #16 2012-04-24 18:44:57 +02:00
Manuel Ebert
ae57eb9d77 Adds tag search, closes #16 2012-04-24 12:05:17 +02:00
Manuel Ebert
632add24f9 Version bump to 0.2.2 2012-04-17 22:01:09 +02:00
Manuel Ebert
a100d147af Version bump to 0.2.2 2012-04-17 21:58:34 +02:00
Manuel Ebert
2ec032b4e6 Notes on --encrypt and --decrypt 2012-04-17 21:54:08 +02:00
Stephan Gabler
7009a88316 add markdown support #8 2012-04-17 21:51:33 +02:00
Stephan Gabler
de98d8f9bd add markdown support to the readme, close #8 2012-04-17 21:46:18 +02:00
Stephan Gabler
5ddbfed35a remove unused import 2012-04-17 21:45:23 +02:00
104 changed files with 9628 additions and 544 deletions

57
.gitignore vendored
View file

@ -1,4 +1,55 @@
jrnl.egg-info
build
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
*.py[co]
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
.DS_Store
.travis-solo
Icon
# Documentation
_build
_sources
_static
*.html
objects.inv
searchindex.js
docs/_themes/jrnl/static/css/jrnl.css
# MS Visual Studio (PyTools)
obj
*.pyproj
*.sln
*.suo
# virtaulenv
env/
env*/
docs/_themes/jrnl/static/less/3L.less
# PyCharm Project files
.idea/
# export testing director
exp/
_extras/
*.sublime-*

18
.travis.yml Normal file
View file

@ -0,0 +1,18 @@
sudo: false
language: python
cache: pip
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
- "nightly"
install:
- "pip install -e ."
- "pip install -q behave"
before_script:
- python --version
# command to run tests
script:
- behave

View file

@ -1,34 +1,171 @@
Changelog
=========
### 0.2.2
## 2.0
* Adds --encrypt and --decrypt to encrypt / descrypt existing journal files
* Cryptographical backend changed from PyCrypto to cryptography.io
* Config now respects XDG conventions and may move accordingly
* Config now saved as YAML
* Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`
### 0.2.1
### 1.9 (July 21, 2014)
* Submitted to [PyPi](http://pypi.python.org/pypi/jrnl/0.2.1).
* __1.9.5__ Multi-word tags for DayOne Journals
* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
* __1.9.3__ Fixed: Tags at the beginning of lines
* __1.9.2__ Fixed: Tag search ignores email-addresses (thanks to @mjhoffman65)
* __1.9.1__ Fixed: Dates in the future can be parsed as well.
* __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering
### 0.2.0
### 1.8 (May 22, 2014)
* Encrypts using CBC
* `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.)
* __1.8.7__ Fixed: -from and -to filters are inclusive (thanks to @grplyler)
* __1.8.6__ Improved: Tags like @C++ and @OS/2 work, too (thanks to @chaitan94)
* __1.8.5__ Fixed: file names when exporting to individual files contain full year (thanks to @jdevera)
* __1.8.4__ Improved: using external editors (thanks to @chrissexton)
* __1.8.3__ Fixed: export to text files and improves help (thanks to @igniteflow and @mpe)
* __1.8.2__ Better integration with environment variables (thanks to @ajaam and @matze)
* __1.8.1__ Minor bug fixes
* __1.8.0__ Official support for python 3.4
### 0.1.1
### 1.7 (December 22, 2013)
* Removed unnecessary print commands
* Created the documentation
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters.
* __1.7.21__ jrnl now uses PKCS#7 padding.
* __1.7.20__ Minor fixes when parsing DayOne journals
* __1.7.19__ Creates full path to journal during installation if it doesn't exist yet
* __1.7.18__ Small update to parsing regex
* __1.7.17__ Fixes writing new lines between entries
* __1.7.16__ Even more unicode fixes!
* __1.7.15__ More unicode fixes
* __1.7.14__ Fix for trailing whitespaces (eg. when writing markdown code block)
* __1.7.13__ Fix for UTF-8 in DayOne journals
* __1.7.12__ Fixes a bug where filtering by tags didn't work for DayOne journals
* __1.7.11__ `-ls` will list all available journals (Thanks @jtan189)
* __1.7.10__ Supports `-3` as a shortcut for `-n 3` and updates to tzlocal 1.1
* __1.7.9__ Fix a logic bug so that jrnl -h and jrnl -v are possible even if jrnl not configured yet.
* __1.7.8__ Upgrade to parsedatetime 1.2
* __1.7.7__ Cleaned up imports, better unicode support
* __1.7.6__ Python 3 port for slugify
* __1.7.5__ Colorama is only needed on Windows. Smaller fixes
* __1.7.3__ Touches temporary files before opening them to allow more external editors.
* __1.7.2__ Dateutil added to requirements.
* __1.7.1__ Fixes issues with parsing time information in entries.
* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`.
### 0.1.0
* Supports encrypted journals using AES encryption
* Support external editors for composing entries
### 1.6 (November 5, 2013)
### 0.0.2
* __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py
* __1.6.5__ Allows composing multi-line entries on the command line or importing files
* __1.6.4__ Fixed a bug that caused creating encrypted journals to fail
* __1.6.3__ New, pretty, _useful_ documentation!
* __1.6.2__ Starring entries now works for plain-text journals too!
* __1.6.1__ Attempts to fix broken config files automatically
* __1.6.0__ Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
* Filtering by tags and dates
* Now using dedicated classes for Journals and entries
### 1.5 (August 6, 2013)
### 0.0.1
* __1.5.7__ 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__ DayOne journals can now handle tags
* __1.5.3__ Fixed: DayOne integration with older DayOne Journals
* __1.5.2__ 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__ 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`
* Composing entries works. That's pretty much it.
### 1.4 (July 22, 2013)
* __1.4.2__ Fixed: Tagging works again
* __1.4.0__ 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 (July 17, 2013)
* __1.3.2__ Everything that is not direct output of jrnl will be written stderr to improve integration
* __1.3.0__ Export to multiple files
* __1.3.0__ Feature to export to given output file
### 1.2 (July 15, 2013)
* __1.2.0__ Fixed: Timezone support for DayOne
### 1.1 (June 9, 2013)
* __1.1.1__ Fixed: Unicode and Python3 issues resolved.
* __1.1.0__
* JSON export exports tags as well.
* Nicer error message when there is a syntactical error in your config file.
* Unicode support
### 1.0 (March 4, 2013)
* __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7
* __1.0.4__
* Python 2.6 compatibility
* Better utf-8 support
* Python 3 compatibility
* Respects the `XDG_CONFIG_HOME` environment variable for storing your configuration file (Thanks [evaryont](https://github.com/evaryont))
* __1.0.3__
* Removed clint in favour of colorama
* Fixed: Fixed a bug where showing tags failed when no tags are defined.
* Fixed: Improvements to config parsing (Thanks [alapolloni](https://github.com/alapolloni))
* Fixed: Fixes readline support on Windows
* Fixed: Smaller fixes and typos
* __1.0.1__ (March 12, 2013) Fixed: Requires parsedatetime 1.1.2 or newer
* __1.0.0__
* Integrates seamlessly with DayOne
* Each journal can have individual settings
* Fixed: A bug where jrnl would not go into compose mode
* Fixed: A bug where jrnl would not add entries without timestamp
* Fixed: Support for parsedatetime 1.x
### 0.3 (May 24, 2012)
* __0.3.2__ Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
* __0.3.1__
* Supports deleting of last entry.
* Fixed: Fixes a bug where --encrypt or --decrypt without a target file would not work.
* Supports a config option for setting word wrap.
* Supports multiple journal files.
* __0.3.0__
* Fixed: Dates such as "May 3" will now be interpreted as being in the past if the current day is at least 28 days in the future
* Fixed: Bug where composed entry is lost when the journal file fails to load
* Changed directory structure and install scripts (removing the necessity to make an alias from `jrnl` to `jrnl.py`)
### 0.2 (April 16, 2012)
* __0.2.4__
* Fixed: Parsing of new lines in journal files and entries
* Adds support for encrypting and decrypting into new files
* __0.2.3__
* Adds a `-short` option that will only display the titles of entries (or, when filtering by tags, the context of the tag)
* Adds tag export
* Adds coloured highlight of tags (by default, highlights all tags - when filtering by tags, only highlights search tags)
* `.jrnl_config` will get automatically updated when updating jrnl to a new version
* __0.2.2__
* Adds --encrypt and --decrypt to encrypt / decrypt existing journal files
* Adds markdown export (kudos to dedan)
* __0.2.1__ Submitted to [PyPi](http://pypi.python.org/pypi/jrnl/0.2.1).
* __0.2.0__
* Encrypts using CBC
* Fixed: `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.)
### 0.1 (April 13, 2012)
* __0.1.1__
* Fixed: Removed unnecessary print commands
* Created the documentation
* __0.1.0__
* Supports encrypted journals using AES encryption
* Support external editors for composing entries
* __0.0.2__
* Filtering by tags and dates
* Fixed: Now using dedicated classes for Journals and entries
### 0.0 (March 29, 2012)
* __0.0.1__ Composing entries works. That's pretty much it.

35
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,35 @@
Contributing
============
If you use jrnl, you can totally make my day by just saying "thanks for the code" or by [tweeting about jrnl](https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fmaebert.github.io%2Fjrnl&via=maebert). It's your chance to make a programmer happy today! If you have a minute or two, let me know what you use jrnl for and how, it'll help me to make it even better. If you blog about jrnl, I'll send you a post card!
Docs & Typos
------------
If you find a typo or a mistake in the docs, just fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.rst` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, and then open `docs/_build/html/index.html` in your browser. Note that this requires [lessc](http://lesscss.org/#using-less-installation) and [Sphinx](https://pypi.python.org/pypi/Sphinx) to be installed. Changes to the CSS or Javascript should be made on `docs/_themes/jrnl/`. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that.
Bugs
----
They unfortunately happen. Specifically, I don't have a Windows machine to test on, so expect a few rough spots. If you found a bug, please [open a new issue](https://www.github.com/maebert/jrnl/issues/new) and describe it as well as possible. If you're a programmer and have a little time time spare, go ahead, fork the code and fix bugs you spot, it'll be much appreciated!
Feature requests and ideas
--------------------------
So, you have an idea for a great feature? Awesome. I love you. As with bugs, first you should [open a new issue](https://www.github.com/maebert/jrnl/issues/new) on GitHub, describe the use case and what the feature should accomplish. If we agree that this feature is useful, it will sooner or later get implemented. Even sooner if you roll up your sleeves and code it yourself ;-)
Keep in mind that the design goal of jrnl is to be _slim_. That means
* having as few dependencies as possible
* creating as little interface as possible to boost the learning curve
* doing one thing and one thing well
Beyond that, it should also play nice with other software and tools -- however, avoid duplicating functionality that existing tools already provide. For example, we played around with the idea of a git integrated journal so new entries would be stored in commits. However, the proposed implementation required a rather heavy git module for python as an dependency, and the same feature could be implemented with a little bit of shell scripting around jrnl.
A short note for new programmers and programmers new to python
--------------------------------------------------------------
Although jrnl grew quite a bit since I first started working on it, the overall complexity (for an end-user program) is fairly low, and I hope you'll find the code easy enough to understand -- if you have a question, don't hesitate to ask! Python is known for it's great community and openness to novice programmers. Feel free to fork the code and play around with it. If you think you created something worth sharing, create a pull request. I never expect pull requests to be perfect, idiomatic, instantly mergeable code, and we can work through it together. Go for it!

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 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.

3
MANIFEST.in Normal file
View file

@ -0,0 +1,3 @@
include LICENSE
include README.md
include CHANGELOG.md

39
Makefile Normal file
View file

@ -0,0 +1,39 @@
# A Makefile for commands I run frequently:
clean:
rm -rf dist
rm -rf _static
rm -rf jrnl.egg-info
rm -rf docs/_build
rm -rf _build
rm -rf _sources
rm -rf _static
rm -f *.html
html:
curl https://raw.githubusercontent.com/mateuszkocz/3l/master/3L/3L.less > docs/_themes/jrnl/static/less/3L.less ;\
lessc --clean-css docs/_themes/jrnl/static/less/jrnl.less docs/_themes/jrnl/static/css/jrnl.css ;\
cd docs ;\
make html ;\
cd .. ;\
open docs/_build/html/index.html ;\
# Build GitHub Page from docs
docs:
git checkout gh-pages ; \
git checkout master docs ; \
git checkout master jrnl ; \
curl https://raw.githubusercontent.com/mateuszkocz/3l/master/3L/3L.less > docs/_themes/jrnl/static/less/3L.less ;\
lessc --clean-css docs/_themes/jrnl/static/less/jrnl.less docs/_themes/jrnl/static/css/jrnl.css ; \
cd docs ; \
make html ; \
cd .. ; \
cp -r docs/_build/html/* . ; \
git add -A ; \
git commit -m "Updated docs from master" ; \
git push -u origin gh-pages ; \
git checkout master
# Upload to pipy
dist:
python setup.py publish

127
README.md
View file

@ -1,7 +1,11 @@
jrnl
jrnl [![Build Status](http://img.shields.io/travis/maebert/jrnl.svg?style=flat)](https://travis-ci.org/maebert/jrnl) [![Downloads](http://img.shields.io/pypi/dm/jrnl.svg?style=flat)](https://pypi.python.org/pypi/jrnl/) [![Version](http://img.shields.io/pypi/v/jrnl.svg?style=flat)](https://pypi.python.org/pypi/jrnl/)
====
*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 syncinc 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.
_For news on updates or to get help, [read the docs](http://maebert.github.io/jrnl), 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.
Optionally, your journal can be encrypted using the [256-bit AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
@ -18,131 +22,20 @@ to make a new entry, just type
and hit return. `yesterday:` will be interpreted as a timestamp. Everything until the first sentence mark (`.?!`) will be interpreted as the title, the rest as the body. In your journal file, the result will look like this:
2012-03-29 09:00 Called in sick.
2012-03-29 09:00 Called in sick.
Used the time to clean the house and spent 4h on writing my book.
If you just call `jrnl`, you will be prompted to compose your entry - but you can also configure _jrnl_ to use your external editor.
Usage
-----
_jrnl_ has to modes: __composing__ and __viewing__.
### Viewing:
jrnl -n 10
will list you the ten latest entries,
jrnl -from "last year" -to march
everything that happened from the start of last year to the start of last march.
### Using Tags:
Keep track of people, projects or locations, by tagging them with an `@` in your entries:
jrnl Had a wonderful day on the #beach with @Tom and @Anna.
You can filter your journal entries just like this:
jrnl @pinkie @WorldDomination
Will print all entries in which either `@pinkie` or `@WorldDomination` occurred.
jrnl -n 5 -and @pineapple @lubricant
the last five entries containing both `@pineapple` __and__ `@lubricant`. You can change which symbols you'd like to use for tagging in the configuration.
> __Note:__ `jrnl @pinkie @WorldDomination` will switch to viewing mode because although now command line arguments are given, all the input strings look like tags - _jrnl_ will assume you want to filter by tag.
### Smart timestamps:
Timestamps that work:
* at 6am
* yesterday
* last monday
* sunday at noon
* 2 march 2012
* 7 apr
* 5/20/1998 at 23:42
Installation
------------
You can install _jrnl_ manually by cloning the repository:
git clone git://github.com/maebert/jrnl.git
cd jrnl
python setup.py install
or by using pip:
Install _jrnl_ using pip:
pip install jrnl
Afterwards, you may want to create an alias in your `.bashrc` or `.bash_profile` or whatever floats your shell:
Or, if you want the option to encrypt your journal,
alias jrnl="jrnl.py"
pip install jrnl[encrypted]
### Known Issues
_jrnl_ relies on the `Crypto` package to encrypt journals, which has some known problems in automatically installing within virtual environments.
Advanced usage
--------------
The first time launched, _jrnl_ will create a file called `.jrnl_config` in your home directory.
### .jrnl_config
It's just a regular `json` file:
{
journal: "~/journal.txt",
editor: "",
encrypt: false,
password: ""
tagsymbols: '@'
default_hour: 9,
default_minute: 0,
timeformat: "%Y-%m-%d %H:%M",
}
- `journal`: path to your journal file
- `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
> __Note on `tagsymbols`:__ Although it seems intuitive to use the `#` character for tags, there's a drawback: on most shells, this is interpreted as a meta-character starting a comment. This means that if you type
>
> jrnl Implemented endless scrolling on the #frontend of our website.
>
> your bash will chop off everything after the `#` before passing it to _jrnl_). To avoid this, wrap your input into quotation marks like this:
>
> jrnl "Implemented endless scrolling on the #frontend of our website."
>
> Or use the built-in prompt or an external editor to compose your entries.
### JSON export
Can do:
jrnl -json
Why not create a beautiful [timeline](http://timeline.verite.co/) of your journal?
### Encryption
Should you ever want to decrypt your journal manually, you can do so with any program that supports the AES algorithm. The key used for encryption is the SHA-256-hash of your password, and the IV (initialisation vector) is stored in the first 16 bytes of the encrypted file. So, to decrypt a journal file in python, run
import hashlib, Crypto.Cipher
key = hashlib.sha256(my_password).digest()
with open("my_journal.txt") as f:
cipher = f.read()
crypto = AES.new(key, AES.MODE_CBC, iv = cipher[:16])
plain = crypto.decrypt(cipher[16:])

177
docs/Makefile Normal file
View file

@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jrnl.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jrnl.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/jrnl"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jrnl"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

93
docs/_themes/jrnl/index.html vendored Executable file
View file

@ -0,0 +1,93 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>jrnl- The Command Line Journal</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{{ pathto('_static/css/jrnl.css', 1) }}">
<link rel="apple-touch-icon-precomposed" href="{{ pathto('_static/img/favicon-152.png', 1) }}">
<link rel="shortcut icon" href="{{ pathto('_static/img/favicon.ico', 1) }}">
</head>
<body id="landing" class="landing">
<div id="upper">
<a id="twitter" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fmaebert.github.io%2Fjrnl&via=maebert">Tell your friends</a>
<div id="title">
<img id="logo" src="{{ pathto('_static/img/logo.png', 1) }}" width="90px" height="98px" title="jrnl"/>
<h1>Collect your thoughts and notes <br />without leaving the command line</h1>
</div>
<div id="prompt">
<div class="pleft" onclick="reset(); prev(); return false;"><i class="icon left"></i></div>
<div class="terminal">$ jrnl <span id="args"></span><span id="input">today: Started writing my memoirs. On the command line. Like a boss.</span><div id="output"></div></div>
<div class="pright" onclick="reset(); next(); return false;"><i class="icon right"></i></div>
</div>
</div>
<div id="nav">
<a href="{{ pathto('overview') }}" title="Documentation">Documentation</a>
<a href="http://github.com/maebert/jrnl" title="View on Github">Fork me on GitHub</a>
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fmaebert.github.io%2Fjrnl&via=maebert">Tell your friends on twitter</a>
<a href="{{ pathto('installation') }}" title="Quick Start" class="cta">Download &#9654;</a>
</div>
<div id="lower">
<div class="row3">
<div class="col">
<i class="icon nli"></i>
<h3>Human friendly.</h3>
<p>jrnl has a natural-language interface so you don't have to remember cryptic shortcuts when you're writing down your thoughts.</p>
</div>
<div class="col">
<i class="icon future"></i>
<h3>Future-proof.</h3>
<p>your journals are stored in plain-text files that will still be readable in 50 years when all your fancy iPad apps will have gone the way of the Dodo.</p>
</div>
<div class="col">
<i class="icon secure"></i>
<h3>Secure.</h3>
<p>Encrypt your journals with the military-grade AES encryption. Even the NSA won't be able to read your dirty secrets.</p>
</div>
</div>
<div class="row4">
<div class="col">
<i class="icon sync"></i>
<h3>Accessible anywhere.</h3>
<p>Sync your journals with Dropbox and capture your thoughts where ever you are</p>
</div>
<div class="col">
<i class="icon dayone"></i>
<h3>DayOne compatible.</h3>
<p>Read, write and search your DayOne journal from the command line.</p>
</div>
<div class="col">
<i class="icon github"></i>
<h3>Free &amp; Open Source.</h3>
<p>jrnl is made by a bunch of really friendly and remarkably attractive people. Maybe even <a href="https://www.github.com/maebert/jrnl" title="Fork jrnl on GitHub">you</a>?</p>
</div>
<div class="col">
<i class="icon folders"></i>
<h3>For work and play.</h3>
<p>Effortlessly access several journals for all parts of your life.</p>
</div>
</div>
<div class="footer">
{{ copyright }}
</div>
</div>
<script src="{{ pathto('_static/js/landing.js', 1) }}"></script>
<script>
var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src='//www.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</body>
</html>

29
docs/_themes/jrnl/layout.html vendored Executable file
View file

@ -0,0 +1,29 @@
{% if pagename == "index" %}
{% include "index.html" %}
{% else %}
{%- extends "basic/layout.html" %}
{%- block extrahead %}
{{ super() }}
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
<link rel="apple-touch-icon-precomposed" href="{{ pathto('_static/img/favicon-152.png', 1) }}">
<link rel="shortcut icon" href="{{ pathto('_static/img/favicon.ico', 1) }}">
{% endblock %}
{%- block relbar1 %}{% endblock %}
{%- block relbar2 %}{% endblock %}
{%- block sidebar2 %}
<aside>
<a href="{{ pathto('index') }}" id="logolink" title="jrnl"><img class="logo" src="{{ pathto('_static/img/logo.png', 1) }}" width="90px" height="98px" title="jrnl"/></a>
<h2>Documentation</h2>
{{ toctree() }}
{%- include "searchbox.html" %}
</aside>
{% endblock %}
{%- block footer %}
<div class="footer">
{{ copyright }}
</div>
{%- endblock %}
{% endif %}

19
docs/_themes/jrnl/relations.html vendored Executable file
View file

@ -0,0 +1,19 @@
<h3>Related Topics</h3>
<ul>
<li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
{%- for parent in parents %}
<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
{%- endfor %}
{%- if prev %}
<li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
}}">{{ prev.title }}</a></li>
{%- endif %}
{%- if next %}
<li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
}}">{{ next.title }}</a></li>
{%- endif %}
{%- for parent in parents %}
</ul></li>
{%- endfor %}
</ul></li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
docs/_themes/jrnl/static/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
docs/_themes/jrnl/static/img/icons.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/_themes/jrnl/static/img/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
docs/_themes/jrnl/static/img/logo@2x.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

BIN
docs/_themes/jrnl/static/img/twitter.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

109
docs/_themes/jrnl/static/js/landing.js vendored Normal file
View file

@ -0,0 +1,109 @@
var phrases = [
["", "today: Started writing my memoirs. On the command line. Like a boss.", ""],
["", "yesterday 2pm: used jrnl to keep track of accomplished tasks. The done.txt for my todo.txt", ""],
["-from 2009 -until may", "", "(Displays all entries from January 2009 to last may)"],
["", "A day on the beach with @beth and @frank. Taggidy-tag-tag.", ""],
["--tags", "", "@idea 7<br />@beth 5"],
["--export json", "", "(Exports your entire journal to json)"],
["--encrypt", "", "(256 bit AES encryption. Crack this, NSA.)"]
]
var args = document.getElementById("args");
var input = document.getElementById("input");
var output = document.getElementById("output");
var current = 0
var timer = null;
var fadeInTimer = null;
var fadeOutTimer = null;
var letterTimer = null;
var unletterTimer = null;
var next = function() {
current = (current + 1) % phrases.length;
reveal(current);
timer = setTimeout(next, 5000);
}
var prev = function() {
current = (current === 0) ? phrases.length - 1 : current - 1;
reveal(current);
timer = setTimeout(next, 5000);
}
var reveal = function(idx) {
var args_text = phrases[idx][0];
var input_text = phrases[idx][1];
var output_text = phrases[idx][2];
var old_dix = idx == 0 ? phrases.length - 1 : idx - 1;
console.log(idx, old_dix, "++++++++++++")
var old_args_text = args.innerHTML;
var old_input_text = input.innerHTML;
var old_output_text = output.innerHTML;
console.log(args_text, input_text, output_text)
console.log(old_args_text, old_input_text, old_output_text)
var s4 = function() {fadeIn(output_text, output);}
var s3 = function() {letter(input_text, input, s4);}
var s2 = function() {letter(args_text, args, s3);}
var s1 = function() {unletter(old_args_text, args, s2);}
var s0 = function() {unletter(old_input_text, input, s1);}
fadeOut(old_output_text, output, s0, 10);
}
var fadeIn = function(text, element, next, step) {
step = step || 0
var nx = function() { fadeIn(text, element, next, ++step); }
if (step==0) {
element.innerHTML = "";
fadeInTimer = setTimeout(nx, 550);
return;
}
if (step==1) {element.innerHTML = text;}
if (step>10 || !text) { if (next) {next(); return;} else return;}
element.style.opacity = (step-1)/10;
element.style.filter = 'alpha(opacity=' + (step-1)*10 + ')';
fadeInTimer = setTimeout(nx, 50);
}
var fadeOut = function(text, element, next, step) {
if (step===10) element.innerHTML = text;
if (step<0 || !text) {
element.innerHTML = "";
if (next) {next(); return;}
else return;
}
element.style.opacity = step/10;
element.style.filter = 'alpha(opacity=' + step*10 + ')';
var nx = function() { fadeOut(text, element, next, --step); }
fadeOutTimer = setTimeout(nx, 50);
}
var unletter = function(text, element, next, timeout, index) {
timeout = timeout||10;
if (index==null) index = text.length;
if (index==-1 || !text.length) { if (next) {next(); return;} else return;}
element.innerHTML = text.substring(0, index);
var nx = function() { unletter(text, element, next, timeout, --index); }
unletterTimer = setTimeout(nx, timeout);
}
var letter = function(text, element, next, timeout, index) {
timeout = timeout||35;
index = index||0;
if (index > text.length || !text.length) { if (next) {next(); return;} else return;}
element.innerHTML = text.substring(0, index);
var nx = function() { letter(text, element, next, timeout, ++index); }
letterTimer = setTimeout(nx, timeout);
}
var reset = function() {
var timers = [timer, fadeInTimer, fadeOutTimer, letterTimer, unletterTimer];
timers.forEach(function (t) {
clearTimeout(t);
});
args.innerHTML = "";
input.innerHTML = "";
output.innerHTML = "";
}
timer = setTimeout(next, 3000);

3558
docs/_themes/jrnl/static/landing.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 208 KiB

283
docs/_themes/jrnl/static/less/docs.less vendored Normal file
View file

@ -0,0 +1,283 @@
body
{
font-family: "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 300;
color: #333;
background: @white;
}
body:not(.landing)
{
padding:0px 20px;
padding-top: 40px;
h2
{
margin-top: 40px;
}
}
input
{
background: transparent;
border: 1px solid #999;
.border-radius(3px);
padding: 2px 5px;
color: #666;
font-family: "Open Sans";
font-weight: 300;
outline: none;
&:focus
{
background: white;
}
}
div.related
{
background: rgba(255,200,200,.2);
}
* > a.headerlink
{
display: none;
}
h1, h2, h3, h4, h5, h6
{
font-weight: 300;
}
a:link, a:visited
{
color: @orange;
text-decoration: none;
}
a:hover, a:active
{
text-decoration: underline;
color: lighten(@orange, 10);
}
.literal
{
color: @purple;
font-size: 1em;
background: lighten(@purple-light, 45);
padding: 1px 2px;
.border-radius(2px);
.box-shadow(inset 0px 0px 0px 1px lighten(@purple-light, 30));
}
.note
{
.gradient(lighten(@purple-light, 10), lighten(@purple-light-shade, 10));
.border-radius(5px);
.box-shadow(0px 2px 3px @purple-shade);
padding: 10px 20px 10px 70px;
position: relative;
color: white;
.admonition-title {display: none;}
a { color: lighten(@orange, 30);}
&:before
{
content: "";
display: block;
.icon;
.icon.info;
position: absolute;
margin: auto;
top: 0; bottom: 0; left: 20px;
}
.literal, .highlight-note
{
color: white;
background: darken(@purple-light, 3);
padding: 1px 3px;
.border-radius(2px);
.box-shadow(inset 0px 0px 0px 1px lighten(@purple-light, 10));
}
.highlight-note
{
padding: 1px 10px;
pre:before
{
content: "$ ";
color: @orange;
}
}
}
.highlight
{
background:transparent !important;
}
.highlight-output, .highlight-javascript, .highlight-sh
{
.pre-block;
background: desaturate(lighten(@terminal,10), 10);
pre {color: white;}
}
.highlight-python
{
.terminal;
pre
{
margin: 0 0 10px 0;
&:before
{
content: "$ ";
color: @orange;
}
}
}
*:hover > a.headerlink
{
display: inline;
color: lighten(@purple-light, 30);
margin-left: 10px;
text-decoration: none;
&:hover { color: @purple-light; }
}
tt
{
color: @purple;
font-size: 1.2em;
}
ul li {
margin-bottom: 10px;
}
div.document
{
max-width: 900px;
margin: 20px auto;
position: relative;
}
div.documentwrapper
{
margin-left: 240px;
padding: 0;
}
aside
{
position: absolute;
width: 220px;
top: 0px;
.logo
{
margin: 0 auto 20px auto;
display: block;
width: 90px;
height: 98px;
}
color: #999;
h2, h3, h3 a:link, h3 a:visited
{
color: #777;
}
a:link, a:visited
{
color: #999;
}
a:hover, a:active
{
color: @orange;
}
input[type=submit]
{
display: none;
}
&>ul
{
margin: 0 4px;
padding: 0;
list-style: none;
&>li
{
margin-bottom: 10px;
font-size: 18px;
color: #777;
a:link, a:visited {color: #777;}
ul
{
margin: 10px 0 0 0;
padding-left: 20px;
font-size: 16px;
color: #999;
a:link, a:visited {color: #999;}
}
}
}
}
div.footer
{
font-size: .8em;
text-align: center;
margin: 40px 0;
color: #999;
a:link, a:visited {color: #555;}
}
@media screen and (max-width: 820px)
{
body:not(.landing){
padding-top: 130px;
.highlight-output,.highlight-python, .highlight-javascript
{
width: auto;
max-width: 500px;
}
.highlight-python
{
pre { margin: -10px 0 10px 0;}
&:before
{
height: 24px !important;
line-height: 24px;
font-size: .7em;
}
&:after
{
background: none;
}
}
aside
{
position: static;
}
div.documentwrapper
{
margin: 0px;
}
h1, .section
{
margin: 0px !important;
}
aside
{
background-color: #f0f0f0;
width: 100%;
margin: 5px -20px;
padding: 5px 20px 10px 20px;
}
#logolink
{
position: absolute;
top: -120px;
left: 50%;
margin-left: -49px;
}
}
}
@media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)
{
aside .logo, body#landing #upper #logo
{
width: 90px;
height: 98px;
content: url(../img/logo@2x.png);
}
}

317
docs/_themes/jrnl/static/less/jrnl.less vendored Normal file
View file

@ -0,0 +1,317 @@
@import "retina";
@import "3L";
@white: #f7f8f9;
@blue: #5e7dc5;
@blue-light: #7c95ca;
@terminal: #2f1e34;
@purple: #47375d;
@purple-shade: #413155;
@purple-light: #725794;
@purple-light-shade: #564371;
@orange: #deaa09;
.normalize();
@import "docs.less";
.icon,
{
.sprite("../img/icons.png", 32px, 5, 3, 8px);
&.secure {.sprite(0, 0)};
&.future {.sprite(1, 0)};
&.search {.sprite(2, 0)};
&.nli {.sprite(3, 0)};
&.share {.sprite(0, 1)};
&.sync {.sprite(0, 1)};
&.dayone {.sprite(1, 1)};
&.github {.sprite(2, 1)};
&.folders{.sprite(3, 1)};
&.cal {.sprite(4, 1)};
&.left {.sprite(0, 2)};
&.right {.sprite(1, 2)};
&.info {.sprite(2, 2)};
}
.pre-block
{
background: @terminal;
.border-radius(6px);
padding: 1px 20px;
margin: 40px auto;
width: 500px;
.box-shadow(0px 1px 8px darken(@white, 30));
position: relative;
color: @white;
font-family: "Monaco", "Courier New";
font-size: 12pt;
#args {color: #f6f7b9}
#output {color: #9278b5}
}
.terminal
{
.pre-block;
@p: 20px;
padding: @p + 30px @p (@p - 10px) @p;
&:before
{
content: "Terminal";
display: block;
width: 100%;
position: absolute;
left: 0;
.box-shadow(inset 0px 1px 0px #f4f4f4, inset 0px -1px 0px #888);
margin-top: -50px;
// margin: -@p -@p 0px -@p;
text-align: center;
height: 30px;
line-height: 30px;
color: #777;
text-shadow: 0px 1px 0px #ddd;
.border-radius(5px 5px 0px 0px);
.gradient(#eaeaea, #bababa);
}
&:after
{
content: "";
width: 48px;
height: 30px;
position: absolute;
top: 0px;
left: 10px;
background: url(../img/terminal.png) no-repeat center center;
}
}
body#landing
{
background-color: @purple;
font-family: "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 300;
#twitter
{
display: block;
position: absolute;
top: 20px;
right: 20px;
border: 1px solid @purple;
padding: 5px 10px 5px 30px;
color: @purple;
.border-radius(3px);
.opacity(.7);
background: url(../img/twitter.png) 8px center no-repeat transparent;
&:hover, &:active
{
.opacity(1);
text-decoration: none;
}
}
#title, .row3, .row4, #prompt
{
width: 900px;
margin: 0px auto;
}
#upper
{
.clearfix;
background: @white;
.box-shadow(inset 0px -6px 6px -3px darken(@white, 10));
#title
{
width: 650px;
margin: 150px auto 75px auto;
}
img
{
float: left;
margin-right: 30px;
}
h1
{
color: @purple-light-shade;
font-weight: 300;
}
#prompt
{
width: 640px;
margin: 0 auto;
.clearfix;
}
.terminal
{
.border-radius(6px 6px 0px 0px);
float: left;
margin: 0px;
width: 500px;
min-height: 134px;
.border-box-sizing;
}
.pleft, .pright
{
text-align: center;
.border-box-sizing;
float: left;
padding-top: 50px;
width: 70px;
i {.opacity(60);}
i:hover {.opacity(1000); cursor: pointer;}
}
}
#nav
{
.gradient(@blue-light, @blue);
height: 60px;
.box-shadow(0px 6px 6px -3px @purple-shade);
text-align: center;
a#twitter-nav {display: none;}
a
{
color: @white;
text-shadow: 0px -1px 0px darken(@blue, 30);
text-decoration: none;
font-size: 14pt;
line-height: 60px;
margin: 0 40px;
&:hover
{
color: lighten(@orange, 20);
text-shadow: 0px -1px 0px darken(@orange, 15);
}
}
a.cta
{
.gradient(@purple-light, @purple-light-shade);
.box-shadow(0px 1px 0px @purple-shade);
.border-radius(5px);
padding: 6px 10px 5px 10px;
white-space: nowrap;
&:hover
{
.gradient(lighten(@orange, 10), darken(@orange, 5));
.box-shadow(0px 1px 0px darken(@orange, 15));
text-shadow: 0px -1px 0px darken(@orange, 15);
color: @white;
}
}
}
#lower
{
color: @white;
padding-top: 40px;
a
{
color: @orange;
text-decoration: none;
&:hover
{
color: lighten(@orange, 20);
text-decoration: underline;
}
}
.row3, .row4 {
.clearfix;
margin-bottom: 20px;
.col
{
position: relative;
padding-left: 40px;
i
{
position: absolute;
left: 0;
top: 16px;
}
h3 {font-size: 12pt; margin-bottom: .5em;}
p {font-size: 10pt; margin: 0;}
float: left;
width: 25%;
padding-right: 2%;
.border-box-sizing;
&:last-child {padding-right: 0;}
}
}
.row3 .col { width: 33.3333%; }
.row4 .col { color: mix(@white, @purple, 80); i {.opacity(80);}}
}
}
@media screen and (max-width: 680px)
{
body#landing
{
#nav
{
height: auto;
padding-bottom: 10px;
a, a#twitter-nav
{
display: block;
}
a.cta
{
margin: 10px;
padding: 1px;
}
}
#upper
{
#twitter { display: none;}
#title
{
margin: 30px 0 10px 0;
}
#logo
{
backgound: red;
display: block;
float: none;
margin: 0px auto;
}
#title br {display: none;}
.pleft, .pright {display: none;}
#prompt, #title
{
width: 100%;
.border-box-sizing;
padding: 0px 20px;
}
.terminal
{
width: 100%;
}
}
}
}
@media screen and (max-width: 900px)
{
body#landing
{
#lower
{
padding: 40px 20px;
.row3, .row4
{
margin: 0px;
width: auto;
}
.row3 .col, .row4 .col
{
float: none;
width: 100%;
text-align: center;
padding: 0px;
margin: 0 0 40px 0;
h3 {font-size: 1.5em;}
p {font-size: 1em;}
i
{
position: static;
margin-bottom: -20px;
}
}
}
}
}

View file

@ -0,0 +1,35 @@
// A helper mixin for applying high-resolution background images (http://www.retinajs.com)
@highdpi: ~"(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)";
.at2x(@path, @w: auto, @h: auto) {
background-image: url(@path);
@at2x_path: ~`@{path}.replace(/\.\w+$/, function(match) { return "@2x" + match; })`;
background-size: @w @h;
@media @highdpi {
background-image: url("@{at2x_path}");
}
}
// Sprite mixin, see https://coderwall.com/p/oztebw
.sprite (@path, @size, @w, @h, @pad: 0) when (isstring(@path))
{
background-image: url(@path);
width: @size;
height: @size;
display: inline-block;
@at2x_path: ~`@{path}.replace(/\.[\w\?=]+$/, function(match) { return "@2x" + match; })`;
font-size: @size + @pad;
background-size: (@size + @pad) * @w (@size + @pad) * @h;
@media @highdpi
{
background-image: url("@{at2x_path}");
}
}
.sprite(@x, @y)
{
background-position: -@x * 1em -@y * 1em;
}

96
docs/_themes/jrnl/static/sprites.svg vendored Executable file
View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: IcoMoon.io -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1008"
height="48"
viewBox="0 0 1008 48"
data-tags="bookmark"
style="margin-left: -8px; margin-top: -8px;"
fill="#333333"
id="svg2"
version="1.1"
inkscape:version="0.48.2 r9819"
sodipodi:docname="sprites.svg">
<metadata
id="metadata30">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs28" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1562"
inkscape:window-height="1153"
id="namedview26"
showgrid="true"
inkscape:zoom="2.7460317"
inkscape:cx="651.02496"
inkscape:cy="38.446605"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2">
<inkscape:grid
type="xygrid"
id="grid3007" />
</sodipodi:namedview>
<path
d="M 141.818,22.909l-15.273-6.545l 6.105-4.361C 129.469,8.651, 124.985,6.545, 120,6.545 C 111.545,6.545, 104.5,12.556, 102.892,20.535L 98.819,18.849C 101.136,9.29, 109.728,2.182, 120,2.182c 6.452,0, 12.227,2.819, 16.224,7.27L 141.818,5.455L 141.818,22.909 z M 107.35,35.998C 110.529,39.349, 115.015,41.455, 120,41.455c 8.487,0, 15.552-6.057, 17.123-14.084l 4.069,1.741C 138.888,38.69, 130.287,45.818, 120,45.818 c-6.452,0-12.229-2.819-16.224-7.27L 98.182,42.545l0-17.455 l 15.273,6.545L 107.35,35.998z"
id="path6" />
<path
d="M 224.71875 2.09375 L 224.71875 10.8125 C 224.71875 10.8125 201.34725 10.877 200.78125 36.125 C 201.12625 32.307 203.78675 17.375 224.71875 17.375 L 224.71875 23.90625 L 240 13 L 224.71875 2.09375 z M 198 7 C 194.676 7 192 9.676 192 13 L 192 40 C 192 43.324 194.676 46 198 46 L 229 46 C 232.324 46 235 43.324 235 40 L 235 23 L 232 25 L 232 40 C 232 41.662 230.662 43 229 43 L 198 43 C 196.338 43 195 41.662 195 40 L 195 13 C 195 11.338 196.338 10 198 10 L 210 10 L 217 7 L 198 7 z "
id="path8" />
<path
d="M 318,0.007c-9.941,0-18,8.059-18,18c0,3.039, 0.76,5.899, 2.093,8.412l-12.516,12.513l 0.011,0.009 C 288.609,39.903, 288,41.235, 288,42.715c0,2.924, 2.37,5.293, 5.293,5.293c 1.478,0, 2.811-0.609, 3.772-1.588l-0.003-0.003l 12.511-12.51 c 2.514,1.337, 5.379,2.1, 8.425,2.1c 9.941,0, 18-8.059, 18-18C 336,8.067, 327.941,0.007, 318,0.007z M 295.192,44.545 c-0.483,0.501-1.152,0.815-1.899,0.815c-1.462,0-2.647-1.183-2.647-2.646c0-0.747, 0.315-1.414, 0.815-1.899l-0.013-0.012l 12.099-12.099 c 1.057,1.426, 2.317,2.686, 3.741,3.747L 295.192,44.545z M 318,33.009c-8.283,0-15-6.719-15-15c0-8.283, 6.717-15, 15-15 c 8.281,0, 15,6.717, 15,15C 333,26.291, 326.282,33.009, 318,33.009zM 318,7.508 C 318.412,7.508 318.75,7.844 318.75,8.258 C 318.75,8.671 318.412,9.008 318,9.008 C 313.029,9.008 309,13.038 309,18.008 C 309,18.422 308.664,18.758 308.25,18.758 C 307.836,18.758 307.5,18.422 307.5,18.008 C 307.5,12.208 312.2,7.508 318,7.508 Z"
id="path10" />
<path
d="M 421.5,19.5L 421.5,13.5 c0-7.457-6.043-13.5-13.5-13.5c-7.457,0-13.5,6.043-13.5,13.5l0,6 c-2.486,0-4.5,2.014-4.5,4.5l0,4.5 l0,1.5 l0,3 l0,1.5 c0,7.457, 6.043,13.5, 13.5,13.5l 9,0 c 7.457,0, 13.5-6.043, 13.5-13.5l0-1.5 l0-3 l0-1.5 l0-4.5 C 426,21.513, 423.984,19.5, 421.5,19.5z M 397.5,13.5c0-5.799, 4.701-10.5, 10.5-10.5c 5.799,0, 10.5,4.701, 10.5,10.5l0,6 l-3,0 L 415.5,13.503 c0-4.143-3.357-7.5-7.5-7.5c-4.143,0-7.5,3.357-7.5,7.5L 400.5,19.5 L 397.5,19.5 L 397.5,13.5 z M 414,13.5l0,0.005 L 414,19.5 l-12,0 L 402,13.503 L 402,13.5 c0-3.314, 2.686-6, 6-6C 411.313,7.5, 414,10.187, 414,13.5z M 423,28.5 l0,1.5 l0,3 l0,1.5 c0,5.788-4.712,10.5-10.5,10.5l-9,0 c-5.788,0-10.5-4.712-10.5-10.5l0-1.5 l0-3 l0-1.5 l0-4.5 c0-0.828, 0.672-1.5, 1.5-1.5c 1.001,0, 1.999,0, 3,0l 21,0 c 0.999,0, 1.998,0, 3,0 c 0.827,0, 1.5,0.672, 1.5,1.5L 423,28.5 zM 408,28.5 C 409.656,28.5 411,29.843 411,31.5 C 411,32.413 410.508,34.152 410.001,35.523 C 409.591,36.63 409.173,37.497 408,37.497 C 406.922,37.497 406.409,36.621 406,35.508 C 405.5,34.14 405,32.41 405,31.5 C 405,29.843 406.344,28.5 408,28.5 Z"
id="path12" />
<path
d="M 524.093,3.87C 521.625,1.405, 518.376,0, 515.174,0c-2.701,0-5.189,1.002-7.005,2.816l-7.3,7.356 c-0.022,0.021-0.048,0.035-0.071,0.057c-0.012,0.012-0.019,0.028-0.032,0.039l 0.003,0.003L 485.276,25.884 c-0.714,0.71-1.232,1.593-1.519,2.558l-3.524,12.762C 480.229,41.238, 480,42.24, 480,42.75C 480,45.648, 482.353,48, 485.256,48 c 0.578,0, 1.695-0.276, 1.736-0.282l 12.717-3.344c 0.966-0.286, 1.844-0.808, 2.558-1.524l 22.895-23.075 C 529.325,15.609, 528.855,8.625, 524.093,3.87z M 504.021,35.693c-0.123-1.353-0.506-2.68-1.079-3.94l 14.183-14.181 c 0.867,2.739, 0.422,5.604-1.479,7.506c-0.012,0.012-0.027,0.019-0.038,0.032l 0.021,0.019l-11.592,11.685 C 504.037,36.439, 504.055,36.073, 504.021,35.693z M 502.189,30.384c-0.559-0.919-1.196-1.808-1.983-2.594 c-0.916-0.916-1.968-1.635-3.066-2.238l 14.298-14.298c 1.122,0.498, 2.198,1.208, 3.147,2.157c 0.812,0.808, 1.438,1.715, 1.921,2.656 L 502.189,30.384z M 495.729,24.843c-1.389-0.559-2.844-0.879-4.302-0.898l 11.555-11.643c 1.768-1.725, 4.344-2.222, 6.88-1.593 L 495.729,24.843z M 486.25,44.809C 486.087,44.847, 485.579,44.976, 485.233,45C 484,44.985, 483,43.983, 483,42.75 c 0.018-0.252, 0.118-0.685, 0.153-0.843l 1.579-5.721c 1.715-0.046, 3.56,0.621, 5.010,2.075c 1.473,1.47, 2.166,3.351, 2.091,5.087 L 486.25,44.809z M 493.311,42.956c-0.036-2.013-0.855-4.107-2.508-5.757C 489.24,35.634, 487.194,34.731, 485.154,34.65l 1.494-5.411 c 0.108-0.36, 0.323-0.716, 0.587-1.026c 3.009-2.154, 7.636-1.518, 10.851,1.7c 3.401,3.399, 3.925,8.379, 1.306,11.352 c-0.174,0.091-0.35,0.178-0.538,0.234L 493.311,42.956z M 523.036,17.658l-2.526,2.546c0-0.339, 0.041-0.664, 0.009-1.011 c-0.264-2.902-1.617-5.709-3.815-7.904c-2.444-2.445-5.684-3.848-8.892-3.857l 2.484-2.505C 511.541,3.687, 513.276,3, 515.174,3 c 2.413,0, 4.893,1.092, 6.8,2.993c 1.79,1.787, 2.856,4.006, 3.009,6.252C 525.123,14.34, 524.431,16.261, 523.036,17.658z"
id="path14" />
<path
d="M 600,10.5 C 600.414,10.5 600.75,10.836 600.75,11.25 C 600.75,11.664 600.412,12 600,12 C 592.009,12 585,16.206 585,21 C 585,21.414 584.664,21.75 584.25,21.75 C 583.836,21.75 583.5,21.414 583.5,21 C 583.5,15.309 591.056,10.5 600,10.5 ZM 600,3C 586.745,3, 576,11.059, 576,21c0,6.191, 4.168,11.649, 10.512,14.889 C 586.512,35.929, 586.5,35.956, 586.5,36c0,2.689-2.008,5.585-2.892,7.104c 0.002,0, 0.003,0, 0.003,0C 583.54,43.269, 583.5,43.45, 583.5,43.641 C 583.5,44.391, 584.107,45, 584.859,45C 585,45, 585.248,44.963, 585.241,44.979c 4.688-0.768, 9.104-5.075, 10.13-6.322C 596.87,38.877, 598.415,39, 600,39 c 13.253,0, 24-8.059, 24-18C 624,11.059, 613.254,3, 600,3z M 600,36c-1.376,0-2.787-0.105-4.194-0.31c-0.146-0.024-0.291-0.032-0.435-0.032 c-0.891,0-1.744,0.396-2.319,1.095c-0.642,0.782-2.469,2.526-4.627,3.809c 0.585-1.343, 1.042-2.847, 1.074-4.398 c 0.009-0.096, 0.013-0.194, 0.013-0.276c0-1.128-0.631-2.159-1.635-2.671C 582.318,30.378, 579,25.811, 579,21C 579,12.729, 588.42,6, 600,6 c 11.577,0, 21,6.729, 21,15C 621,29.271, 611.579,36, 600,36z"
id="path16" />
<path
d="M 774 0 L 771 26.84375 L 773.96875 27.15625 L 776.65625 3 L 807.34375 3 L 810.03125 27.15625 L 813 26.84375 L 810 0 L 774 0 z M 780 6 L 780 9 L 804 9 L 804 6 L 780 6 z M 780 12 L 780 15 L 804 15 L 804 12 L 780 12 z M 780 18 L 780 21 L 804 21 L 804 18 L 780 18 z M 780 24 L 780 27 L 804 27 L 804 24 L 780 24 z M 769.5 30 C 768.675 30 768.20775 30.6545 768.46875 31.4375 L 773.53125 46.5625 C 773.79225 47.3455 774.675 48 775.5 48 L 808.5 48 C 809.325 48 810.20775 47.3455 810.46875 46.5625 L 815.53125 31.4375 C 815.79325 30.6545 815.325 30 814.5 30 L 769.5 30 z M 786 33 L 798 33 L 798 36 L 786 36 L 786 33 z "
id="path20" />
<path
d="M 888 0 C 874.745 0 864 10.745 864 24 C 864 37.255 874.745 48 888 48 C 901.255 48 912 37.255 912 24 C 912 10.745 901.255 0 888 0 z M 888 3 C 899.59798 3 909 12.402018 909 24 C 909 33.513233 902.67471 41.543499 894 44.125 L 894 43.25 L 894 41.53125 L 894 39.65625 C 894 37.76525 893.32825 36.35975 892.03125 35.46875 C 892.84425 35.39075 893.60225 35.29625 894.28125 35.15625 C 894.96025 35.01625 895.6795 34.8275 896.4375 34.5625 C 897.1955 34.2975 897.86675 33.96075 898.46875 33.59375 C 899.07075 33.22675 899.66475 32.74225 900.21875 32.15625 C 900.77275 31.57025 901.21875 30.9295 901.59375 30.1875 C 901.96875 29.4455 902.281 28.539 902.5 27.5 C 902.719 26.461 902.8125 25.3125 902.8125 24.0625 C 902.8125 21.6405 902.04675 19.579 900.46875 17.875 C 901.18775 16 901.09375 13.953 900.21875 11.75 L 899.625 11.6875 C 899.219 11.6405 898.4915 11.82775 897.4375 12.21875 C 896.3835 12.60975 895.21925 13.234 893.90625 14.125 C 892.04725 13.609 890.09375 13.375 888.09375 13.375 C 886.07775 13.375 884.17175 13.61 882.34375 14.125 C 881.51575 13.562 880.742 13.118 880 12.75 C 879.258 12.383 878.66375 12.133 878.21875 12 C 877.77375 11.867 877.35175 11.781 876.96875 11.75 C 876.58575 11.719 876.32775 11.70275 876.21875 11.71875 C 876.10975 11.73475 876.046 11.76525 876 11.78125 C 875.125 14.00025 875.031 16.016 875.75 17.875 C 874.172 19.578 873.375 21.6405 873.375 24.0625 C 873.375 25.3125 873.49975 26.461 873.71875 27.5 C 873.93775 28.539 874.21875 29.4455 874.59375 30.1875 C 874.96875 30.9295 875.445 31.57025 876 32.15625 C 876.555 32.74225 877.149 33.22575 877.75 33.59375 C 878.351 33.96175 879.02325 34.2975 879.78125 34.5625 C 880.53925 34.8275 881.0085 35.01625 881.6875 35.15625 C 882.3665 35.29625 883.1255 35.422 883.9375 35.5 C 882.6565 36.375 882 37.75025 882 39.65625 L 882 41.34375 L 882 43.3125 L 882 44.125 C 873.3253 41.543499 867 33.513233 867 24 C 867 12.402018 876.40203 3 888 3 z "
id="path22" />
<path
d="M 978.072,25.441c-0.267,0.386-0.593,1.352-0.978,1.588c-0.386,0.235-0.821,0.401-1.304,0.492 c-0.485,0.093-0.986,0.134-1.503,0.118l0,2.29 l 3.7,0 L 977.988,39 l 2.987,0 l0-14.994 l-2.376,0 C 978.514,24.578, 978.339,25.056, 978.072,25.441z M 994.5,12l 3,0 c 0.83,0, 1.5-0.672, 1.5-1.5L 999,1.5 c0-0.828-0.67-1.5-1.5-1.5l-3,0 c-0.83,0-1.5,0.672-1.5,1.5l0,9 C 993,11.328, 993.67,12, 994.5,12z M 970.5,12l 3,0 c 0.828,0, 1.5-0.672, 1.5-1.5L 975,1.5 c0-0.828-0.672-1.5-1.5-1.5L 970.5,0 C 969.672,0, 969,0.672, 969,1.5l0,9 C 969,11.328, 969.672,12, 970.5,12z M 1005,6l-3,0 l0,7.5 c0,0.828-0.67,1.5-1.5,1.5l-9,0 c-0.83,0-1.5-0.672-1.5-1.5L 990,6 l-12,0 l0,7.5 c0,0.828-0.672,1.5-1.5,1.5L 967.5,15 c-0.828,0-1.5-0.672-1.5-1.5L 966,6 L 963,6 C 961.344,6, 960,7.344, 960,9l0,36 c0,1.656, 1.344,3, 3,3l 42,0 c 1.656,0, 3-1.344, 3-3L 1008,9 C 1008,7.344, 1006.656,6, 1005,6z M 1005,43.5c0,0.83-0.67,1.5-1.5,1.5L 964.5,45 c-0.828,0-1.5-0.67-1.5-1.5L 963,19.5 c0-0.828, 0.672-1.5, 1.5-1.5l 39,0 c 0.83,0, 1.5,0.672, 1.5,1.5L 1005,43.5 z M 983.977,26.973l 7.452,0 c-1.404,1.728-2.534,3.488-3.397,5.558c-0.862,2.071-0.957,4.227-1.123,6.468l 3.196,0 c 0.013-0.999-0.292-2.078-0.076-3.234c 0.219-1.157, 0.528-2.298, 0.925-3.428c 0.401-1.128, 0.897-2.192, 1.494-3.19 c 0.594-1.001, 1.28-1.844, 2.050-2.529l0-2.613 l-10.522,0 L 983.976,26.973 z"
id="path24" />
<path
style="fill:#333333;fill-opacity:1;stroke:none"
d="M 9.46875,0 C 6.437316,0 4,2.4373158 4,5.46875 l 0,37.0625 C 4,45.562684 6.437316,48 9.46875,48 l 29.0625,0 C 41.562684,48 44,45.562684 44,42.53125 L 44,5.46875 C 44,2.4373158 41.562684,0 38.53125,0 z M 11,3 l 2,0 0,42 -2,0 C 8.784,45 7,43.216 7,41 L 7,7 C 7,4.784 8.784,3 11,3 z m 5,0 11,0 0,13 4,-3 4,3 0,-13 2,0 c 2.216,0 4,1.784 4,4 l 0,34 c 0,2.216 -1.784,4 -4,4 l -21,0 z"
id="rect3009"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssssssssssccssssccccccsssscc" />
<path
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#333333;fill-opacity:1;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
d="M 697 -1 C 696.25371 -1 695.55642 -0.73689699 695 -0.28125 C 694.44358 0.17439699 694 0.87583597 694 1.71875 L 694 3 L 693 3 L 693 4 L 687.5 4 C 687.448 3.99729 687.39579 3.99729 687.34375 4 C 686.61183 4.0767148 685.99606 4.7640806 686 5.5 L 686 6 L 684.5 6 C 683.71462 6.0000785 683.00008 6.7146233 683 7.5 L 683 8.09375 C 682.42224 8.2980557 682.0033 8.8871877 682 9.5 L 682 12.5 C 682.00008 13.285377 682.71462 13.999921 683.5 14 L 686 14 L 686 39 L 684.75 40.1875 C 684.29266 40.451552 683.9953 40.971929 684 41.5 L 684 44.5 C 684.00008 45.285377 684.71462 45.999921 685.5 46 L 693.40625 46 L 700.5 46 L 700.65625 46 L 708.5 46 C 709.28538 45.999921 709.99992 45.285377 710 44.5 L 710 41.5 C 710.005 40.971929 709.70734 40.451552 709.25 40.1875 L 708 39 L 708 14 L 710.5 14 C 711.28538 13.999921 711.99992 13.285377 712 12.5 L 712 9.5 C 711.997 8.8871877 711.57776 8.2980557 711 8.09375 L 711 7.5 C 710.99992 6.7146233 710.28538 6.0000785 709.5 6 L 708 6 L 708 5.5 C 707.99992 4.7146233 707.28538 4.0000785 706.5 4 L 701 4 L 701 3 L 700 3 L 700 1.71875 C 700 0.87583597 699.55642 0.17439702 699 -0.28125 C 698.44358 -0.73689702 697.74629 -1 697 -1 z M 697 1 C 697.20585 1 697.5132 1.1129297 697.71875 1.28125 C 697.9243 1.4495703 698 1.6095221 698 1.71875 L 698 3 L 696 3 L 696 1.71875 C 696 1.6095221 696.0757 1.4495702 696.28125 1.28125 C 696.4868 1.1129298 696.79415 1 697 1 z M 688 7 L 706 7 L 706 8 L 708 8 C 707.9472 8.7370898 708.11489 9.2632977 709 9.0625 L 709 12 L 705 12 L 689 12 L 685 12 L 685 9.15625 C 685.81891 9.2047089 685.94068 8.6267293 686 8 L 688 8 L 688 7 z M 689 14 L 705 14 L 705 41 L 707 42.375 L 707 43 L 687 43 L 687 42.375 L 689 41 L 689 14 z M 691 16 L 691 20 L 693 20 L 693 16 L 691 16 z M 694 16 L 694 20 L 696 20 L 696 16 L 694 16 z M 698 16 L 698 20 L 700 20 L 700 16 L 698 16 z M 701 16 L 701 20 L 703 20 L 703 16 L 701 16 z M 691 21 L 691 25 L 693 25 L 693 21 L 691 21 z M 694 21 L 694 25 L 696 25 L 696 21 L 694 21 z M 698 21 L 698 25 L 700 25 L 700 21 L 698 21 z M 701 21 L 701 25 L 703 25 L 703 21 L 701 21 z M 699.375 29.625 C 698.61451 29.625 698 30.239511 698 31 C 698 31.760489 698.61451 32.375 699.375 32.375 C 700.13549 32.375 700.75 31.760489 700.75 31 C 700.75 30.239511 700.13549 29.625 699.375 29.625 z "
id="path3858" />
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

7
docs/_themes/jrnl/theme.conf vendored Executable file
View file

@ -0,0 +1,7 @@
[theme]
inherit = basic
stylesheet = css/jrnl.css
pygments_style = flask_theme_support.FlaskyStyle
[options]
touch_icon =

129
docs/advanced.rst Normal file
View file

@ -0,0 +1,129 @@
.. _advanced:
Advanced Usage
==============
Configuration File
-------------------
You can configure the way jrnl behaves in a configuration file. By default, this is ``~/.jrnl_config``. If you have the ``XDG_CONFIG_HOME`` variable set, the configuration file will be saved under ``$XDG_CONFIG_HOME/jrnl``.
.. note::
On Windows, The configuration file is typically found at ``C:\Users\[Your Username]\.jrnl_config``.
The configuration file is a simple JSON file with the following options and can be edited with any plain text editor.
- ``journals``
paths to your journal files
- ``editor``
if set, executes this command to launch an external editor for writing your entries, e.g. ``vim``. Some editors require special options to work properly, see :doc:`FAQ <recipes>` for details.
- ``encrypt``
if ``true``, encrypts your journal using AES.
- ``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
- ``highlight``
if ``true``, tags will be highlighted in cyan.
- ``linewrap``
controls the width of the output. Set to ``false`` if you don't want to wrap long lines.
.. note::
Although it seems intuitive to use the `#` character for tags, there's a drawback: on most shells, this is interpreted as a meta-character starting a comment. This means that if you type
.. code-block:: note
jrnl Implemented endless scrolling on the #frontend of our website.
your bash will chop off everything after the ``#`` before passing it to _jrnl_). To avoid this, wrap your input into quotation marks like this:
.. code-block:: note
jrnl "Implemented endless scrolling on the #frontend of our website."
Or use the built-in prompt or an external editor to compose your entries.
DayOne Integration
------------------
Using your DayOne journal instead of a flat text file is dead simple -- instead of pointing to a text file, change your ``.jrnl_config`` to point to your DayOne journal. This is a folder named something like ``Journal_dayone`` or ``Journal.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.
Instead of all entries being in a single file, each entry will live in a separate `plist` file. So your ``.jrnl_config`` should look like this:
.. code-block:: javascript
{
...
"journals": {
"default": "~/journal.txt",
"dayone": "~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/Journal_dayone"
}
Alfred Integration
------------------
You can use _jrnl_ with the popular `Alfred <https://www.alfredapp.com/>`_ app with `this handy workflow <http://www.packal.org/workflow/jrnl>`_.
Multiple journal files
----------------------
You can configure _jrnl_ to use with multiple journals (eg. ``private`` and ``work``) by defining more journals in your ``.jrnl_config``, for example:
.. code-block:: javascript
{
...
"journals": {
"default": "~/journal.txt",
"work": "~/work.txt"
}
}
The ``default`` journal gets created the first time you start _jrnl_. Now you can access the ``work`` journal by using ``jrnl work`` instead of ``jrnl``, eg. ::
jrnl work at 10am: Meeting with @Steve
::
jrnl work -n 3
will both use ``~/work.txt``, while ``jrnl -n 3`` will display the last three entries from ``~/journal.txt`` (and so does ``jrnl default -n 3``).
You can also override the default options for each individual journal. If you ``.jrnl_config`` looks like this:
.. code-block:: javascript
{
...
"encrypt": false
"journals": {
"default": "~/journal.txt",
"work": {
"journal": "~/work.txt",
"encrypt": true
},
"food": "~/my_recipes.txt",
}
Your ``default`` and your ``food`` journals won't be encrypted, however your ``work`` journal will! You can override all options that are present at the top level of ``.jrnl_config``, just make sure that at the very least you specify a ``"journal": ...`` key that points to the journal file of that journal.
.. note::
Changing ``encrypt`` to a different value will not encrypt or decrypt your journal file, it merely says whether or not your journal `is` encrypted. Hence manually changing this option will most likely result in your journal file being impossible to load.
Known Issues
~~~~~~~~~~~~
- The Windows shell prior to Windows 7 has issues with Unicode encoding. If you want to use non-ASCII characters, change the code page 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.

257
docs/conf.py Normal file
View file

@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
#
# jrnl documentation build configuration file, created by
# sphinx-quickstart on Wed Aug 7 13:22:51 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
import jrnl
from jrnl import __version__
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'jrnl'
copyright = u'jrnl is made with love by <a href="http://www.1450.me">Manuel Ebert</a> and <a href="https://github.com/maebert/jrnl/graphs/contributors" title="Contributors">other fabulous people</a>. If you need help, tweet to <a href="https://twitter.com/maebert" title="Follow @maebert on twitter">@maebert</a> or <a href="https://github.com/maebert/jrnl/issues/new" title="Open a new issue on Github">submit an issue</a> on Github.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = __version__
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'native'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'jrnl'
# On read the docs, use their standard theme.
RTD_NEW_THEME = True
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'jrnldoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'jrnl.tex', u'jrnl Documentation',
u'Manuel Ebert', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'jrnl', u'jrnl Documentation',
[u'Manuel Ebert'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'jrnl', u'jrnl Documentation',
u'Manuel Ebert', 'jrnl', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
html_theme = 'jrnl'

47
docs/encryption.rst Normal file
View file

@ -0,0 +1,47 @@
.. _encryption:
Encryption
==========
Encrypting and decrypting
-------------------------
If you don't choose to encrypt your file when you run `jrnl` for the first time, you can encrypt your existing journal file or change its password using ::
jrnl --encrypt
If it is already encrypted, you will first be asked for the current password. You can then enter a new password and your plain journal will replaced by the encrypted file. Conversely, ::
jrnl --decrypt
will replace your encrypted journal file by a Journal in plain text. You can also specify a filename, i.e. ``jrnl --decrypt plain_text_copy.txt``, to leave your original file untouched.
Storing passwords in your keychain
----------------------------------
Whenever you encrypt your journal, you are asked whether you want to store the encryption password in your keychain. If you do this, you won't have to enter your password every time you want to write or read your journal.
If you don't initially store the password in the keychain but decide to do so at a later point -- or maybe want to store it on one computer but not on another -- you can simply run ``jrnl --encrypt`` on an encrypted journal and use the same password again.
A note on security
------------------
While jrnl follows best practices, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell history -- it's meant to keep journals secure in transit, for example when storing it on an `untrusted <http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/>`_ services such as Dropbox. If you're concerned about security, disable history logging for journal in your ``.bashrc`` ::
HISTIGNORE="jrnl *:"
Manual decryption
-----------------
Should you ever want to decrypt your journal manually, you can do so with any program that supports the AES algorithm in CBC. The key used for encryption is the SHA-256-hash of your password, the IV (initialisation vector) is stored in the first 16 bytes of the encrypted file. The plain text is encoded in UTF-8 and padded according to PKCS#7 before being encrypted. So, to decrypt a journal file in python, run::
import hashlib, Crypto.Cipher
key = hashlib.sha256(my_password).digest()
with open("my_journal.txt") as f:
cipher = f.read()
crypto = AES.new(key, AES.MODE_CBC, iv = cipher[:16])
plain = crypto.decrypt(cipher[16:])
plain = plain.strip(plain[-1])
plain = plain.decode("utf-8")

78
docs/export.rst Normal file
View file

@ -0,0 +1,78 @@
.. _export:
Import and Export
=================
Tag export
----------
With::
jrnl --tags
you'll get a list of all tags you used in your journal, sorted by most frequent. Tags occurring several times in the same entry are only counted as one.
List of all entries
-------------------
::
jrnl --short
Will only display the date and title of each entry.
JSON export
-----------
Can do::
jrnl --export json
Why not create a `beautiful timeline <http://timeline.verite.co/>`_ of your journal?
Markdown export
---------------
Use::
jrnl --export markdown
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
Pretty-prints your entire journal.
XML export
-----------
::
jrnl --export xml
Why anyone would want to export stuff to XML is beyond me, but here you go.
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:
.. code-block:: output
my_entries/
|- 2013_06_03_a-beautiful-day.json
|- 2013_06_07_dinner-with-gabriel.json
|- ...

20
docs/index.rst Normal file
View file

@ -0,0 +1,20 @@
.. jrnl documentation master file, created by
sphinx-quickstart on Wed Aug 7 13:22:51 2013.
jrnl: The command-line journal
==============================
Release v\ |version|.
Contents:
.. toctree::
:maxdepth: 3
overview
installation
usage
encryption
export
advanced
recipes

35
docs/installation.rst Normal file
View file

@ -0,0 +1,35 @@
.. _download:
Getting started
===============
Installation
------------
Install *jrnl* using pip ::
pip install jrnl
Alternatively, on OS X with [Homebrew](http://brew.sh/) installed:
brew install jrnl
The first time you run ``jrnl`` you will be asked where your journal file should be created and whether you wish to encrypt it.
Quickstart
----------
to make a new entry, just type::
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.
and hit return. ``yesterday:`` will be interpreted as a time stamp. Everything until the first sentence mark (``.?!:``) will be interpreted as the title, the rest as the body. In your journal file, the result will look like this:
.. code-block:: output
2012-03-29 09:00 Called in sick.
Used the time to clean the house and spent 4h on writing my book.
If you just call ``jrnl``, you will be prompted to compose your entry - but you can also configure *jrnl* to use your external editor.

23
docs/overview.rst Normal file
View file

@ -0,0 +1,23 @@
.. _overview:
Overview
===============
What is jrnl?
-------------
`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.
Optionally, your journal can be encrypted using the `256-bit AES <http://en.wikipedia.org/wiki/Advanced_Encryption_Standard>`_.
Why keep a journal?
-------------------
Journals aren't only for 13-year old girls and people who have too much time on their summer vacation. A journal helps you to keep track of the things you get done and how you did them. Your imagination may be limitless, but your memory isn't.
For personal use, make it a good habit to write at least 20 words a day. Just to reflect what made this day special, why you haven't wasted it.
For professional use, consider a text-based journal to be the perfect complement to your GTD todo list - a documentation of what and how you've done it. Or use it as a quick way to keep a change log. Or use it to keep a lab book.

125
docs/recipes.rst Normal file
View file

@ -0,0 +1,125 @@
.. _recipes:
FAQ
===
Recipes
-------
Co-occurrence of tags
~~~~~~~~~~~~~~~~~~~~~
If I want to find out how often I mentioned my flatmates Alberto and Melo in the same entry, I run ::
jrnl @alberto --tags | grep @melo
And will get something like ``@melo: 9``, meaning there are 9 entries where both ``@alberto`` and ``@melo`` are tagged. How does this work? First, ``jrnl @alberto`` will filter the journal to only entries containing the tag ``@alberto``, and then the ``--tags`` option will print out how often each tag occurred in this `filtered` journal. Finally, we pipe this to ``grep`` which will only display the line containing ``@melo``.
Combining filters
~~~~~~~~~~~~~~~~~
You can do things like ::
jrnl @fixed -starred -n 10 -until "jan 2013" --short
To get a short summary of the 10 most recent, favourited entries before January 1, 2013 that are tagged with ``@fixed``.
Statistics
~~~~~~~~~~
How much did I write last year? ::
jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w
Will give you the number of words you wrote in 2013. How long is my average entry? ::
expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l)
This will first get the total number of words in the journal and divide it by the number of entries (this works because ``jrnl --short`` will print exactly one line per entry).
Importing older files
~~~~~~~~~~~~~~~~~~~~~
If you want to import a file as an entry to jrnl, you can just do ``jrnl < entry.ext``. But what if you want the modification date of the file to be the date of the entry in jrnl? Try this ::
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl
The first part will format the modification date of ``entry.txt``, and then combine it with the contents of the file before piping it to jrnl. If you do that often, consider creating a function in your ``.bashrc`` or ``.bash_profile``
.. code-block:: sh
jrnlimport () {
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
}
Using templates
~~~~~~~~~~~~~~~
Say you always want to use the same template for creating new entries. If you have an :doc:`external editor <advanced>` set up, you can use this ::
jrnl < my_template.txt
$ jrnl -1 --edit
Another nice solution that allows you to define individual prompts comes from `Jacobo de Vera <https://github.com/maebert/jrnl/issues/194#issuecomment-47402869>`_:
.. code-block:: sh
function log_question()
{
echo $1
read
jrnl today: ${1}. $REPLY
}
log_question 'What did I achieve today?'
log_question 'What did I make progress with?'
External editors
----------------
To use external editors for writing and editing journal entries, set them up in your ``.jrnl_config`` (see :doc:`advanced usage <advanced>` for details). Generally, after writing an entry, you will have to save and close the file to save the changes to jrnl.
Sublime Text
~~~~~~~~~~~~
To use Sublime Text, install the command line tools for Sublime Text and configure your ``.jrnl_config`` like this:
.. code-block:: javascript
"editor": "subl -w"
Note the ``-w`` flag to make sure jrnl waits for Sublime Text to close the file before writing into the journal.
MacVim
~~~~~~
Similar to Sublime Text, MacVim must be started with a flag that tells the the process to wait until the file is closed before passing control back to journal. In the case of MacVim, this is ``-f``:
.. code-block:: javascript
"editor": "mvim -f"
iA Writer
~~~~~~~~~
On OS X, you can use the fabulous `iA Writer <http://www.iawriter.com/mac>`_ to write entries. Configure your ``.jrnl_config`` like this:
.. code-block:: javascript
"editor": "open -b jp.informationarchitects.WriterForMacOSX -Wn"
What does this do? ``open -b ...`` opens a file using the application identified by the bundle identifier (a unique string for every app out there). ``-Wn`` tells the application to wait until it's closed before passing back control, and to use a new instance of the application.
Notepad++ on Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To set `Notepad++ <http://notepad-plus-plus.org/>`_ as your editor, edit the jrnl config file (``.jrnl_config``) like this:
.. code-block:: javascript
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
The double backslashes are needed so jrnl can read the file path correctly. The ``-multiInst -nosession`` options will cause jrnl to open its own Notepad++ window.

134
docs/usage.rst Normal file
View file

@ -0,0 +1,134 @@
.. _usage:
Basic Usage
===========
*jrnl* has two modes: **composing** and **viewing**. Basically, whenever you `don't` supply any arguments that start with a dash or double-dash, you're in composing mode, meaning you can write your entry on the command line or an editor of your choice.
We intentionally break a convention on command line arguments: all arguments starting with a `single dash` will `filter` your journal before viewing it, and can be combined arbitrarily. Arguments with a `double dash` will control how your journal is displayed or exported and are mutually exclusive (ie. you can only specify one way to display or export your journal at a time).
Listing Journals
----------------
You can list the journals accessible by jrnl::
jrnl -ls
The journals displayed correspond to those specified in the jrnl configuration file.
Composing Entries
-----------------
Composing mode is entered by either starting ``jrnl`` without any arguments -- which will prompt you to write an entry or launch your editor -- or by just writing an entry on the prompt, such as::
jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
.. note::
Most shell contains a certain number of reserved characters, such as ``#`` and ``*``. Unbalanced quotes, parenthesis, and so on will also get into the way of your editing. For writing longer entries, just enter ``jrnl`` and hit ``return``. Only then enter the text of your journal entry. Alternatively, :doc:`use an external editor <advanced>`).
You can also import an entry directly from a file::
jrnl < my_entry.txt
Smart timestamps
~~~~~~~~~~~~~~~~
Timestamps that work:
* at 6am
* yesterday
* last monday
* sunday at noon
* 2 march 2012
* 7 apr
* 5/20/1998 at 23:42
Starring entries
~~~~~~~~~~~~~~~~
To mark an entry as a favourite, simply "star" it::
jrnl last sunday *: Best day of my life.
If you don't want to add a date (ie. your entry will be dated as now), The following options are equivalent:
* ``jrnl *: Best day of my life.``
* ``jrnl *Best day of my life.``
* ``jrnl Best day of my life.*``
.. note::
Just make sure that the asterisk sign is **not** surrounded by whitespaces, e.g. ``jrnl Best day of my life! *`` will **not** work (the reason being that the ``*`` sign has a special meaning on most shells).
Viewing
-------
::
jrnl -n 10
will list you the ten latest entries (if you're lazy, ``jrnl -10`` will do the same), ::
jrnl -from "last year" -until march
everything that happened from the start of last year to the start of last march. To only see your favourite entries, use ::
jrnl -starred
Using Tags
----------
Keep track of people, projects or locations, by tagging them with an ``@`` in your entries ::
jrnl Had a wonderful day on the @beach with @Tom and @Anna.
You can filter your journal entries just like this: ::
jrnl @pinkie @WorldDomination
Will print all entries in which either ``@pinkie`` or ``@WorldDomination`` occurred. ::
jrnl -n 5 -and @pineapple @lubricant
the last five entries containing both ``@pineapple`` **and** ``@lubricant``. You can change which symbols you'd like to use for tagging in the configuration.
.. note::
``jrnl @pinkie @WorldDomination`` will switch to viewing mode because although **no** command line arguments are given, all the input strings look like tags - *jrnl* will assume you want to filter by tag.
Editing older entries
---------------------
You can edit selected entries after you wrote them. This is particularly useful when your journal file is encrypted or if you're using a DayOne journal. To use this feature, you need to have an editor configured in your journal configuration file (see :doc:`advanced usage <advanced>`)::
jrnl -until 1950 @texas -and @history --edit
Will open your editor with all entries tagged with ``@texas`` and ``@history`` before 1950. You can make any changes to them you want; after you save the file and close the editor, your journal will be updated.
Of course, if you are using multiple journals, you can also edit e.g. the latest entry of your work journal with ``jrnl work -n 1 --edit``. In any case, this will bring up your editor and save (and, if applicable, encrypt) your edited journal after you save and exit the editor.
You can also use this feature for deleting entries from your journal::
jrnl @girlfriend -until 'june 2012' --edit
Just select all text, press delete, and everything is gone...
Editing DayOne Journals
~~~~~~~~~~~~~~~~~~~~~~~
DayOne journals can be edited exactly the same way, however the output looks a little bit different because of the way DayOne stores its entries:
.. code-block:: output
# af8dbd0d43fb55458f11aad586ea2abf
2013-05-02 15:30 I told everyone I built my @robot wife for sex.
But late at night when we're alone we mostly play Battleship.
# 2391048fe24111e1983ed49a20be6f9e
2013-08-10 03:22 I had all kinds of plans in case of a @zombie attack.
I just figured I'd be on the other side.
The long strings starting with hash symbol are the so-called UUIDs, unique identifiers for each entry. Don't touch them. If you do, then the old entry would get deleted and a new one written, which means that you could lose DayOne data that jrnl can't handle (such as as the entry's geolocation).

60
features/core.feature Normal file
View file

@ -0,0 +1,60 @@
Feature: Basic reading and writing to a journal
Scenario: Loading a sample journal
Given we use the config "basic.yaml"
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.yaml"
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: Filtering for dates
Given we use the config "basic.yaml"
When we run "jrnl -on 2013-06-10 --short"
Then the output should be "2013-06-10 15:40 Life is good."
When we run "jrnl -on 'june 6 2013' --short"
Then the output should be "2013-06-10 15:40 Life is good."
Scenario: Emoji support
Given we use the config "basic.yaml"
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.yaml"
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."
Scenario: Displaying the version number
Given we use the config "basic.yaml"
When we run "jrnl -v"
Then we should get no error
Then the output should contain "version"
Scenario: --short displays the short version of entries (only the title)
Given we use the config "basic.yaml"
When we run "jrnl -on 2013-06-10 --short"
Then the output should be "2013-06-10 15:40 Life is good."
Scenario: -s displays the short version of entries (only the title)
Given we use the config "basic.yaml"
When we run "jrnl -on 2013-06-10 -s"
Then the output should be "2013-06-10 15:40 Life is good."

View file

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

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: false
highlight: true
journals:
default: features/journals/bug153.dayone
linewrap: 80
password: ''
tagsymbols: '@'
template: false
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
template: false
encrypt: false
highlight: true
journals:
simple: features/journals/simple.journal
work: features/journals/work.journal
linewrap: 80
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

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

View file

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

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: true
template: false
highlight: true
journals:
default: features/journals/encrypted.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

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

View file

@ -0,0 +1,11 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: true
highlight: true
journals:
default: features/journals/encrypted_jrnl1-9-5.journal
linewrap: 80
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: false
highlight: true
template: false
journals:
default: features/journals/markdown-headings-335.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

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

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: false
highlight: true
template: false
journals:
default: features/journals/tags-216.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

@ -0,0 +1,13 @@
default_hour: 9
default_minute: 0
editor: ''
encrypt: false
highlight: true
template: false
journals:
default: features/journals/tags-237.journal
linewrap: 80
password: ''
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'
indent_character: "|"

View file

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

View file

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

View file

@ -0,0 +1,56 @@
<?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-10-27T02:27:27Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>iPhone/iPhone3,1</string>
<key>Generation Date</key>
<date>2013-10-27T07:02:27Z</date>
<key>Host Name</key>
<string>omrt104001</string>
<key>OS Agent</key>
<string>iOS/7.0.3</string>
<key>Software Agent</key>
<string>Day One (iOS)/1.11.4</string>
</dict>
<key>Entry Text</key>
<string>Some text.</string>
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>Östergötlands län</string>
<key>Country</key>
<string>Sverige</string>
<key>Latitude</key>
<real>58.383400000000000</real>
<key>Locality</key>
<string>City</string>
<key>Longitude</key>
<real>15.577170000000000</real>
<key>Place Name</key>
<string>Street</string>
</dict>
<key>Starred</key>
<false/>
<key>Time Zone</key>
<string>Europe/Stockholm</string>
<key>UUID</key>
<string>B40EE704E15846DE8D45C44118A4D511</string>
<key>Weather</key>
<dict>
<key>Celsius</key>
<string>12</string>
<key>Description</key>
<string>Clear</string>
<key>Fahrenheit</key>
<string>54</string>
<key>IconName</key>
<string>sunnyn.png</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,52 @@
<dict>
<key>Creation Date</key>
<date>2013-10-27T02:27:27Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>iPhone/iPhone3,1</string>
<key>Generation Date</key>
<date>2013-10-27T07:02:27Z</date>
<key>Host Name</key>
<string>omrt104001</string>
<key>OS Agent</key>
<string>iOS/7.0.3</string>
<key>Software Agent</key>
<string>Day One (iOS)/1.11.4</string>
</dict>
<key>Entry Text</key>
<string>This is not a valid plist.</string>
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>Östergötlands län</string>
<key>Country</key>
<string>Sverige</string>
<key>Latitude</key>
<real>58.383400000000000</real>
<key>Locality</key>
<string>City</string>
<key>Longitude</key>
<real>15.577170000000000</real>
<key>Place Name</key>
<string>Street</string>
</dict>
<key>Starred</key>
<false/>
<key>Time Zone</key>
<string>Europe/Stockholm</string>
<key>UUID</key>
<string>B40EE704E15846DE8D45C44118A4D511</string>
<key>Weather</key>
<dict>
<key>Celsius</key>
<string>12</string>
<key>Description</key>
<string>Clear</string>
<key>Fahrenheit</key>
<string>54</string>
<key>IconName</key>
<string>sunnyn.png</string>
</dict>
</dict>
</plist>

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

View file

@ -0,0 +1 @@
gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==

Binary file not shown.

View file

@ -0,0 +1,42 @@
[2015-04-14 13:23] Heading Test
H1-1
=
H1-2
===
H1-3
============================
H2-1
-
H2-2
---
H2-3
----------------------------------
Horizontal Rules (ignore)
---
===
# ATX H1
## ATX H2
### ATX H3
#### ATX H4
##### ATX H5
###### ATX H6
Stuff
More stuff
more stuff again

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,3 @@
2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive".

View file

@ -0,0 +1,2 @@
[2013-06-10 15:40] I programmed for @OS/2.
Almost makes me want to go back to @C++, though. (Still better than @C#).

View file

@ -0,0 +1,3 @@
[2014-07-22 11:11] This entry has an email.
@Newline tag should show as a tag.
Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.

View file

@ -0,0 +1,8 @@
[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.
inst

View file

View file

@ -0,0 +1,19 @@
---
extension: txt
---
{% block journal %}
{% for entry in entries %}
{% include entry %}
{% endfor %}
{% endblock %}
{% block entry %}
{{ entry.title }}
{{ "-" * len(entry.title) }}
{{ entry.body }}
{% endblock %}
`

57
features/dayone.feature Normal file
View file

@ -0,0 +1,57 @@
Feature: DayOne Ingetration
Scenario: Loading a DayOne Journal
Given we use the config "dayone.yaml"
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!
"""
@skip
Scenario: Entries without timezone information will be interpreted as in the current timezone
Given we use the config "dayone.yaml"
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.yaml"
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.yaml"
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.yaml"
When we run "jrnl A hard day at @work"
and we run "jrnl --tags"
Then the output should be
"""
@work : 2
@play : 1
"""
Scenario: Filtering by tags from a DayOne Journal
Given we use the config "dayone.yaml"
When we run "jrnl @work"
Then the output should be
"""
2013-05-17 11:39 This entry has tags!
"""

View file

@ -0,0 +1,42 @@
Feature: Encrypted journals
Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml"
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.yaml"
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.yaml"
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: Storing a password in Keychain
Given we use the config "multiple.yaml"
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"
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter
"""
Y
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

42
features/environment.py Normal file
View file

@ -0,0 +1,42 @@
from behave import *
import shutil
import os
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,94 @@
Feature: Exporting a Journal
Scenario: Exporting to json
Given we use the config "tags.yaml"
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.yaml"
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"
Scenario: Exporting dayone to json
Given we use the config "dayone.yaml"
When we run "jrnl --export json"
Then we should get no error
and the output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"
Scenario: Exporting using custom templates
Given we use the config "basic.yaml"
Given we load template "sample.template"
When we run "jrnl --export sample"
Then the output should be
"""
My first entry.
---------------
Everything is alright
Life is good.
-------------
But I'm better.
"""
Scenario: Increasing Headings on Markdown export
Given we use the config "markdown-headings-335.yaml"
When we run "jrnl --export markdown"
Then the output should be
"""
2015
====
April
-----
### 2015-04-14 13:23 Heading Test
#### H1-1
#### H1-2
#### H1-3
##### H2-1
##### H2-2
##### H2-3
Horizontal Rules (ignore)
---
===
#### ATX H1
##### ATX H2
###### ATX H3
####### ATX H4
######## ATX H5
######### ATX H6
Stuff
More stuff
more stuff again
"""

View file

@ -0,0 +1,41 @@
Feature: Multiple journals
Scenario: Loading a config with two journals
Given we use the config "multiple.yaml"
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.yaml"
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.yaml"
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.yaml"
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.yaml"
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.yaml"
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
Scenario: Don't crash if no default journal is specified
Given we use the config "bug343.yaml"
When we run "jrnl a long day in the office"
Then we should see the message "No default journal configured"

View file

@ -0,0 +1,80 @@
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.yaml"
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.yaml"
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"
Scenario: Date with time should be parsed correctly
# https://github.com/maebert/jrnl/issues/117
Given we use the config "basic.yaml"
When we run "jrnl 2013-11-30 15:42: Project Started."
Then we should see the message "Entry added"
and the journal should contain "[2013-11-30 15:42] Project Started."
Scenario: Date in the future should be parsed correctly
# https://github.com/maebert/jrnl/issues/185
Given we use the config "basic.yaml"
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
Then we should see the message "Entry added"
and the journal should contain "[2019-06-26 09:00] Planet?"
Scenario: Loading entry with ambiguous time stamp
#https://github.com/maebert/jrnl/issues/153
Given we use the config "bug153.yaml"
When we run "jrnl -1"
Then we should get no error
and the output should be
"""
2013-10-27 03:27 Some text.
"""
Scenario: Title with an embedded period.
Given we use the config "basic.yaml"
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should be
"""
2014-04-24 09:00 Created a new website - empty.com.
| Hope to get a lot of traffic.
"""
Scenario: Upgrade and parse journals with square brackets
Given we use the config "upgrade_from_195.json"
When we run "jrnl -2" and enter "Y"
Then the output should contain
"""
2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive".
"""
Scenario: Title with an embedded period on DayOne journal
Given we use the config "dayone.yaml"
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch.""
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should be
"""
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
| I'm feeling sore because I forgot to stretch.
"""
Scenario: DayOne tag searching should work with tags containing a mixture of upper and lower case.
# https://github.com/maebert/jrnl/issues/354
Given we use the config "dayone.yaml"
When we run "jrnl @plAy"
Then the output should contain
"""
2013-05-17 11:39 This entry has tags!
"""

20
features/starring.feature Normal file
View file

@ -0,0 +1,20 @@
Feature: Starring entries
Scenario: Starring an entry will mark it in the journal file
Given we use the config "basic.yaml"
When we run "jrnl 20 july 2013 *: Best day of my life!"
Then we should see the message "Entry added"
and the journal should contain "[2013-07-20 09:00] Best day of my life! *"
Scenario: Filtering by starred entries
Given we use the config "basic.yaml"
When we run "jrnl -starred"
Then the output should be
"""
"""
When we run "jrnl 20 july 2013 *: Best day of my life!"
When we run "jrnl -starred"
Then the output should be
"""
2013-07-20 09:00 Best day of my life!
"""

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

@ -0,0 +1,260 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from behave import given, when, then
from jrnl import cli, install, Journal, util, plugins
from jrnl import __version__
from dateutil import parser as date_parser
from collections import defaultdict
import os
import json
import yaml
import keyring
class TestKeyring(keyring.backend.KeyringBackend):
"""A test keyring that just stores its valies in a hash"""
priority = 1
keys = defaultdict(dict)
def set_password(self, servicename, username, password):
self.keys[servicename][username] = password
def get_password(self, servicename, username):
return self.keys[servicename].get(username)
def delete_password(self, servicename, username, password):
self.keys[servicename][username] = None
# set the keyring for keyring lib
keyring.set_keyring(TestKeyring())
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
import tzlocal
import shlex
import sys
def ushlex(command):
if sys.version_info[0] == 3:
return shlex.split(command)
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
def read_journal(journal_name="default"):
config = util.load_config(install.CONFIG_FILE_PATH)
with open(config['journals'][journal_name]) as journal_file:
journal = journal_file.read()
return journal
def open_journal(journal_name="default"):
config = util.load_config(install.CONFIG_FILE_PATH)
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.open_journal(journal_name, config)
@given('we use the config "{config_file}"')
def set_config(context, config_file):
full_path = os.path.join("features/configs", config_file)
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
if config_file.endswith("yaml"):
# Add jrnl version to file for 2.x journals
with open(install.CONFIG_FILE_PATH, 'a') as cf:
cf.write("version: {}".format(__version__))
@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 = ushlex(command)[1:]
buffer = StringIO(text.strip())
util.STDIN = buffer
try:
cli.run(args or [])
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@when('we run "{command}"')
def run(context, command):
args = ushlex(command)[1:]
try:
cli.run(args or None)
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@given('we load template "{filename}"')
def load_template(context, filename):
full_path = os.path.join("features/data/templates", filename)
exporter = plugins.template_exporter.__exporter_from_file(full_path)
plugins.__exporter_types[exporter.names[0]] = exporter
@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 json output should contain {path} = "{value}"')
def check_json_output_path(context, path, value):
""" E.g.
the json output should contain entries.0.title = "hello"
"""
out = context.stdout_capture.getvalue()
struct = json.loads(out)
for node in path.split('.'):
try:
struct = struct[int(node)]
except ValueError:
struct = struct[node]
assert struct == value, struct
@then('the output should be')
@then('the output should be "{text}"')
def check_output(context, text=None):
text = (text or context.text).strip().splitlines()
out = context.stdout_capture.getvalue().strip().splitlines()
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
for line_text, line_out in zip(text, out):
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
@then('the output should contain "{text}" in the local time')
def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue()
local_tz = tzlocal.get_localzone()
utc_time = date_parser.parse(text)
local_date = utc_time.astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
assert local_date in out, local_date
@then('the output should contain')
@then('the output should contain "{text}"')
def check_output_inline(context, text=None):
text = text or context.text
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text in out, text
@then('the output should not contain "{text}"')
def check_output_not_inline(context, text):
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
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, journal
@then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"):
with open(install.CONFIG_FILE_PATH) as config_file:
config = yaml.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)
config = util.load_config(install.CONFIG_FILE_PATH)
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_entries(context, number, journal_name="default"):
journal = open_journal(journal_name)
assert len(journal.entries) == number
@then('fail')
def debug_fail(context):
assert False

52
features/tagging.feature Normal file
View file

@ -0,0 +1,52 @@
Feature: Tagging
Scenario: Displaying tags
Given we use the config "tags.yaml"
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.yaml"
When we run "jrnl -from 'may 2013' --tags"
Then we should get no error
and the output should be
"""
@idea : 1
@dan : 1
"""
Scenario: Tags should allow certain special characters
Given we use the config "tags-216.yaml"
When we run "jrnl --tags"
Then we should get no error
and the output should be
"""
@os/2 : 1
@c++ : 1
@c# : 1
"""
Scenario: An email should not be a tag
Given we use the config "tags-237.yaml"
When we run "jrnl --tags"
Then we should get no error
and the output should be
"""
@newline : 1
@email : 1
"""
Scenario: Entry cans start and end with tags
Given we use the config "basic.yaml"
When we run "jrnl today: @foo came over, we went to a @bar"
When we run "jrnl --tags"
Then the output should be
"""
@foo : 1
@bar : 1
"""

385
jrnl.py
View file

@ -1,385 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
import os
import tempfile
import parsedatetime.parsedatetime as pdt
import parsedatetime.parsedatetime_consts as pdc
import subprocess
import re
import argparse
from datetime import datetime
import time
try: import simplejson as json
except ImportError: import json
import sys
import readline, glob
from Crypto.Cipher import AES
from Crypto.Random import random, atfork
import hashlib
import getpass
import mimetypes
default_config = {
'journal': os.path.expanduser("~/journal.txt"),
'editor': "",
'encrypt': False,
'password': "",
'default_hour': 9,
'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M",
'tagsymbols': '@'
}
CONFIG_PATH = os.path.expanduser('~/.jrnl_config')
class Entry:
def __init__(self, journal, date=None, title="", body=""):
self.journal = journal # Reference to journal mainly to access it's config
self.date = date
self.title = title.strip()
self.body = body.strip()
self.tags = self.parse_tags()
def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower()
tags = re.findall(r"([%s]\w+)" % self.journal.config['tagsymbols'], fulltext)
self.tags = set(tags)
def __str__(self):
date_str = self.date.strftime(self.journal.config['timeformat'])
body_wrapper = "\n" if self.body else ""
body = body_wrapper + self.body.strip()
space = "\n"
return "%(date)s %(title)s %(body)s %(space)s" % {
'date': date_str,
'title': self.title,
'body': body,
'space': space
}
def __repr__(self):
return str(self)
def to_dict(self):
return {
'title': self.title.strip(),
'body': self.body.strip(),
'date': self.date.strftime("%Y-%m-%d"),
'time': self.date.strftime("%H:%M")
}
class Journal:
def __init__(self, config, **kwargs):
config.update(kwargs)
self.config = config
# Set up date parser
consts = pdc.Constants()
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
journal_txt = self.open()
self.entries = self.parse(journal_txt)
self.sort()
def _decrypt(self, cipher):
"""Decrypts a cipher string using self.key as the key and the first 16 byte of the cipher as the IV"""
if not cipher:
return ""
crypto = AES.new(self.key, AES.MODE_CBC, cipher[:16])
plain = crypto.decrypt(cipher[16:])
if plain[-1] != " ": # Journals are always padded
return None
else:
return plain
def _encrypt(self, plain):
"""Encrypt a plaintext string using self.key as the key"""
atfork() # A seed for PyCrypto
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
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
return iv + crypto.encrypt(plain)
def make_key(self, prompt="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).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
with open(filename) as f:
journal = f.read()
if self.config['encrypt']:
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
return journal
def parse(self, journal):
"""Parses a journal that's stored in a string and returns a list of entries"""
# Entries start with a line that looks like 'date title' - let's figure out how
# long the date will be by constructing one
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
for line in journal.split(os.linesep):
if line:
try:
new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], self.config['timeformat'])))
# make a journal entry of the current stuff first
if new_date and current_entry:
entries.append(current_entry)
# Start constructing current entry
current_entry = Entry(self, date=new_date, title=line[date_length+1:])
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
# Append last entry
if current_entry:
entries.append(current_entry)
for entry in entries:
entry.parse_tags()
return entries
def __str__(self):
"""Prettyprints the journal's entries"""
sep = "-"*60+"\n"
return sep.join([str(e) for e in self.entries])
def to_json(self):
"""Returns a JSON representation of the Journal."""
return json.dumps([e.to_dict() for e in self.entries], indent=2)
def __repr__(self):
return "<Journal with %d entries>" % len(self.entries)
def write(self, filename = None):
"""Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal']
journal = os.linesep.join([str(e) for e in self.entries])
if self.config['encrypt']:
journal = self._encrypt(journal)
with open(filename, 'w') as journal_file:
journal_file.write(journal)
def sort(self):
"""Sorts the Journal's entries by date"""
self.entries = sorted(self.entries, key=lambda entry: entry.date)
def limit(self, n=None):
"""Removes all but the last n entries"""
if n:
self.entries = self.entries[-n:]
def filter(self, tags=[], start_date=None, end_date=None, strict=False):
"""Removes all entries from the journal that don't match the filter.
tags is a list of tags, each being a string that starts with one of the
tag symbols defined in the config, e.g. ["@John", "#WorldDomination"].
start_date and end_date define a timespan by which to filter.
If strict is True, all tags must be present in an entry. If false, the
entry is kept if any tag is present."""
search_tags = set([tag.lower() for tag in tags])
end_date = self.parse_date(end_date)
start_date = self.parse_date(start_date)
# If strict mode is on, all tags have to be present in entry
tagged = search_tags.issubset if strict else search_tags.intersection
result = [
entry for entry in self.entries
if (not tags or tagged(entry.tags))
and (not start_date or entry.date > start_date)
and (not end_date or entry.date < end_date)
]
self.entries = result
def parse_date(self, date):
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
if not date:
return None
elif type(date) is datetime:
return date
date, flag = self.dateparse.parse(date)
if not flag: # Oops, unparsable.
return None
if flag is 1: # Date found, but no time. Use the default time.
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
else:
date = datetime(*date[:6])
return date
def new_entry(self, raw, date=None):
"""Constructs a new entry from some raw text input.
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
if not date:
if raw.find(":") > 0:
date = self.parse_date(raw[:raw.find(":")])
if date: # Parsed successfully, strip that from the raw text
raw = raw[raw.find(":")+1:].strip()
if not date: # Still nothing? Meh, just live in the moment.
date = self.parse_date("now")
# Split raw text into title and body
body = ""
title_end = len(raw)
for separator in ".?!":
sep_pos = raw.find(separator)
if 1 < sep_pos < title_end:
title_end = sep_pos
title = raw[:title_end+1]
body = raw[title_end+1:].strip()
self.entries.append(Entry(self, date, title, body))
self.sort()
def save_config(self, config_path = CONFIG_PATH):
with open(config_path, 'w') as f:
json.dump(self.config, f, indent=2)
def setup():
def autocomplete(text, state):
expansions = glob.glob(os.path.expanduser(text)+'*')
expansions = [e+"/" if os.path.isdir(e) else e for e in expansions]
expansions.append(None)
return expansions[state]
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(autocomplete)
# Where to create the journal?
path_query = 'Path to your journal file (leave blank for ~/journal.txt): '
journal_path = raw_input(path_query).strip() or os.path.expanduser('~/journal.txt')
default_config['journal'] = os.path.expanduser(journal_path)
# Encrypt it?
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
if password:
default_config['encrypt'] = True
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.")
open(default_config['journal'], 'a').close() # Touch to make sure it's there
# Write config to ~/.jrnl_conf
with open(CONFIG_PATH, 'w') as f:
json.dump(default_config, f, indent=2)
config = default_config
if password:
config['password'] = password
return config
if __name__ == "__main__":
if not os.path.exists(CONFIG_PATH):
config = setup()
else:
with open(CONFIG_PATH) as f:
config = json.load(f)
parser = argparse.ArgumentParser()
composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments')
composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"')
composing.add_argument('text', metavar='text', nargs="*", help='Log entry (or tags by which to filter in viewing mode)')
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('-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 = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
reading.add_argument('--json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal')
reading.add_argument('--encrypt', dest='encrypt', action="store_true", help='Encrypts your existing journal with a new password')
reading.add_argument('--decrypt', dest='decrypt', action="store_true", help='Decrypts your journal and stores it in plain text')
args = parser.parse_args()
# Guess mode
compose = True
export = False
if args.json or args.decrypt or args.encrypt:
compose = False
export = True
elif args.start_date or args.end_date or args.limit or args.strict:
# Any sign of displaying stuff?
compose = False
elif not args.date and args.text and all(word[0] in config['tagsymbols'] for word in args.text):
# No date and only tags?
compose = False
# No text? Query
if compose and not args.text:
if config['editor']:
tmpfile = os.path.join(tempfile.gettempdir(), "jrnl")
subprocess.call(config['editor'].split() + [tmpfile])
with open(tmpfile) as f:
raw = f.read()
os.remove(tmpfile)
else:
raw = raw_input("Compose Entry: ")
if raw:
args.text = [raw]
else:
compose = False
# open journal
journal = Journal(config=config)
# Writing mode
if compose:
raw = " ".join(args.text).strip()
journal.new_entry(raw, args.date)
print("Entry added.")
journal.write()
elif not export: # read mode
journal.filter(tags=args.text, start_date=args.start_date, end_date=args.end_date, strict=args.strict)
journal.limit(args.limit)
print(journal)
elif args.json: # export to json
print(journal.to_json())
elif args.encrypt:
journal.config['encrypt'] = True
journal.config['password'] = ""
journal.make_key(prompt="Enter new password:")
journal.write()
journal.save_config()
print("Journal encrypted to %s." % journal.config['journal'])
elif args.decrypt:
journal.config['encrypt'] = False
journal.config['password'] = ""
journal.write()
journal.save_config()
print("Journal decrypted to %s." % journal.config['journal'])

225
jrnl/DayOneJournal.py Normal file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry
from . import Journal
from . import time as jrnl_time
from . import __title__ # 'jrnl'
from . import __version__
import os
import re
from datetime import datetime
import time
import fnmatch
import plistlib
import pytz
import uuid
import tzlocal
from xml.parsers.expat import ExpatError
import socket
import platform
class DayOne(Journal.Journal):
"""A special Journal handling DayOne files"""
# InvalidFileException was added to plistlib in Python3.4
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
def __init__(self, **kwargs):
self.entries = []
self._deleted_entries = []
super(DayOne, self).__init__(**kwargs)
def open(self):
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
filenames = []
for root, dirnames, f in os.walk(self.config['journal']):
for filename in fnmatch.filter(f, '*.doentry'):
filenames.append(os.path.join(root, filename))
self.entries = []
for filename in filenames:
with open(filename, 'rb') as plist_entry:
try:
dict_entry = plistlib.readPlist(plist_entry)
except self.PLIST_EXCEPTIONS:
pass
else:
try:
timezone = pytz.timezone(dict_entry['Time Zone'])
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
timezone = tzlocal.get_localzone()
date = dict_entry['Creation Date']
try:
date = date + timezone.utcoffset(date, is_dst=False)
except TypeError:
# if the system timezone is set to UTC,
# pytz.timezone.utcoffset() breaks when given the
# arg `is_dst`
pass
entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"])
entry.uuid = dict_entry["UUID"]
entry._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])]
"""Extended DayOne attributes"""
try:
entry.creator_device_agent = dict_entry['Creator']['Device Agent']
except:
pass
try:
entry.creator_generation_date = dict_entry['Creator']['Generation Date']
except:
entry.creator_generation_date = date
try:
entry.creator_host_name = dict_entry['Creator']['Host Name']
except:
pass
try:
entry.creator_os_agent = dict_entry['Creator']['OS Agent']
except:
pass
try:
entry.creator_software_agent = dict_entry['Creator']['Software Agent']
except:
pass
try:
entry.location = dict_entry['Location']
except:
pass
try:
entry.weather = dict_entry['Weather']
except:
pass
self.entries.append(entry)
self.sort()
return self
def write(self):
"""Writes only the entries that have been modified into plist files."""
for entry in self.entries:
if entry.modified:
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
if not hasattr(entry, "uuid"):
entry.uuid = uuid.uuid1().hex
if not hasattr(entry, "creator_device_agent"):
entry.creator_device_agent = '' # iPhone/iPhone5,3
if not hasattr(entry, "creator_generation_date"):
entry.creator_generation_date = utc_time
if not hasattr(entry, "creator_host_name"):
entry.creator_host_name = socket.gethostname()
if not hasattr(entry, "creator_os_agent"):
entry.creator_os_agent = '{}/{}'.format(platform.system(), platform.release())
if not hasattr(entry, "creator_software_agent"):
entry.creator_software_agent = '{}/{}'.format(__title__, __version__)
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
entry_plist = {
'Creation Date': utc_time,
'Starred': entry.starred if hasattr(entry, 'starred') else False,
'Entry Text': entry.title + "\n" + entry.body,
'Time Zone': str(tzlocal.get_localzone()),
'UUID': entry.uuid.upper(),
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags],
'Creator': {'Device Agent': entry.creator_device_agent,
'Generation Date': entry.creator_generation_date,
'Host Name': entry.creator_host_name,
'OS Agent': entry.creator_os_agent,
'Software Agent': entry.creator_software_agent}
}
if hasattr(entry, 'location'):
entry_plist['Location'] = entry.location
if hasattr(entry, 'weather'):
entry_plist['Weather'] = entry.weather
plistlib.writePlist(entry_plist, filename)
for entry in self._deleted_entries:
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
os.remove(filename)
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str."""
return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates its entries."""
# Method: create a new list of entries from the edited text, then match
# UUIDs of the new entries against self.entries, updating the entries
# if the edited entries differ, and deleting entries from self.entries
# if they don't show up in the edited entries anymore.
# Initialise our current entry
entries = []
current_entry = None
for line in edited.splitlines():
# try to parse line as UUID => new entry begins
line = line.rstrip()
m = re.match("# *([a-f0-9]+) *$", line.lower())
if m:
if current_entry:
entries.append(current_entry)
current_entry = Entry.Entry(self)
current_entry.modified = False
current_entry.uuid = m.group(1).lower()
else:
date_blob_re = re.compile("^\[[^\\]]+\] ")
date_blob = date_blob_re.findall(line)
if date_blob:
date_blob = date_blob[0]
new_date = jrnl_time.parse(date_blob.strip(" []"))
if line.endswith("*"):
current_entry.starred = True
line = line[:-1]
current_entry.title = line[len(date_blob) - 1:].strip()
current_entry.date = new_date
elif current_entry:
current_entry.body += line + "\n"
# Append last entry
if current_entry:
entries.append(current_entry)
# Now, update our current entries if they changed
for entry in entries:
entry._parse_text()
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid.lower()]
# tags in entry body
if matched_entries:
# This entry is an existing entry
match = matched_entries[0]
# merge existing tags with tags pulled from the entry body
entry.tags = list(set(entry.tags + match.tags))
# extended Dayone metadata
if hasattr(match, "creator_device_agent"):
entry.creator_device_agent = match.creator_device_agent
if hasattr(match, "creator_generation_date"):
entry.creator_generation_date = match.creator_generation_date
if hasattr(match, "creator_host_name"):
entry.creator_host_name = match.creator_host_name
if hasattr(match, "creator_os_agent"):
entry.creator_os_agent = match.creator_os_agent
if hasattr(match, "creator_software_agent"):
entry.creator_software_agent = match.creator_software_agent
if hasattr(match, 'location'):
entry.location = match.location
if hasattr(match, 'weather'):
entry.weather = match.weather
if match != entry:
self.entries.remove(match)
entry.modified = True
self.entries.append(entry)
else:
# This entry seems to be new... save it.
entry.modified = True
self.entries.append(entry)
# Remove deleted entries
edited_uuids = [e.uuid for e in entries]
self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids]
self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids]
return entries

94
jrnl/EncryptedJournal.py Normal file
View file

@ -0,0 +1,94 @@
from . import Journal, util
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
import base64
def make_key(password):
password = util.bytes(password)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
# Salt is hard-coded
salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
iterations=100000,
backend=default_backend()
)
key = kdf.derive(password)
return base64.urlsafe_b64encode(key)
class EncryptedJournal(Journal.Journal):
def __init__(self, name='default', **kwargs):
super(EncryptedJournal, self).__init__(name, **kwargs)
self.config['encrypt'] = True
def _load(self, filename, password=None):
"""Loads an encrypted journal from a file and tries to decrypt it.
If password is not provided, will look for password in the keychain
and otherwise ask the user to enter a password up to three times.
If the password is provided but wrong (or corrupt), this will simply
return None."""
with open(filename, 'rb') as f:
journal_encrypted = f.read()
def validate_password(password):
key = make_key(password)
try:
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
self.config['password'] = password
return plain
except (InvalidToken, IndexError):
return None
if password:
return validate_password(password)
return util.get_password(keychain=self.name, validator=validate_password)
def _store(self, filename, text):
key = make_key(self.config['password'])
journal = Fernet(key).encrypt(text.encode('utf-8'))
with open(filename, 'wb') as f:
f.write(journal)
@classmethod
def _create(cls, filename, password):
key = make_key(password)
dummy = Fernet(key).encrypt(b"")
with open(filename, 'wb') as f:
f.write(dummy)
class LegacyEncryptedJournal(Journal.LegacyJournal):
"""Legacy class to support opening journals encrypted with the jrnl 1.x
standard. You'll not be able to save these journals anymore."""
def __init__(self, name='default', **kwargs):
super(LegacyEncryptedJournal, self).__init__(name, **kwargs)
self.config['encrypt'] = True
def _load(self, filename, password=None):
with open(filename, 'rb') as f:
journal_encrypted = f.read()
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
def validate_password(password):
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
try:
plain_padded = decryptor.update(cipher) + decryptor.finalize()
self.config['password'] = password
if plain_padded[-1] in (" ", 32):
# Ancient versions of jrnl. Do not judge me.
return plain_padded.decode('utf-8').rstrip(" ")
else:
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plain = unpadder.update(plain_padded) + unpadder.finalize()
return plain.decode('utf-8')
except ValueError:
return None
if password:
return validate_password(password)
return util.get_password(keychain=self.name, validator=validate_password)

124
jrnl/Entry.py Executable file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
import re
import textwrap
from datetime import datetime
from .util import split_title
class Entry:
def __init__(self, journal, date=None, text="", starred=False):
self.journal = journal # Reference to journal mainly to access its config
self.date = date or datetime.now()
self.text = text
self._title = self._body = self._tags = None
self.starred = starred
self.modified = False
@property
def fulltext(self):
return self.title + " " + self.body
def _parse_text(self):
raw_text = self.text
lines = raw_text.splitlines()
if lines[0].strip().endswith("*"):
self.starred = True
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
self._title, self._body = split_title(raw_text)
if self._tags is None:
self._tags = list(self._parse_tags())
@property
def title(self):
if self._title is None:
self._parse_text()
return self._title
@property
def body(self):
if self._body is None:
self._parse_text()
return self._body
@property
def tags(self):
if self._tags is None:
self._parse_text()
return self._tags
@staticmethod
def tag_regex(tagsymbols):
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
return re.compile(pattern, re.UNICODE)
def _parse_tags(self):
tagsymbols = self.journal.config['tagsymbols']
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
def __unicode__(self):
"""Returns a string representation of the entry to be written into a journal file."""
date_str = self.date.strftime(self.journal.config['timeformat'])
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
if self.starred:
title += " *"
return "{title}{sep}{body}\n".format(
title=title,
sep="\n" if self.body.rstrip("\n ") else "",
body=self.body.rstrip("\n ")
)
def pprint(self, short=False):
"""Returns a pretty-printed version of the entry.
If short is true, only print the title."""
date_str = self.date.strftime(self.journal.config['timeformat'])
if self.journal.config['indent_character']:
indent = self.journal.config['indent_character'].rstrip() + " "
else:
indent = ""
if not short and self.journal.config['linewrap']:
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
body = "\n".join([
textwrap.fill(
line,
self.journal.config['linewrap'],
initial_indent=indent,
subsequent_indent=indent,
drop_whitespace=True) or indent
for line in self.body.rstrip(" \n").splitlines()
])
else:
title = date_str + " " + self.title.rstrip("\n ")
body = self.body.rstrip("\n ")
# Suppress bodies that are just blanks and new lines.
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
if short:
return title
else:
return "{title}{sep}{body}\n".format(
title=title,
sep="\n" if has_body else "",
body=body if has_body else "",
)
def __repr__(self):
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
def __hash__(self):
return hash(self.__repr__())
def __eq__(self, other):
if not isinstance(other, Entry) \
or self.title.strip() != other.title.strip() \
or self.body.rstrip() != other.body.rstrip() \
or self.date != other.date \
or self.starred != other.starred:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)

328
jrnl/Journal.py Normal file
View file

@ -0,0 +1,328 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry
from . import util
from . import time
import os
import sys
import codecs
import re
from datetime import datetime
import logging
log = logging.getLogger(__name__)
class Tag(object):
def __init__(self, name, count=0):
self.name = name
self.count = count
def __str__(self):
return self.name
def __repr__(self):
return "<Tag '{}'>".format(self.name)
class Journal(object):
def __init__(self, name='default', **kwargs):
self.config = {
'journal': "journal.txt",
'encrypt': False,
'default_hour': 9,
'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M",
'tagsymbols': '@',
'highlight': True,
'linewrap': 80,
'indent_character': '|',
}
self.config.update(kwargs)
# Set up date parser
self.search_tags = None # Store tags we're highlighting
self.name = name
def __len__(self):
"""Returns the number of entries"""
return len(self.entries)
def __iter__(self):
"""Iterates over the journal's entries."""
return (entry for entry in self.entries)
@classmethod
def from_journal(cls, other):
"""Creates a new journal by copying configuration and entries from
another journal object"""
new_journal = cls(other.name, **other.config)
new_journal.entries = other.entries
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
return new_journal
def import_(self, other_journal_txt):
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
self.sort()
def open(self, filename=None):
"""Opens the journal file defined in the config and parses it into a list of Entries.
Entries have the form (date, title, body)."""
filename = filename or self.config['journal']
if not os.path.exists(filename):
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
self._create(filename)
text = self._load(filename)
self.entries = self._parse(text)
self.sort()
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self
def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal']
text = "\n".join([e.__unicode__() for e in self.entries])
self._store(filename, text)
def _load(self, filename):
raise NotImplementedError
def _store(self, filename, text):
raise NotImplementedError
@classmethod
def _create(cls, filename):
raise NotImplementedError
def _parse(self, journal_txt):
"""Parses a journal that's stored in a string and returns a list of entries"""
# Initialise our current entry
entries = []
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
last_entry_pos = 0
for match in date_blob_re.finditer(journal_txt):
date_blob = match.groups()[0]
new_date = time.parse(date_blob)
if new_date:
if entries:
entries[-1].text = journal_txt[last_entry_pos:match.start()]
last_entry_pos = match.end()
entries.append(Entry.Entry(self, date=new_date))
# Finish the last entry
if entries:
entries[-1].text = journal_txt[last_entry_pos:]
for entry in entries:
entry._parse_text()
return entries
def __unicode__(self):
return self.pprint()
def pprint(self, short=False):
"""Prettyprints the journal's entries"""
sep = "\n"
pp = sep.join([e.pprint(short=short) for e in self.entries])
if self.config['highlight']: # highlight tags
if self.search_tags:
for tag in self.search_tags:
tagre = re.compile(re.escape(tag), re.IGNORECASE)
pp = re.sub(tagre,
lambda match: util.colorize(match.group(0)),
pp, re.UNICODE)
else:
pp = re.sub(
Entry.Entry.tag_regex(self.config['tagsymbols']),
lambda match: util.colorize(match.group(0)),
pp
)
return pp
def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries))
def sort(self):
"""Sorts the Journal's entries by date"""
self.entries = sorted(self.entries, key=lambda entry: entry.date)
def limit(self, n=None):
"""Removes all but the last n entries"""
if n:
self.entries = self.entries[-n:]
@property
def tags(self):
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
tags = [tag
for entry in self.entries
for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags])
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False):
"""Removes all entries from the journal that don't match the filter.
tags is a list of tags, each being a string that starts with one of the
tag symbols defined in the config, e.g. ["@John", "#WorldDomination"].
start_date and end_date define a timespan by which to filter.
starred limits journal to starred entries
If strict is True, all tags must be present in an entry. If false, the
entry is kept if any tag is present."""
self.search_tags = set([tag.lower() for tag in tags])
end_date = time.parse(end_date, inclusive=True)
start_date = time.parse(start_date)
# If strict mode is on, all tags have to be present in entry
tagged = self.search_tags.issubset if strict else self.search_tags.intersection
result = [
entry for entry in self.entries
if (not tags or tagged(entry.tags))
and (not starred or entry.starred)
and (not start_date or entry.date >= start_date)
and (not end_date or entry.date <= end_date)
]
self.entries = result
def new_entry(self, raw, date=None, sort=True):
"""Constructs a new entry from some raw text input.
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
starred = False
# Split raw text into title and body
sep = re.search("\n|[\?!.]+ +\n?", raw)
first_line = raw[:sep.end()].strip() if sep else raw
starred = False
if not date:
colon_pos = first_line.find(": ")
if colon_pos > 0:
date = time.parse(raw[:colon_pos], default_hour=self.config['default_hour'], default_minute=self.config['default_minute'])
if date: # Parsed successfully, strip that from the raw text
starred = raw[:colon_pos].strip().endswith("*")
raw = raw[colon_pos + 1:].strip()
starred = starred or first_line.startswith("*") or first_line.endswith("*")
if not date: # Still nothing? Meh, just live in the moment.
date = time.parse("now")
entry = Entry.Entry(self, date, raw, starred=starred)
entry.modified = True
self.entries.append(entry)
if sort:
self.sort()
return entry
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str."""
return "\n".join([e.__unicode__() for e in self.entries])
def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries."""
mod_entries = self._parse(edited)
# Match those entries that can be found in self.entries and set
# these to modified, so we can get a count of how many entries got
# modified and how many got deleted later.
for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.entries = mod_entries
class PlainJournal(Journal):
@classmethod
def _create(cls, filename):
with codecs.open(filename, "a", "utf-8"):
pass
def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
return f.read()
def _store(self, filename, text):
with codecs.open(filename, 'w', "utf-8") as f:
f.write(text)
class LegacyJournal(Journal):
"""Legacy class to support opening journals formatted with the jrnl 1.x
standard. Main difference here is that in 1.x, timestamps were not cuddled
by square brackets. You'll not be able to save these journals anymore."""
def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
return f.read()
def _parse(self, journal_txt):
"""Parses a journal that's stored in a string and returns a list of entries"""
# Entries start with a line that looks like 'date title' - let's figure out how
# long the date will be by constructing one
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
for line in journal_txt.splitlines():
line = line.rstrip()
try:
# try to parse line as date => new entry begins
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
# parsing successful => save old entry and create new one
if new_date and current_entry:
entries.append(current_entry)
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
except ValueError:
# Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body.
if current_entry:
current_entry.text += line + u"\n"
# Append last entry
if current_entry:
entries.append(current_entry)
for entry in entries:
entry._parse_text()
return entries
def open_journal(name, config, legacy=False):
"""
Creates a normal, encrypted or DayOne journal based on the passed config.
If legacy is True, it will open Journals with legacy classes build for
backwards compatibility with jrnl 1.x
"""
config = config.copy()
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
from . import DayOneJournal
return DayOneJournal.DayOne(**config).open()
else:
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1)
if not config['encrypt']:
if legacy:
return LegacyJournal(name, **config).open()
return PlainJournal(name, **config).open()
else:
from . import EncryptedJournal
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
return EncryptedJournal.EncryptedJournal(name, **config).open()

14
jrnl/__init__.py Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env python
# encoding: utf-8
"""
jrnl is a simple journal application for your command line.
"""
from __future__ import absolute_import
__title__ = 'jrnl'
__version__ = '2.0.0-rc1.20150215'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2015 Manuel Ebert'

8
jrnl/__main__.py Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import cli
if __name__ == "__main__":
cli.run()

289
jrnl/cli.py Normal file
View file

@ -0,0 +1,289 @@
#!/usr/bin/env python
# encoding: utf-8
"""
jrnl
license: MIT, see LICENSE for more details.
"""
from __future__ import unicode_literals
from __future__ import absolute_import
from . import Journal
from . import util
from . import install
from . import plugins
from . import __title__, __version__
from .util import ERROR_COLOR, RESET_COLOR
import argparse
import sys
import logging
log = logging.getLogger(__name__)
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
def parse_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
composing.add_argument('text', metavar='', nargs="*")
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date')
reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)')
reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries')
reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', 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('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
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
import_ = False
if args.import_ is not False:
compose = False
export = False
import_ = True
elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)):
compose = False
export = True
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
# Any sign of displaying stuff?
compose = False
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
# No date and only tags?
compose = False
return compose, export, import_
def encrypt(journal, filename=None):
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
from . import EncryptedJournal
journal.config['password'] = util.getpass("Enter new password: ")
journal.config['encrypt'] = True
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
new_journal.entries = journal.entries
new_journal.write(filename)
if util.yesno("Do you want to store the password in your keychain?", default=True):
util.set_keychain(journal.name, journal.config['password'])
util.prompt("Journal encrypted to {0}.".format(filename or new_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'] = ""
new_journal = Journal.PlainJournal(filename, **journal.config)
new_journal.entries = journal.entries
new_journal.write(filename)
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
def list_journals(config):
"""List the journals specified in the configuration file"""
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
ml = min(max(len(k) for k in config['journals']), 20)
for journal, cfg in config['journals'].items():
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
return result
def update_config(config, new_config, scope, force_local=False):
"""Updates a config dict with new values - either global if scope is None
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
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 configure_logger(debug=False):
logging.basicConfig(
level=logging.DEBUG if debug else logging.INFO,
format='%(levelname)-8s %(name)-12s %(message)s'
)
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
def run(manual_args=None):
args = parse_args(manual_args)
configure_logger(args.debug)
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
if args.version:
version_str = "{0} version {1}".format(__title__, __version__)
print(util.py2encode(version_str))
sys.exit(0)
config = install.load_or_install_jrnl()
if args.ls:
util.prnt(list_journals(config))
sys.exit(0)
log.debug('Using configuration "%s"', config)
original_config = config.copy()
# If the first textual argument points to a journal file,
# use this!
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
if journal_name is not 'default':
args.text = args.text[1:]
elif "default" not in config['journals']:
util.prompt("No default journal configured.")
util.prompt(list_journals(config))
sys.exit(1)
config = util.scope_config(config, journal_name)
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter
if not args.limit and args.text and args.text[0].startswith("-"):
try:
args.limit = int(args.text[0].lstrip("-"))
args.text = args.text[1:]
except:
pass
log.debug('Using journal "%s"', journal_name)
mode_compose, mode_export, mode_import = guess_mode(args, config)
# How to quit writing?
if "win32" in sys.platform:
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
else:
_exit_multiline_code = "press Ctrl+D"
if mode_compose and not args.text:
if not sys.stdin.isatty():
# Piping data into jrnl
raw = util.py23_read()
elif config['editor']:
template = ""
if config['template']:
try:
template = open(config['template']).read()
except:
util.prompt("[Could not read template at '']".format(config['template']))
sys.exit(1)
raw = util.get_text_from_editor(config, template)
else:
try:
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
except KeyboardInterrupt:
util.prompt("[Entry NOT saved to journal.]")
sys.exit(0)
if raw:
args.text = [raw]
else:
mode_compose = False
# This is where we finally open the journal!
try:
journal = Journal.open_journal(journal_name, config)
except KeyboardInterrupt:
util.prompt("[Interrupted while opening journal]".format(journal_name))
sys.exit(1)
# Import mode
if mode_import:
plugins.get_importer(args.import_).import_(journal, args.input)
# Writing mode
elif mode_compose:
raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding())
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write()
if not mode_compose:
old_entries = journal.entries
if args.on_date:
args.start_date = args.end_date = args.on_date
journal.filter(tags=args.text,
start_date=args.start_date, end_date=args.end_date,
strict=args.strict,
short=args.short,
starred=args.starred)
journal.limit(args.limit)
# Reading mode
if not mode_compose and not mode_export and not mode_import:
print(util.py2encode(journal.pprint()))
# Various export modes
elif args.short:
print(util.py2encode(journal.pprint(short=True)))
elif args.tags:
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
elif args.export is not False:
exporter = plugins.get_exporter(args.export)
print(exporter.export(journal, args.output))
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}, journal_name, force_local=True)
install.save_config(original_config)
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}, journal_name, force_local=True)
install.save_config(original_config)
elif args.edit:
if not config['editor']:
util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR))
sys.exit(1)
other_entries = [e for e in old_entries if e not in journal.entries]
# Edit
old_num_entries = len(journal)
edited = util.get_text_from_editor(config, journal.editable_str())
journal.parse_editable_str(edited)
num_deleted = old_num_entries - len(journal)
num_edited = len([e for e in journal.entries if e.modified])
prompts = []
if num_deleted:
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
if num_edited:
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
journal.entries += other_entries
journal.sort()
journal.write()

66
jrnl/export.py Normal file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .util import ERROR_COLOR, RESET_COLOR
from .util import slugify, u
from .template import Template
import os
import codecs
class Exporter(object):
"""This Exporter can convert entries and journals into text files."""
def __init__(self, format):
with open("jrnl/templates/" + format + ".template") as f:
front_matter, body = f.read().strip("-\n").split("---", 2)
self.template = Template(body)
def export_entry(self, entry):
"""Returns a unicode representation of a single entry."""
return entry.__unicode__()
def _get_vars(self, journal):
return {
'journal': journal,
'entries': journal.entries,
'tags': journal.tags
}
def export_journal(self, journal):
"""Returns a unicode representation of an entire journal."""
return self.template.render_block("journal", **self._get_vars(journal))
def write_file(self, journal, path):
"""Exports a journal into a single file."""
try:
with codecs.open(path, "w", "utf-8") as f:
f.write(self.export_journal(journal))
return "[Journal exported to {0}]".format(path)
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
def make_filename(self, entry):
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension))
def write_files(self, journal, path):
"""Exports a journal into individual files for each entry."""
for entry in journal.entries:
try:
full_path = os.path.join(path, self.make_filename(entry))
with codecs.open(full_path, "w", "utf-8") as f:
f.write(self.export_entry(entry))
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
return "[Journal exported to {0}]".format(path)
def export(self, journal, format="text", output=None):
"""Exports to individual files if output is an existing path, or into
a single file if output is a file name, or returns the exporter's
representation as unicode if output is None."""
if output and os.path.isdir(output): # multiple files
return self.write_files(journal, output)
elif output: # single file
return self.write_file(journal, output)
else:
return self.export_journal(journal)

134
jrnl/install.py Normal file
View file

@ -0,0 +1,134 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import
import readline
import glob
import getpass
import os
import xdg.BaseDirectory
from . import util
from . import upgrade
from . import __version__
from .Journal import PlainJournal
from .EncryptedJournal import EncryptedJournal
import yaml
import logging
DEFAULT_CONFIG_NAME = 'jrnl.yaml'
DEFAULT_JOURNAL_NAME = 'journal.txt'
XDG_RESOURCE = 'jrnl'
USER_HOME = os.path.expanduser('~')
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
CONFIG_FILE_PATH_FALLBACK = os.path.join(USER_HOME, ".jrnl_config")
JOURNAL_PATH = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or USER_HOME
JOURNAL_FILE_PATH = os.path.join(JOURNAL_PATH, DEFAULT_JOURNAL_NAME)
log = logging.getLogger(__name__)
def module_exists(module_name):
"""Checks if a module exists and can be imported"""
try:
__import__(module_name)
except ImportError:
return False
else:
return True
default_config = {
'version': __version__,
'journals': {
"default": JOURNAL_FILE_PATH
},
'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "",
'encrypt': False,
'template': False,
'default_hour': 9,
'default_minute': 0,
'timeformat': "%Y-%m-%d %H:%M",
'tagsymbols': '@',
'highlight': True,
'linewrap': 79,
'indent_character': '|',
}
def upgrade_config(config):
"""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."""
missing_keys = set(default_config).difference(config)
if missing_keys or config['version'] != __version__:
for key in missing_keys:
config[key] = default_config[key]
save_config(config)
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
def save_config(config):
config['version'] = __version__
with open(CONFIG_FILE_PATH, 'w') as f:
yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False)
def load_or_install_jrnl():
"""
If jrnl is already installed, loads and returns a config object.
Else, perform various prompts to install jrnl.
"""
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
if os.path.exists(config_path):
log.debug('Reading configuration from file %s', config_path)
config = util.load_config(config_path)
upgrade.upgrade_jrnl_if_necessary(config_path)
upgrade_config(config)
return config
else:
log.debug('Configuration file not found, installing jrnl...')
return install()
def install():
def autocomplete(text, state):
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*')
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
expansions.append(None)
return expansions[state]
readline.set_completer_delims(' \t\n;')
readline.parse_and_bind("tab: complete")
readline.set_completer(autocomplete)
# Where to create the journal?
path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH)
journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
try:
os.makedirs(path)
except OSError:
pass
# Encrypt it?
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)
else:
util.set_keychain("default", None)
EncryptedJournal._create(default_config['journals']['default'], password)
print("Journal will be encrypted.")
else:
PlainJournal._create(default_config['journals']['default'])
config = default_config
save_config(config)
if password:
config['password'] = password
return config

35
jrnl/plugins/__init__.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .jrnl_importer import JRNLImporter
from .json_exporter import JSONExporter
from .markdown_exporter import MarkdownExporter
from .tag_exporter import TagExporter
from .xml_exporter import XMLExporter
from .yaml_exporter import YAMLExporter
from .template_exporter import __all__ as template_exporters
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
__importers =[JRNLImporter]
__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names])
__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names])
EXPORT_FORMATS = sorted(__exporter_types.keys())
IMPORT_FORMATS = sorted(__importer_types.keys())
def get_exporter(format):
for exporter in __exporters:
if hasattr(exporter, "names") and format in exporter.names:
return exporter
return None
def get_importer(format):
for importer in __importers:
if hasattr(importer, "names") and format in importer.names:
return importer
return None

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
import sys
from .. import util
class JRNLImporter(object):
"""This plugin imports entries from other jrnl files."""
names = ["jrnl"]
@staticmethod
def import_(journal, input=None):
"""Imports from an existing file if input is specified, and
standard input otherwise."""
old_cnt = len(journal.entries)
old_entries = journal.entries
if input:
with codecs.open(input, "r", "utf-8") as f:
other_journal_txt = f.read()
else:
try:
other_journal_txt = util.py23_read()
except KeyboardInterrupt:
util.prompt("[Entries NOT imported into journal.]")
sys.exit(0)
journal.import_(other_journal_txt)
new_cnt = len(journal.entries)
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name))
journal.write()

View file

@ -0,0 +1,41 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
import json
from .util import get_tags_count
class JSONExporter(TextExporter):
"""This Exporter can convert entries and journals into json."""
names = ["json"]
extension = "json"
@classmethod
def entry_to_dict(cls, entry):
entry_dict = {
'title': entry.title,
'body': entry.body,
'date': entry.date.strftime("%Y-%m-%d"),
'time': entry.date.strftime("%H:%M"),
'starred': entry.starred
}
if hasattr(entry, "uuid"):
entry_dict['uuid'] = entry.uuid
return entry_dict
@classmethod
def export_entry(cls, entry):
"""Returns a json representation of a single entry."""
return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n"
@classmethod
def export_journal(cls, journal):
"""Returns a json representation of an entire journal."""
tags = get_tags_count(journal)
result = {
"tags": dict((tag, count) for count, tag in tags),
"entries": [cls.entry_to_dict(e) for e in journal.entries]
}
return json.dumps(result, indent=2)

View file

@ -0,0 +1,79 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter
import re
import sys
from ..util import WARNING_COLOR, RESET_COLOR
class MarkdownExporter(TextExporter):
"""This Exporter can convert entries and journals into Markdown."""
names = ["md", "markdown"]
extension = "md"
@classmethod
def export_entry(cls, entry, to_multifile=True):
"""Returns a markdown representation of a single entry."""
date_str = entry.date.strftime(entry.journal.config['timeformat'])
body_wrapper = "\n" if entry.body else ""
body = body_wrapper + entry.body
if to_multifile is True:
heading = '#'
else:
heading = '###'
'''Increase heading levels in body text'''
newbody = ''
previous_line = ''
warn_on_heading_level = False
for line in body.splitlines(True):
if re.match(r"#+ ", line):
"""ATX style headings"""
newbody = newbody + previous_line + heading + line
if re.match(r"#######+ ", heading + line):
warn_on_heading_level = True
line = ''
elif re.match(r"=+$", line) and not re.match(r"^$", previous_line):
"""Setext style H1"""
newbody = newbody + heading + "# " + previous_line
line = ''
elif re.match(r"-+$", line) and not re.match(r"^$", previous_line):
"""Setext style H2"""
newbody = newbody + heading + "## " + previous_line
line = ''
else:
newbody = newbody + previous_line
previous_line = line
newbody = newbody + previous_line # add very last line
if warn_on_heading_level is True:
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
return "{md} {date} {title}\n{body}{space}".format(
md=heading,
date=date_str,
title=entry.title,
body=newbody,
space="\n"
)
@classmethod
def export_journal(cls, journal):
"""Returns a Markdown representation of an entire journal."""
out = []
year, month = -1, -1
for e in journal.entries:
if not e.date.year == year:
year = e.date.year
out.append(str(year))
out.append("=" * len(str(year)) + "\n")
if not e.date.month == month:
month = e.date.month
out.append(e.date.strftime("%B"))
out.append('-' * len(e.date.strftime("%B")) + "\n")
out.append(cls.export_entry(e, False))
result = "\n".join(out)
return result

View file

@ -0,0 +1,75 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
import sys
import re
from .text_exporter import TextExporter
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
class PrjctExporter(TextExporter):
"""This Exporter can convert entries and journals into Markdown formatted
text with front matter usable by the Ablog extention for Sphinx."""
names = ["prjct"]
extension = "md"
@classmethod
def export_entry(cls, entry, to_multifile=True):
"""Returns a markdown representation of a single entry, with Ablog front matter."""
if to_multifile is False:
print("{}ERROR{}: Prjct export must be to individual files. Please \
specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR, file=sys.stderr))
return
date_str = entry.date.strftime(entry.journal.config['timeformat'])
body_wrapper = "\n" if entry.body else ""
body = body_wrapper + entry.body
tagsymbols = entry.journal.config['tagsymbols']
# see also Entry.Entry.rag_regex
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE)
newbody = ''
for line in body.splitlines(True):
if multi_tag_regex.match(line):
"""Tag only lines"""
line = ''
newbody = newbody + line
# pass headings as is
if len(entry.tags) > 0:
tags_str = ' :tags: ' + ', '.join([tag[1:] for tag in entry.tags]) + '\n'
else:
tags_str = ''
if hasattr(entry, 'location'):
location_str = ' :location: {}\n'.format(entry.location.get('Locality', ''))
else:
location_str = ''
# source directory is entry.journal.config['journal']
# output directory is...?
return "# {title}\n\n```eval_rst\n.. post:: {date}\n{tags}{category}{author}{location}{language}```\n\n{body}{space}" \
.format(
date=date_str,
title=entry.title,
tags=tags_str,
category=" :category: jrnl\n",
author="",
location=location_str,
language="",
body=newbody,
space="\n"
)
@classmethod
def export_journal(cls, journal):
"""Returns an error, as Prjct export requires a directory as a target."""
print("{}ERROR{}: Prjct export must be to individual files. \
Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr)
return

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .util import get_tags_count
class TagExporter(TextExporter):
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
names = ["tags"]
extension = "tags"
@classmethod
def export_entry(cls, entry):
"""Returns a list of tags for a single entry."""
return ", ".join(entry.tags)
@classmethod
def export_journal(cls, journal):
"""Returns a list of tags and their frequency for an entire journal."""
tag_counts = get_tags_count(journal)
result = ""
if not tag_counts:
return '[No tags found in 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("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result

123
jrnl/plugins/template.py Normal file
View file

@ -0,0 +1,123 @@
import re
import asteval
import yaml
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
PRINT_RE = r"{{ *(.+?) *}}"
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
END_BLOCK_RE = r"{% *end(for|if) *%}"
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
IF_RE = r"{% *if +(.+?) *%}"
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
INCLUDE_RE = r"{% *include +(.+?) *%}"
class Template(object):
def __init__(self, template):
self.template = template
self.clean_template = None
self.blocks = {}
@classmethod
def from_file(cls, filename):
with open(filename) as f:
front_matter, body = f.read().strip("-\n").split("---", 2)
front_matter = yaml.load(front_matter)
template = cls(body)
template.__dict__.update(front_matter)
return template
def render(self, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.clean_template, **vars)
def render_block(self, block, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.blocks[block], **vars)
def _eval_context(self, vars):
e = asteval.Interpreter(symtable=vars, use_numpy=False, writer=None)
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
return e
def _get_blocks(self):
def s(match):
name, contents = match.groups()
self.blocks[name] = self._strip_single_nl(contents)
return ""
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
def _expand(self, template, **vars):
stack = sorted(
[(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] +
[(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)]
)
last_nesting, nesting = 0, 0
start = 0
result = ""
block_type = None
if not stack:
return self._expand_vars(template, **vars)
for pos, indent, typ in stack:
nesting += indent
if nesting == 1 and last_nesting == 0:
block_type = typ
result += self._expand_vars(template[start:pos], **vars)
start = pos
elif nesting == 0 and last_nesting == 1:
if block_type == "if":
result += self._expand_cond(template[start:pos], **vars)
elif block_type == "for":
result += self._expand_loops(template[start:pos], **vars)
elif block_type == "block":
result += self._save_block(template[start:pos], **vars)
start = pos
last_nesting = nesting
result += self._expand_vars(template[stack[-1][0]:], **vars)
return result
def _expand_vars(self, template, **vars):
safe_eval = self._eval_context(vars)
expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template)
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
def _expand_cond(self, template, **vars):
start_block = re.search(IF_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
expression = start_block.groups()[0]
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
safe_eval = self._eval_context(vars)
if safe_eval(expression):
return self._expand(sub_template)
return ""
def _strip_single_nl(self, template, strip_r=True):
if template[0] == "\n":
template = template[1:]
if strip_r and template[-1] == "\n":
template = template[:-1]
return template
def _expand_loops(self, template, **vars):
start_block = re.search(FOR_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
var_name, iterator = start_block.groups()
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
safe_eval = self._eval_context(vars)
result = ''
items = safe_eval(iterator)
for idx, var in enumerate(items):
vars[var_name] = var
vars['__last_iteration'] = idx == len(items) - 1
result += self._expand(sub_template, **vars)
del vars[var_name]
return self._strip_single_nl(result)

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .template import Template
import os
from glob import glob
class GenericTemplateExporter(TextExporter):
"""This Exporter can convert entries and journals into text files."""
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
vars = {
'entry': entry,
'tags': entry.tags
}
return cls.template.render_block("entry", **vars)
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
vars = {
'journal': journal,
'entries': journal.entries,
'tags': journal.tags
}
return cls.template.render_block("journal", **vars)
def __exporter_from_file(template_file):
"""Create a template class from a file"""
name = os.path.basename(template_file).replace(".template", "")
template = Template.from_file(template_file)
return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), {
"names": [name],
"extension": template.extension,
"template": template
})
__all__ = []
# Factory pattern to create Exporter classes for all available templates
for template_file in glob("jrnl/templates/*.template"):
__all__.append(__exporter_from_file(template_file))

View file

@ -0,0 +1,62 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
from ..util import u, slugify
import os
from ..util import ERROR_COLOR, RESET_COLOR
class TextExporter(object):
"""This Exporter can convert entries and journals into text files."""
names = ["text", "txt"]
extension = "txt"
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
return entry.__unicode__()
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
return "\n".join(cls.export_entry(entry) for entry in journal)
@classmethod
def write_file(cls, journal, path):
"""Exports a journal into a single file."""
try:
with codecs.open(path, "w", "utf-8") as f:
f.write(cls.export_journal(journal))
return "[Journal exported to {0}]".format(path)
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
@classmethod
def make_filename(cls, entry):
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
@classmethod
def write_files(cls, journal, path):
"""Exports a journal into individual files for each entry."""
for entry in journal.entries:
try:
full_path = os.path.join(path, cls.make_filename(entry))
with codecs.open(full_path, "w", "utf-8") as f:
f.write(cls.export_entry(entry))
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
return "[Journal exported to {0}]".format(path)
@classmethod
def export(cls, journal, output=None):
"""Exports to individual files if output is an existing path, or into
a single file if output is a file name, or returns the exporter's
representation as unicode if output is None."""
if output and os.path.isdir(output): # multiple files
return cls.write_files(journal, output)
elif output: # single file
return cls.write_file(journal, output)
else:
return cls.export_journal(journal)

27
jrnl/plugins/util.py Normal file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python
# encoding: utf-8
def get_tags_count(journal):
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
tags = [tag
for entry in journal.entries
for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts
def oxford_list(lst):
"""Return Human-readable list of things obeying the object comma)"""
lst = sorted(lst)
if not lst:
return "(nothing)"
elif len(lst) == 1:
return lst[0]
elif len(lst) == 2:
return lst[0] + " or " + lst[1]
else:
return ', '.join(lst[:-1]) + ", or " + lst[-1]

View file

@ -0,0 +1,60 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .json_exporter import JSONExporter
from .util import get_tags_count
from ..util import u
from xml.dom import minidom
class XMLExporter(JSONExporter):
"""This Exporter can convert entries and journals into XML."""
names = ["xml"]
extension = "xml"
@classmethod
def export_entry(cls, entry, doc=None):
"""Returns an XML representation of a single entry."""
doc_el = doc or minidom.Document()
entry_el = doc_el.createElement('entry')
for key, value in cls.entry_to_dict(entry).items():
elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(u(value)))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
return doc_el.toprettyxml()
else:
return entry_el
@classmethod
def entry_to_xml(cls, entry, doc):
entry_el = doc.createElement('entry')
entry_el.setAttribute('date', entry.date.isoformat())
if hasattr(entry, "uuid"):
entry_el.setAttribute('uuid', u(entry.uuid))
entry_el.setAttribute('starred', u(entry.starred))
entry_el.appendChild(doc.createTextNode(entry.fulltext))
return entry_el
@classmethod
def export_journal(cls, journal):
"""Returns an XML representation of an entire journal."""
tags = get_tags_count(journal)
doc = minidom.Document()
xml = doc.createElement('journal')
tags_el = doc.createElement('tags')
entries_el = doc.createElement('entries')
for count, tag in tags:
tag_el = doc.createElement('tag')
tag_el.setAttribute('name', tag)
count_node = doc.createTextNode(u(count))
tag_el.appendChild(count_node)
tags_el.appendChild(tag_el)
for entry in journal.entries:
entries_el.appendChild(cls.entry_to_xml(entry, doc))
xml.appendChild(entries_el)
xml.appendChild(tags_el)
doc.appendChild(xml)
return doc.toprettyxml()

View file

@ -0,0 +1,104 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter
import re
import sys
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
class YAMLExporter(TextExporter):
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
names = ["yaml"]
extension = "md"
@classmethod
def export_entry(cls, entry, to_multifile=True):
"""Returns a markdown representation of a single entry, with YAML front matter."""
if to_multifile is False:
print("{}ERROR{}: YAML export must be to individual files. Please \
specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR, file=sys.stderr))
return
date_str = entry.date.strftime(entry.journal.config['timeformat'])
body_wrapper = "\n" if entry.body else ""
body = body_wrapper + entry.body
tagsymbols = entry.journal.config['tagsymbols']
# see also Entry.Entry.rag_regex
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE)
"""Increase heading levels in body text"""
newbody = ''
heading = '#'
previous_line = ''
warn_on_heading_level = False
for line in body.splitlines(True):
if re.match(r"#+ ", line):
"""ATX style headings"""
newbody = newbody + previous_line + heading + line
if re.match(r"#######+ ", heading + line):
warn_on_heading_level = True
line = ''
elif re.match(r"=+$", line) and not re.match(r"^$", previous_line):
"""Setext style H1"""
newbody = newbody + heading + "# " + previous_line
line = ''
elif re.match(r"-+$", line) and not re.match(r"^$", previous_line):
"""Setext style H2"""
newbody = newbody + heading + "## " + previous_line
line = ''
elif multi_tag_regex.match(line):
"""Tag only lines"""
line = ''
else:
newbody = newbody + previous_line
previous_line = line
newbody = newbody + previous_line # add very last line
if warn_on_heading_level is True:
print("{}WARNING{}: Headings increased past H6 on export - {} {}"
.format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
dayone_attributes = ''
if hasattr(entry, "uuid"):
dayone_attributes += 'uuid: ' + entry.uuid + '\n'
if hasattr(entry, 'creator_device_agent') or \
hasattr(entry, 'creator_generation_date') or \
hasattr(entry, 'creator_host_name') or \
hasattr(entry, 'creator_os_agent') or \
hasattr(entry, 'creator_software_agent'):
dayone_attributes += 'creator:\n'
if hasattr(entry, 'creator_device_agent'):
dayone_attributes += ' device agent: {}\n'.format(entry.creator_device_agent)
if hasattr(entry, 'creator_generation_date'):
dayone_attributes += ' generation date: {}\n'.format(str(entry.creator_generation_date))
if hasattr(entry, 'creator_host_name'):
dayone_attributes += ' host name: {}\n'.format(entry.creator_host_name)
if hasattr(entry, 'creator_os_agent'):
dayone_attributes += ' os agent: {}\n'.format(entry.creator_os_agent)
if hasattr(entry, 'creator_software_agent'):
dayone_attributes += ' software agent: {}\n'.format(entry.creator_software_agent)
# TODO: copy over pictures, if present
# source directory is entry.journal.config['journal']
# output directory is...?
return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone}{body}{space}" \
.format(
date=date_str,
title=entry.title,
stared=entry.starred,
tags=', '.join([tag[1:] for tag in entry.tags]),
dayone=dayone_attributes,
body=newbody,
space="\n"
)
@classmethod
def export_journal(cls, journal):
"""Returns an error, as YAML export requires a directory as a target."""
print("{}ERROR{}: YAML export must be to individual files. \
Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr)
return

View file

@ -0,0 +1,18 @@
---
extension: txt
---
{% block journal %}
{% for entry in entries %}
{% include entry %}
{% endfor %}
{% endblock %}
{% block entry %}
{{ entry.title }}
{{ "-" * len(entry.title) }}
{{ entry.body }}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show more