Compare commits
644 commits
develop
...
prjct-0.6.
Author | SHA1 | Date | |
---|---|---|---|
|
5dc724dd4d | ||
|
779a8f4ddf | ||
|
9441998ecf | ||
|
126e188c0f | ||
|
89eba3e274 | ||
|
1c244da79b | ||
|
b84bc9b856 | ||
|
14ef6e3dcb | ||
|
b11cbd9e6f | ||
|
5a98089522 | ||
|
43df14f18c | ||
|
8721ff819f | ||
|
ccaca2d135 | ||
|
a31eb9628a | ||
|
cccdc157fb | ||
|
8615679beb | ||
|
a95859a292 | ||
|
d65a9779ca | ||
|
6d7bd18d52 | ||
|
2031902f1c | ||
|
4b3dc38e05 | ||
|
ce113af4e0 | ||
|
dab853555e | ||
|
b3f5c092de | ||
|
49a374e552 | ||
|
7776b3d24c | ||
|
8b64cc95d9 | ||
|
3580154f7d | ||
|
5921cd63f8 | ||
|
37f6e3c8b4 | ||
|
1ac5444051 | ||
|
a4783109b3 | ||
|
4a871aff32 | ||
|
e9d856b039 | ||
|
91cf1c911f | ||
|
1aef13d757 | ||
|
659502adf5 | ||
|
155be26840 | ||
|
06e3f0c101 | ||
|
7a0e056130 | ||
|
38682b922f | ||
|
5805cef008 | ||
|
06a9ed2026 | ||
|
79fa9c5434 | ||
|
76bc803776 | ||
|
168f63c657 | ||
|
d0f86d398f | ||
|
dc39a25d5c | ||
|
8a6563d958 | ||
|
549f8501d6 | ||
|
78bae0476c | ||
|
c8b8fab684 | ||
|
fd46ffea23 | ||
|
e4249bb00c | ||
|
4c7d37a153 | ||
|
281b74da74 | ||
|
6444b6b137 | ||
|
f7cadfc9dd | ||
|
72cbafac55 | ||
|
2141bcacd4 | ||
|
06dbe2fb30 | ||
|
586ce47c68 | ||
|
ea6060d75c | ||
|
bedab7c121 | ||
|
eb0af8f47c | ||
|
924037e2b0 | ||
|
70c1f8c2ed | ||
|
61f1d866b3 | ||
|
308fe0d478 | ||
|
61cc792ad7 | ||
|
5a3e3320a7 | ||
|
e88fa16bbc | ||
|
d17558f3c6 | ||
|
425ecf791d | ||
|
ee782cc5c5 | ||
|
94d6ebeb2d | ||
|
4b70709248 | ||
|
978d735351 | ||
|
57ef796ccb | ||
|
10eb36076f | ||
|
08fbd0d960 | ||
|
bdfe588a01 | ||
|
a2c75e180c | ||
|
162e33089e | ||
|
7d23945df7 | ||
|
2caaa039b1 | ||
|
0a730f2a94 | ||
|
087b45f0bf | ||
|
dbfc61b437 | ||
|
f182dae6eb | ||
|
2d49aaa1a0 | ||
|
442e27a5d1 | ||
|
8e44ce0845 | ||
|
c10a4d4fed | ||
|
429c8283e0 | ||
|
f1641d8084 | ||
|
141eb2a647 | ||
|
bbbc2de1b4 | ||
|
3b85121f51 | ||
|
873356b623 | ||
|
c68a60f6dc | ||
|
35b9827eb1 | ||
|
b2a7d1f30e | ||
|
00a5657c1b | ||
|
6bcd5be7d4 | ||
|
41bec199a2 | ||
|
8e627b01c0 | ||
|
b41b5d3376 | ||
|
cca630059f | ||
|
c720c15787 | ||
|
109a11d18e | ||
|
94964dc31c | ||
|
b7d8e1a9a5 | ||
|
f459dd5bc2 | ||
|
1543a93799 | ||
|
812d289740 | ||
|
4010d38af2 | ||
|
7dc2ccbd3a | ||
|
367b13b849 | ||
|
24ada92787 | ||
|
9c7d5af152 | ||
|
f68ca1221e | ||
|
c31e62d9d6 | ||
|
75ecc0dbbd | ||
|
4955902702 | ||
|
b90a4b16bb | ||
|
dd79639095 | ||
|
003fb507ae | ||
|
cc22fd8e60 | ||
|
6b8cbe52b5 | ||
|
1771ff4e27 | ||
|
cfd8856989 | ||
|
5685952a73 | ||
|
584f0a969e | ||
|
9e766d7ad0 | ||
|
5e68b97e59 | ||
|
359eabd56d | ||
|
be009b08b4 | ||
|
79f87abd76 | ||
|
c5c6676bab | ||
|
dc03c47da8 | ||
|
0a28a9807b | ||
|
84d886d0ef | ||
|
4ea5ae80b5 | ||
|
293204040b | ||
|
9651adcdd0 | ||
|
044accff6d | ||
|
84002574e6 | ||
|
37d3f93b8a | ||
|
953c18dff2 | ||
|
4f05c2bc07 | ||
|
37ebc473c8 | ||
|
cbab596b43 | ||
|
9c16ad4577 | ||
|
bdcdce343f | ||
|
487c902d37 | ||
|
99c2884343 | ||
|
76710d06f7 | ||
|
dbb3fe40df | ||
|
b77a579ab5 | ||
|
f5a44487c1 | ||
|
411b9eb844 | ||
|
391269335f | ||
|
182e51b92d | ||
|
050eea7560 | ||
|
6d783a80ac | ||
|
a43ebd7b08 | ||
|
4390e1f10c | ||
|
ccd4cca9e6 | ||
|
0d981241ac | ||
|
b851941574 | ||
|
2780a0d9df | ||
|
bf49f31d22 | ||
|
2150b5b3e7 | ||
|
4d6cdc835e | ||
|
b8a3b6d9ad | ||
|
112f626d09 | ||
|
8d1f5fe862 | ||
|
4b4d29bf08 | ||
|
929896e53e | ||
|
340c510a8b | ||
|
30c30812a9 | ||
|
6837c46351 | ||
|
7f9cd675fe | ||
|
22b5119cb0 | ||
|
41d093cc19 | ||
|
2d46b0250b | ||
|
49f67df064 | ||
|
f604862488 | ||
|
811167704b | ||
|
ad2d278ae5 | ||
|
fd529a485c | ||
|
85826ca77b | ||
|
f3871aab02 | ||
|
6f4ab856d3 | ||
|
0618082aae | ||
|
53e947ac18 | ||
|
c05fdfdad7 | ||
|
4df3571b47 | ||
|
919b9ae3e4 | ||
|
8be672a1d9 | ||
|
15b7d2603c | ||
|
9871221fb2 | ||
|
2332fca6fe | ||
|
a2be5d25fc | ||
|
3a65ce227d | ||
|
07f2e2be34 | ||
|
b423bfc963 | ||
|
3f5b5ff3c3 | ||
|
8b45458f90 | ||
|
8314c85463 | ||
|
f5a3d7328c | ||
|
fa243065d9 | ||
|
b4944c808d | ||
|
a593ed80b9 | ||
|
3f4b873ec9 | ||
|
948d031777 | ||
|
f9af744a29 | ||
|
6f8755ab55 | ||
|
8eeba1481d | ||
|
fe41e41a6f | ||
|
828ea4d427 | ||
|
872cab6cb4 | ||
|
9276a92a0b | ||
|
b8b928f277 | ||
|
cabaa6a832 | ||
|
9878b6e350 | ||
|
6b2df88d29 | ||
|
38ebf7920f | ||
|
466a9d1f84 | ||
|
2a0d9333fc | ||
|
7e80071088 | ||
|
c95f8b5a1f | ||
|
cfefb287db | ||
|
562f700615 | ||
|
32203f95d8 | ||
|
57b272febf | ||
|
9b2f05c9d7 | ||
|
e60ff1faf6 | ||
|
3639e00183 | ||
|
632d6990ec | ||
|
db5bb6366c | ||
|
98a9d53217 | ||
|
97f19614ce | ||
|
8a4401b670 | ||
|
e889e7d7a3 | ||
|
7b2c47cbbe | ||
|
378baf17a1 | ||
|
73ee4054d6 | ||
|
db99510720 | ||
|
a0333e60d5 | ||
|
6953e340c4 | ||
|
6a60cac4b5 | ||
|
c2692d88fe | ||
|
b9685a55b0 | ||
|
689f5b7461 | ||
|
4442829306 | ||
|
88a38efdc1 | ||
|
16e09a8095 | ||
|
91f677d825 | ||
|
0b678ce69c | ||
|
2b6768e427 | ||
|
181c36163a | ||
|
8604bf8455 | ||
|
a5d345173a | ||
|
0161d834c0 | ||
|
b7479860a1 | ||
|
db4d2ddc54 | ||
|
666bab572e | ||
|
60b49f8c37 | ||
|
a78d8d6337 | ||
|
69ded73304 | ||
|
052f571e0b | ||
|
1a37161853 | ||
|
915bb99607 | ||
|
c4c44d74eb | ||
|
b8c1360f2d | ||
|
da827952b0 | ||
|
e9e9b1611b | ||
|
9d4c1a23b1 | ||
|
599c5a9ea1 | ||
|
f72b10700d | ||
|
040c09dbd4 | ||
|
aa70bb3274 | ||
|
e2111ff209 | ||
|
9ba6b71a61 | ||
|
4d4a85b4b8 | ||
|
bdb77d488d | ||
|
ed43ae9bc4 | ||
|
4f55e895bc | ||
|
d5496467cf | ||
|
f03d7a3bf9 | ||
|
887c7027b2 | ||
|
36601cba39 | ||
|
f1ea01ff9a | ||
|
f504e2a61c | ||
|
27c261943c | ||
|
ec3751ccc0 | ||
|
24a5712c4d | ||
|
cac0b722cd | ||
|
18d5348529 | ||
|
3931e40cd1 | ||
|
1a6c9aa74a | ||
|
5b5a5d9f57 | ||
|
1d574c7407 | ||
|
d900b239a9 | ||
|
ca7a804b09 | ||
|
9e5664ba46 | ||
|
a20381ce68 | ||
|
a26fd3d1d7 | ||
|
3a4607f671 | ||
|
6f302ad664 | ||
|
f1e5c88b23 | ||
|
5ff185b2df | ||
|
81b684afdf | ||
|
fc72514434 | ||
|
1e4506803b | ||
|
cebe33574d | ||
|
7b630fb65e | ||
|
60bcac8d55 | ||
|
b88180fbce | ||
|
ea70ac713f | ||
|
7599688dba | ||
|
6c51374f08 | ||
|
ab9a3b5192 | ||
|
382917b8c4 | ||
|
816aa66356 | ||
|
4f7827fc91 | ||
|
6c9687110c | ||
|
92867a2f84 | ||
|
447fb4d59f | ||
|
d4a1a10880 | ||
|
a0828ef151 | ||
|
57e926aaaa | ||
|
26878883dd | ||
|
f0aab36e19 | ||
|
b050f2b030 | ||
|
fcd8c4d214 | ||
|
c3e8f59fe0 | ||
|
77b669aedf | ||
|
860d74bc0e | ||
|
cdf312de28 | ||
|
a4448b0c34 | ||
|
8c9ae82b98 | ||
|
e0ef258a83 | ||
|
9df688d6f0 | ||
|
d34874947f | ||
|
077bff7e88 | ||
|
fa130143b5 | ||
|
88db6fca23 | ||
|
c06bfd781f | ||
|
410e4299ea | ||
|
c6bde5e9ac | ||
|
4d6b22861e | ||
|
abe2281361 | ||
|
7fd210de3c | ||
|
bc75d4031b | ||
|
e1f31a23ae | ||
|
c6e620d9cb | ||
|
1c7f0b351d | ||
|
f9ea3e69ff | ||
|
91b4257f7c | ||
|
b49e868f93 | ||
|
b6016611fd | ||
|
fd1fc05bd0 | ||
|
006361d963 | ||
|
e5262067a2 | ||
|
f2b3f56993 | ||
|
0911c2e470 | ||
|
a88459dc1c | ||
|
8821158835 | ||
|
421d27f021 | ||
|
37fad66942 | ||
|
7097ed03a2 | ||
|
afa1b2bc4d | ||
|
7195c545ae | ||
|
c582ac2af9 | ||
|
7247af2195 | ||
|
bc9b8600a7 | ||
|
bb5f8141ee | ||
|
b7afaa355c | ||
|
e0655d894a | ||
|
73d0969d20 | ||
|
04567ff5a7 | ||
|
89afb5ba36 | ||
|
1dafa66ef3 | ||
|
241dac087a | ||
|
840425d3a4 | ||
|
07e8f19e21 | ||
|
7965e1b4ef | ||
|
84641ee62f | ||
|
e6592ddd5c | ||
|
630aaefffd | ||
|
e37119b1d5 | ||
|
a8c232b310 | ||
|
9c6db60cc4 | ||
|
a64d1a2350 | ||
|
6ec4cb10c1 | ||
|
c201e56713 | ||
|
6a88f78d9a | ||
|
967e888936 | ||
|
b60605567e | ||
|
f9f20fb4ad | ||
|
0d451be033 | ||
|
2b4afeb635 | ||
|
36b375831a | ||
|
aac68db995 | ||
|
360580dbc7 | ||
|
7aa252c0ab | ||
|
ed66d76db2 | ||
|
50036e2ee1 | ||
|
68cd41de40 | ||
|
7e3faeef45 | ||
|
be2c511ea4 | ||
|
d9f01fd753 | ||
|
d588b31a71 | ||
|
f5826fb56b | ||
|
8f5082a919 | ||
|
dd36b1faa3 | ||
|
08d260f3b2 | ||
|
7315aa8c06 | ||
|
1246fb2bbb | ||
|
fd95c6a83f | ||
|
2235a2f974 | ||
|
d4aa08e18a | ||
|
3d49937335 | ||
|
e37395ffd9 | ||
|
8f85b97942 | ||
|
a7afed7c8b | ||
|
79b61dfbab | ||
|
30692efc6c | ||
|
d36711b36b | ||
|
e796f18d6e | ||
|
81e8e26b0a | ||
|
575026b51d | ||
|
19d342d6f2 | ||
|
59de6351bc | ||
|
f3e1f94f55 | ||
|
8446908139 | ||
|
58ea57f62b | ||
|
887f765495 | ||
|
24ff65d6fc | ||
|
2593b23d26 | ||
|
93813ef040 | ||
|
2e44a29c33 | ||
|
6dae28f196 | ||
|
ad98031c7f | ||
|
619285fb7f | ||
|
0e2f63a5b1 | ||
|
9ba3e447de | ||
|
ff0b85e28b | ||
|
1889cee6c3 | ||
|
3ef6095b41 | ||
|
978d9db072 | ||
|
9dda1b39bd | ||
|
08d6f5fa06 | ||
|
9443fb3b5f | ||
|
6bcf83ed78 | ||
|
13a0e7ce86 | ||
|
601e574d91 | ||
|
bf69e0042e | ||
|
e78786e19a | ||
|
9c3841dded | ||
|
b1266dcc4e | ||
|
95f68a5109 | ||
|
0b7b88dcf0 | ||
|
7cf4fd701e | ||
|
91638fd429 | ||
|
c303ea8455 | ||
|
7f680f6260 | ||
|
608cc04897 | ||
|
6abd7d5b68 | ||
|
d4cbd4f944 | ||
|
3a4c87ca4c | ||
|
6521d55a2b | ||
|
89423bdccb | ||
|
783603fb1c | ||
|
c5cd42028d | ||
|
02ce7dfbd3 | ||
|
b773c0d7f5 | ||
|
edc41dab20 | ||
|
0c374a839c | ||
|
d56f4140fb | ||
|
9309ec391b | ||
|
39a8b3a4cd | ||
|
a7e64dd756 | ||
|
de8caf08a6 | ||
|
d81f263e8c | ||
|
f6be2f13a5 | ||
|
42daea2dd6 | ||
|
bce370174b | ||
|
6fff395701 | ||
|
659f9c4d98 | ||
|
bdc0b4c5f5 | ||
|
9e969786f5 | ||
|
5e448270d2 | ||
|
d7dfba008c | ||
|
cf4592fbe3 | ||
|
99dd9a05aa | ||
|
d48f5137e5 | ||
|
65760eabff | ||
|
5408f60db3 | ||
|
a959ed49fd | ||
|
868d29a92e | ||
|
cd2eebab34 | ||
|
b4a28d3c7b | ||
|
5bb8f9c567 | ||
|
cf7581c0c0 | ||
|
32f3d23b90 | ||
|
48c5dd8aa1 | ||
|
0bf2354bc2 | ||
|
fde308e4ba | ||
|
aef9a0ee7e | ||
|
1417305c2e | ||
|
4528d9f612 | ||
|
56a9c0bbcc | ||
|
2790f250e6 | ||
|
dab192db86 | ||
|
dbddb7d6c9 | ||
|
3abe14bab1 | ||
|
8865816d60 | ||
|
62a48d7b02 | ||
|
9f33c9c9c4 | ||
|
8200ebb6fe | ||
|
52b5b4848d | ||
|
80da7326b8 | ||
|
41706c3cb5 | ||
|
9b0bee0637 | ||
|
36fe783e5e | ||
|
8ed2e36669 | ||
|
b2a5b7b219 | ||
|
24f769cf66 | ||
|
fe521147c0 | ||
|
38d2734c96 | ||
|
080bfd513f | ||
|
9a55b2c81b | ||
|
1fc6bc2dd7 | ||
|
ba2b4a592a | ||
|
e3613b8628 | ||
|
3d31e31eb5 | ||
|
ba9e39f050 | ||
|
dd47a94b61 | ||
|
fe5f318c16 | ||
|
caad0bdbae | ||
|
05198b65b7 | ||
|
a8777b747a | ||
|
123000ce41 | ||
|
5236ca0c44 | ||
|
1c0916a8f3 | ||
|
918a46a2fa | ||
|
8418c2f696 | ||
|
370e703d11 | ||
|
fd0d35e151 | ||
|
9476d3f115 | ||
|
ac266fd3b1 | ||
|
386c574815 | ||
|
911ec2eb3f | ||
|
61d73281c4 | ||
|
a56b395b7a | ||
|
25ea115eca | ||
|
cb6ee5889e | ||
|
33195829c4 | ||
|
a04675e33e | ||
|
c1bfd8b451 | ||
|
9a39c8962d | ||
|
9796f5b0b0 | ||
|
907a75d41e | ||
|
144785d2b5 | ||
|
fb3d7abc2d | ||
|
7e59e8ad3b | ||
|
3cbef42e2b | ||
|
80bb29bf0e | ||
|
1903749901 | ||
|
017843b948 | ||
|
4898759b8b | ||
|
9439c294da | ||
|
41c26d6115 | ||
|
482e579242 | ||
|
46ba622aa1 | ||
|
c289aaeb8c | ||
|
4cca754ba5 | ||
|
3935681365 | ||
|
3b9610f5f4 | ||
|
2f8a95f553 | ||
|
6c1e3f2351 | ||
|
084e956d16 | ||
|
cc786e5376 | ||
|
08b7624513 | ||
|
0d19f1790f | ||
|
dd824c4f8d | ||
|
bfcdb2928f | ||
|
f7eee0fa76 | ||
|
56c974f1dd | ||
|
4a1312a70b | ||
|
4850828e07 | ||
|
8ec7c51d89 | ||
|
3c05edde25 | ||
|
386d813eeb | ||
|
99e8679b48 | ||
|
e8f346242e | ||
|
32b691f938 | ||
|
4b354bd40e | ||
|
19ed9a6cf8 | ||
|
5bee8ff1e1 | ||
|
945ee4145b | ||
|
59049a87f0 | ||
|
19a1db7038 | ||
|
cd48d4e41d | ||
|
5d3070ddd7 | ||
|
44333a5330 | ||
|
4ec53e1b69 | ||
|
4e39ab2821 | ||
|
f8a8cdf24b | ||
|
7dcd78f3ff | ||
|
e9063e1336 | ||
|
3e07ef5fe4 | ||
|
432997969b | ||
|
7c3a6d0222 | ||
|
448f56473d | ||
|
d4bd93779a | ||
|
e4eef0fa81 | ||
|
e238d88225 | ||
|
f96d73d2e1 | ||
|
cfbfcbc50b | ||
|
bd834e7863 | ||
|
b14493eea5 | ||
|
0710411cde | ||
|
d5b931c4a1 | ||
|
46a7bea4b4 | ||
|
a79f094fd8 | ||
|
0cc57bab49 | ||
|
5a096ccd54 | ||
|
d550932b98 | ||
|
0a56658c43 | ||
|
3ca6ef89f4 | ||
|
6a23b54c36 | ||
|
f2e207de85 | ||
|
ae57eb9d77 | ||
|
632add24f9 | ||
|
a100d147af | ||
|
2ec032b4e6 | ||
|
7009a88316 | ||
|
de98d8f9bd | ||
|
5ddbfed35a |
57
.gitignore
vendored
|
@ -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
|
@ -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
|
173
CHANGELOG.md
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
include LICENSE
|
||||
include README.md
|
||||
include CHANGELOG.md
|
39
Makefile
Normal 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
|
@ -1,7 +1,11 @@
|
|||
jrnl
|
||||
jrnl [](https://travis-ci.org/maebert/jrnl) [](https://pypi.python.org/pypi/jrnl/) [](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
|
@ -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
|
@ -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 ▶</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 & 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
|
@ -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
|
@ -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>
|
BIN
docs/_themes/jrnl/static/img/favicon-152.png
vendored
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
docs/_themes/jrnl/static/img/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
docs/_themes/jrnl/static/img/icons.png
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
docs/_themes/jrnl/static/img/icons@2x.png
vendored
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
docs/_themes/jrnl/static/img/logo.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/_themes/jrnl/static/img/logo@2x.png
vendored
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
docs/_themes/jrnl/static/img/terminal.png
vendored
Normal file
After Width: | Height: | Size: 687 B |
BIN
docs/_themes/jrnl/static/img/twitter.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
109
docs/_themes/jrnl/static/js/landing.js
vendored
Normal 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
After Width: | Height: | Size: 208 KiB |
283
docs/_themes/jrnl/static/less/docs.less
vendored
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
docs/_themes/jrnl/static/less/retina.less
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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."
|
||||
|
12
features/data/configs/basic.yaml
Normal 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: "|"
|
13
features/data/configs/bug153.yaml
Normal 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: "|"
|
13
features/data/configs/bug343.yaml
Normal 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: "|"
|
13
features/data/configs/dayone.yaml
Normal 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: "|"
|
13
features/data/configs/empty_folder.yaml
Normal 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: "|"
|
13
features/data/configs/encrypted.yaml
Normal 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: "|"
|
13
features/data/configs/encrypted_old.json
Normal 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"
|
||||
}
|
11
features/data/configs/encrypted_old.yaml
Normal 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: "|"
|
13
features/data/configs/markdown-headings-335.yaml
Normal 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: "|"
|
16
features/data/configs/multiple.yaml
Normal 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: "|"
|
13
features/data/configs/tags-216.yaml
Normal 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: "|"
|
13
features/data/configs/tags-237.yaml
Normal 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: "|"
|
13
features/data/configs/tags.yaml
Normal 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: "|"
|
11
features/data/configs/upgrade_from_195.json
Normal 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": "@"
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Creation Date</key>
|
||||
<date>2013-05-17T18:39:20Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:39:20Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry has tags!</string>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array>
|
||||
<string>work</string>
|
||||
<string>PLaY</string>
|
||||
</array>
|
||||
<key>Time Zone</key>
|
||||
<string>America/Los_Angeles</string>
|
||||
<key>UUID</key>
|
||||
<string>044F3747A38546168B572C2E3F217FA2</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Creation Date</key>
|
||||
<date>2013-06-17T18:38:29Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:38:29Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry has a location.</string>
|
||||
<key>Location</key>
|
||||
<dict>
|
||||
<key>Administrative Area</key>
|
||||
<string>California</string>
|
||||
<key>Country</key>
|
||||
<string>Germany</string>
|
||||
<key>Latitude</key>
|
||||
<real>52.4979764</real>
|
||||
<key>Locality</key>
|
||||
<string>Berlin</string>
|
||||
<key>Longitude</key>
|
||||
<real>13.2404758</real>
|
||||
<key>Place Name</key>
|
||||
<string>Abandoned Spy Tower</string>
|
||||
</dict>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>Time Zone</key>
|
||||
<string>Europe/Berlin</string>
|
||||
<key>UUID</key>
|
||||
<string>0BDDD6CDA43C4A9AA2681517CC35AD9D</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Creation Date</key>
|
||||
<date>2013-07-17T18:38:08Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:38:08Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry is starred!</string>
|
||||
<key>Starred</key>
|
||||
<true/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>Time Zone</key>
|
||||
<string>America/Los_Angeles</string>
|
||||
<key>UUID</key>
|
||||
<string>422BC895507944A291E6FC44FC6B8BFC</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Creation Date</key>
|
||||
<date>2013-01-17T18:37:50Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:37:50Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This is a DayOne entry without Timezone.</string>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>UUID</key>
|
||||
<string>4BB1F46946AD439996C9B59DE7C4DDC1</string>
|
||||
</dict>
|
||||
</plist>
|
1
features/data/journals/empty_folder/empty.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Nothing to see here
|
1
features/data/journals/encrypted.journal
Normal file
|
@ -0,0 +1 @@
|
|||
gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==
|
BIN
features/data/journals/encrypted_jrnl-1-9-5.journal
Normal file
42
features/data/journals/markdown-headings-335.journal
Normal 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
|
5
features/data/journals/simple.journal
Normal file
|
@ -0,0 +1,5 @@
|
|||
[2013-06-09 15:39] My first entry.
|
||||
Everything is alright
|
||||
|
||||
[2013-06-10 15:40] Life is good.
|
||||
But I'm better.
|
3
features/data/journals/simple_jrnl-1-9-5.journal
Normal 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".
|
2
features/data/journals/tags-216.journal
Normal 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#).
|
3
features/data/journals/tags-237.journal
Normal 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.
|
8
features/data/journals/tags.journal
Normal 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
|
0
features/data/journals/work.journal
Normal file
19
features/data/templates/sample.template
Normal 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
|
@ -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!
|
||||
"""
|
42
features/encryption.feature
Normal 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
|
@ -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)
|
94
features/exporting.feature
Normal 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
|
||||
"""
|
41
features/multiple_journals.feature
Normal 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"
|
80
features/regression.feature
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
31
jrnl/plugins/jrnl_importer.py
Normal 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()
|
41
jrnl/plugins/json_exporter.py
Normal 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)
|
79
jrnl/plugins/markdown_exporter.py
Normal 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
|
75
jrnl/plugins/prjct_exporter.py
Normal 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
|
30
jrnl/plugins/tag_exporter.py
Normal 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
|
@ -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)
|
49
jrnl/plugins/template_exporter.py
Normal 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))
|
62
jrnl/plugins/text_exporter.py
Normal 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
|
@ -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]
|
60
jrnl/plugins/xml_exporter.py
Normal 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()
|
104
jrnl/plugins/yaml_exporter.py
Normal 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
|
18
jrnl/templates/sample.template
Normal 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 %}
|