mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Compare commits
797 commits
v2.7.1-bet
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
7923e815f7 | ||
|
72d4968471 | ||
|
c286012bef | ||
|
c415c68443 | ||
|
e5fc8be2a8 | ||
|
20a087f64c | ||
|
a2eb68963b | ||
|
89be61ccf8 | ||
|
98d0bcac20 | ||
|
a282440118 | ||
|
7e72386da1 | ||
|
6f1e8e5853 | ||
|
906a3fed78 | ||
|
8329cf5537 | ||
|
1dd2d48068 | ||
|
43310f399d | ||
|
591f2457c3 | ||
|
8be5b722aa | ||
|
7249adb232 | ||
|
fbd1b9e78b | ||
|
2581c43ae7 | ||
|
2035619587 | ||
|
37f634ac80 | ||
|
d1e3e546f2 | ||
|
f8d80b2877 | ||
|
8589b2ed65 | ||
|
6645b1e36f | ||
|
316b9761d5 | ||
|
e2a62e99a0 | ||
|
692d0cb132 | ||
|
573e34fb48 | ||
|
f2bb5eb501 | ||
|
7ad6fca011 | ||
|
640150bcbe | ||
|
75c98a408c | ||
|
b8a8de98c3 | ||
|
85eb730186 | ||
|
024ea00a95 | ||
|
1530ad39a1 | ||
|
505a9b6846 | ||
|
30c341ef8b | ||
|
ccb64b1721 | ||
|
a1b3a612a5 | ||
|
247129c760 | ||
|
bb71dff815 | ||
|
ba18bed4d7 | ||
|
eb5fe6fef1 | ||
|
16a0f043b1 | ||
|
b760759906 | ||
|
2ce567e495 | ||
|
1143fba023 | ||
|
41279ae943 | ||
|
985187f866 | ||
|
53fef9021d | ||
|
df4a5a5867 | ||
|
3f3bf3e77e | ||
|
1b79f0b340 | ||
|
428549778c | ||
|
d9dae7af04 | ||
|
5f6be07fbd | ||
|
a94f7c6161 | ||
|
086c34a6a7 | ||
|
0044b2dee8 | ||
|
0cc4acd494 | ||
|
0d5bf1e467 | ||
|
85a98afcd9 | ||
|
96d89ca9d5 | ||
|
de79733957 | ||
|
af1b9f128e | ||
|
5145190584 | ||
|
314576ae13 | ||
|
87c022780d | ||
|
a8bd0bcd44 | ||
|
2f0c5d2eb9 | ||
|
ae32ca6917 | ||
|
5856517b0f | ||
|
1586cc2b49 | ||
|
bcf41ddd17 | ||
|
4017c4acb5 | ||
|
a646801c15 | ||
|
cb504c4bfb | ||
|
21e4552360 | ||
|
cdb9a220bd | ||
|
2d3809e6f7 | ||
|
0ff69bd24c | ||
|
dec4855816 | ||
|
89eaf3440a | ||
|
7953309c78 | ||
|
17e1ba60e5 | ||
|
4d84c491f1 | ||
|
79cf67eca6 | ||
|
c8c49b1ba2 | ||
|
8f2cb9c355 | ||
|
9ac27704fb | ||
|
97b39dfd76 | ||
|
8fc158e35b | ||
|
83d4878044 | ||
|
c9c71b40ef | ||
|
8cd4ac24a8 | ||
|
ca6b4a08a7 | ||
|
be9dbf52e0 | ||
|
4c4e36a534 | ||
|
7c4e3ad76e | ||
|
0c2c40d494 | ||
|
ecf5f07dbb | ||
|
8957ceb74d | ||
|
0deadc03c8 | ||
|
d5a5401db9 | ||
|
0aefdb3f21 | ||
|
9ad1c9f26b | ||
|
48b61f6953 | ||
|
2af05e4008 | ||
|
a13abe446b | ||
|
5ce9d47126 | ||
|
581af57bfd | ||
|
60abba3bfe | ||
|
5323ca64fa | ||
|
1b2fa594e8 | ||
|
36677fdccd | ||
|
896be7fb5f | ||
|
a2a6618959 | ||
|
39291bf1ce | ||
|
fbd468e03e | ||
|
d91c857fdc | ||
|
8eb124b54c | ||
|
f3624a3fa2 | ||
|
de4cf5219e | ||
|
7fdc43b69d | ||
|
df37641fa8 | ||
|
8edc14cfe7 | ||
|
8f2ac67659 | ||
|
c5ba4cb198 | ||
|
d44515f764 | ||
|
ccd68f1e78 | ||
|
fc16c6c3e8 | ||
|
b4e8bb0e12 | ||
|
5ca5ee6723 | ||
|
ce50158167 | ||
|
d5f7182cf4 | ||
|
9d97df04a7 | ||
|
bf32d59844 | ||
|
caf3cdea3d | ||
|
434c32003f | ||
|
48a31e8154 | ||
|
3fe3eda27b | ||
|
65070cb675 | ||
|
311d0b51b6 | ||
|
aca0a185b7 | ||
|
14bf9ac55f | ||
|
cf145123f5 | ||
|
c2a97f3100 | ||
|
296c459252 | ||
|
a176913bda | ||
|
721167a079 | ||
|
7e94208574 | ||
|
ef016cfb76 | ||
|
c2cbf36a87 | ||
|
6379533065 | ||
|
eec83daaf5 | ||
|
00a6724900 | ||
|
69e098d6e5 | ||
|
5bf24d6eac | ||
|
b6f1e76ae4 | ||
|
1fdb76008e | ||
|
681b477543 | ||
|
c2fa27329c | ||
|
299bf090fc | ||
|
a8f5f54a62 | ||
|
75f97ce32f | ||
|
09a96e6db9 | ||
|
f9558f960a | ||
|
1de94ca929 | ||
|
b639915fd4 | ||
|
12525e21ec | ||
|
3177578c64 | ||
|
4ea2fa8c31 | ||
|
a1c24359d2 | ||
|
6529fbf211 | ||
|
d881880c38 | ||
|
0a01420e9d | ||
|
c6f22a2753 | ||
|
c7b204022a | ||
|
1c7ad4d5bb | ||
|
be4cc1abe8 | ||
|
4969d28988 | ||
|
6962c399ea | ||
|
9533b55d8f | ||
|
c4d7f37cc2 | ||
|
5c4724d524 | ||
|
dfdd9b69cb | ||
|
475923d6d5 | ||
|
2674639383 | ||
|
d2fc602618 | ||
|
85bd7d40a5 | ||
|
303ca98d0b | ||
|
c63367b067 | ||
|
42ac0d33cc | ||
|
2a59c708b2 | ||
|
75b703d39f | ||
|
0f4fdf89b2 | ||
|
d2756db940 | ||
|
cfcbd8e6b7 | ||
|
cb69bb474c | ||
|
525cce3e92 | ||
|
cf6aef7ae0 | ||
|
b77ea37b2c | ||
|
488998cd33 | ||
|
f5378dea74 | ||
|
23b169708c | ||
|
b899130009 | ||
|
bb838ed83a | ||
|
4c53cd8830 | ||
|
1aa1ac0976 | ||
|
f4db877ddd | ||
|
0da41a1328 | ||
|
9970873ff5 | ||
|
129428ef80 | ||
|
e1501fd4d1 | ||
|
34c7903300 | ||
|
dd09ece4e7 | ||
|
c6b9114c42 | ||
|
7a9bc929a5 | ||
|
235ba679e3 | ||
|
32f7b9de62 | ||
|
9735a60883 | ||
|
b4088bf8b8 | ||
|
a1de9589b5 | ||
|
875b6d2b35 | ||
|
12355e087c | ||
|
6e8d6e9aee | ||
|
2a05aad0e9 | ||
|
4f1a21819d | ||
|
12e663ee93 | ||
|
9edc7c5cc0 | ||
|
793a5e22c5 | ||
|
10cf68161e | ||
|
21c4aa0b3a | ||
|
649254d0ac | ||
|
f5dc5fbae0 | ||
|
5124a805c8 | ||
|
982e597742 | ||
|
4a7057c038 | ||
|
78d11d74bd | ||
|
b1843ab14b | ||
|
4cb5d1e436 | ||
|
959e18ad91 | ||
|
4292d85a22 | ||
|
9f4ca2683f | ||
|
3e44027ab5 | ||
|
98d2a3b3ec | ||
|
0d8fc9028c | ||
|
f5e5d817be | ||
|
d5f00edb9d | ||
|
6e9fade8dd | ||
|
7045e97a24 | ||
|
fdc87ff8c4 | ||
|
81313c2445 | ||
|
0b8b4244bd | ||
|
7a8b86aadb | ||
|
1f59c15f5e | ||
|
da0c49da83 | ||
|
8f2eb27793 | ||
|
a1a68f7eea | ||
|
cd850c068d | ||
|
e987e34c78 | ||
|
53342c944e | ||
|
0b55c477e1 | ||
|
7e09c4eb66 | ||
|
adeb1acced | ||
|
95836a7dd1 | ||
|
88aa2491b0 | ||
|
73aa146f8a | ||
|
7d237c887f | ||
|
8d95e0bfe9 | ||
|
bd147fa285 | ||
|
ab9128ffb6 | ||
|
d2c92511ff | ||
|
9bd16473c9 | ||
|
ad3b8396be | ||
|
8a22f9db56 | ||
|
b00f5f1531 | ||
|
fc83bee80b | ||
|
0b64d50f4b | ||
|
ea6a5da3eb | ||
|
5bcdcdd5c6 | ||
|
9581b1b81a | ||
|
10216988d4 | ||
|
745373df6a | ||
|
23551a379b | ||
|
deef870213 | ||
|
37b731ff14 | ||
|
3c923ae943 | ||
|
b3c6f90a35 | ||
|
a2b217fdfc | ||
|
0725ea6b87 | ||
|
dad2744e71 | ||
|
94f079293b | ||
|
55f7328b3e | ||
|
5a74b0e722 | ||
|
3764946dfe | ||
|
1b7afe588b | ||
|
f7e2a59332 | ||
|
fc9def0879 | ||
|
7cd301fcbf | ||
|
16fec7c87f | ||
|
a7021b03e7 | ||
|
d24a73e349 | ||
|
d67310ec90 | ||
|
0f203fb198 | ||
|
aa8433f881 | ||
|
e54908be0d | ||
|
22bf6595da | ||
|
427485a1d7 | ||
|
23150ddb31 | ||
|
1a67fd5dec | ||
|
2d0b15ea7b | ||
|
611f3af772 | ||
|
e6804f1627 | ||
|
fecd696b2a | ||
|
3c87111710 | ||
|
592e7bcf27 | ||
|
d81b5ad955 | ||
|
fcac94a30e | ||
|
18397b13eb | ||
|
b4da3a51f7 | ||
|
b41a988d6f | ||
|
6c6937c507 | ||
|
6b179e673b | ||
|
2d1a52afe5 | ||
|
d4ce2a84cf | ||
|
7bd15d12ad | ||
|
17c987c605 | ||
|
fff05eb646 | ||
|
7be67accc1 | ||
|
a13726d4c5 | ||
|
9547411390 | ||
|
120c54a29e | ||
|
a6f2d34b7e | ||
|
458faff8a8 | ||
|
2cec8bdc5b | ||
|
8da6029624 | ||
|
9274ab16ed | ||
|
d5d39971b4 | ||
|
907566b39f | ||
|
7229d77bda | ||
|
d94325127f | ||
|
fe08cfd3ab | ||
|
fcc8d8e3fa | ||
|
e6e08e5d3e | ||
|
92a8ec4f68 | ||
|
b7a3110e7e | ||
|
b508ed6c9a | ||
|
1e69495728 | ||
|
30b41fdb88 | ||
|
c1eb0c54a3 | ||
|
cb7e0ed289 | ||
|
e6130dbf4a | ||
|
8e482321f2 | ||
|
8ad9e2bdd6 | ||
|
51e9ce5638 | ||
|
9150f07984 | ||
|
cd47070894 | ||
|
44e2ace833 | ||
|
5cf9311502 | ||
|
74b7ac834e | ||
|
a77a3d5a56 | ||
|
847dadac5d | ||
|
60590e0c49 | ||
|
415fc336ec | ||
|
81771eff4f | ||
|
6f9a3eb536 | ||
|
c6479c70c1 | ||
|
6cd8c35598 | ||
|
ffdd4f923d | ||
|
c32acd0123 | ||
|
63850a33c1 | ||
|
619de775fd | ||
|
48c528d76a | ||
|
52f82fc855 | ||
|
41e5d5ffff | ||
|
d7242d81a4 | ||
|
a925c81ba8 | ||
|
ceff398a01 | ||
|
dd7f59cef7 | ||
|
ff24ee604d | ||
|
560d076a5f | ||
|
2df7acf488 | ||
|
057f31407a | ||
|
e9be61cae2 | ||
|
66d26cfed6 | ||
|
b5a22379b8 | ||
|
405e0c2199 | ||
|
bd02c52d5a | ||
|
8032aa9f2f | ||
|
a95cd333d5 | ||
|
0618ff57e4 | ||
|
a3ce9bdb55 | ||
|
43d7dc026c | ||
|
0d7acafc44 | ||
|
080fcde440 | ||
|
adc505cef9 | ||
|
cc703ae3c6 | ||
|
7b2e2de3df | ||
|
06a5dab129 | ||
|
4d4f328527 | ||
|
49fc1e4e40 | ||
|
b2f53d48f7 | ||
|
75008dcb1a | ||
|
37af7b8db6 | ||
|
8a7225a249 | ||
|
55e0162a87 | ||
|
76392620be | ||
|
0e2d16982b | ||
|
fbb819d7a0 | ||
|
11119a6143 | ||
|
57adea3cc1 | ||
|
a923be53cd | ||
|
a107c868a0 | ||
|
3efe09ad72 | ||
|
f65f07dbcb | ||
|
bb6491bd06 | ||
|
0cc771f633 | ||
|
0fddb07c09 | ||
|
3c50b01c23 | ||
|
72d1a044d9 | ||
|
b26fdb424b | ||
|
1f58452b76 | ||
|
5a5d31f213 | ||
|
8948334d5d | ||
|
4eb9c9fdec | ||
|
5dcc721f15 | ||
|
803b3956fa | ||
|
fd981216da | ||
|
671f403602 | ||
|
33607acbb6 | ||
|
6f4e669e0d | ||
|
a6756ebe0f | ||
|
4b53d16ea0 | ||
|
5387763169 | ||
|
fb473007da | ||
|
c92ee47632 | ||
|
611d6d7b11 | ||
|
6f4df4b55e | ||
|
6ade0da597 | ||
|
a78632768c | ||
|
62f65ce3b9 | ||
|
049d2a72c8 | ||
|
94d126b0c8 | ||
|
e233ab4a74 | ||
|
506cc664b2 | ||
|
2d15a7a5cf | ||
|
80bfff384e | ||
|
252c63f4dd | ||
|
08c366d950 | ||
|
7b5106dd08 | ||
|
24ee35b95b | ||
|
ec8bbd2187 | ||
|
e695180049 | ||
|
3a5316cedc | ||
|
c45bed7f6c | ||
|
ca1f2a76d8 | ||
|
50b320063a | ||
|
bfe58a18e5 | ||
|
db6f80d721 | ||
|
0279637fe4 | ||
|
97cb7a678e | ||
|
0f2962a95f | ||
|
20254f7434 | ||
|
3fd109b6cd | ||
|
8b955ef002 | ||
|
23bc0f2c8f | ||
|
856f361271 | ||
|
e531d575be | ||
|
c0ebf12cc8 | ||
|
ed4f8eacea | ||
|
28051046d5 | ||
|
5fb5e1140a | ||
|
7dccc469b5 | ||
|
bd590213a1 | ||
|
fe2b976849 | ||
|
2981de95b0 | ||
|
51ab747019 | ||
|
c1c70e643a | ||
|
f5e052937c | ||
|
9639c5386f | ||
|
7e7d374263 | ||
|
37ef1d9bc6 | ||
|
86bc9bfd80 | ||
|
f53110c69b | ||
|
4d683a13c0 | ||
|
e2978029a5 | ||
|
024de4d8b3 | ||
|
b66e28faa0 | ||
|
c07cdbed3c | ||
|
c043f5058f | ||
|
e758986985 | ||
|
1c35c6951d | ||
|
36121007b1 | ||
|
19d579fc5c | ||
|
bb0a6f5854 | ||
|
7f91d512b8 | ||
|
de6a90a5ee | ||
|
fd8d64098f | ||
|
11209b4b42 | ||
|
663f151974 | ||
|
7771afc1a7 | ||
|
ea6df4705c | ||
|
1ce7ce8bfc | ||
|
86c8466ae4 | ||
|
55b72a49b6 | ||
|
33c9dce80d | ||
|
e6ed64ac7a | ||
|
8458cdb06f | ||
|
4dacd86bc3 | ||
|
792f188ecc | ||
|
71c1d38d50 | ||
|
74200f8dc4 | ||
|
048c9d40e7 | ||
|
41e4b81192 | ||
|
6a12853b65 | ||
|
d6d6db0aa3 | ||
|
d21398ee60 | ||
|
2b3cf88097 | ||
|
e2f0f8cc9b | ||
|
97f326a59c | ||
|
4d72f0be62 | ||
|
3d036256b2 | ||
|
96d45847f9 | ||
|
026cafad3f | ||
|
2c626844ef | ||
|
650aeb387b | ||
|
23839305d8 | ||
|
17e09df050 | ||
|
d405e92292 | ||
|
a969f9b3fb | ||
|
a40d4536f6 | ||
|
edc2e524f7 | ||
|
bc42f74b2b | ||
|
a1117918dd | ||
|
7cb8345e85 | ||
|
7a42800168 | ||
|
d3de5b778b | ||
|
e46b5a171f | ||
|
5273f8769d | ||
|
d6ff04cf17 | ||
|
b4e7246526 | ||
|
20d6eda1ad | ||
|
2ec4567f69 | ||
|
117e0de5bc | ||
|
f9a0695865 | ||
|
c2f1d19eaf | ||
|
9bceb39e26 | ||
|
72fea986d0 | ||
|
fa874ace12 | ||
|
cfeb9e01b7 | ||
|
4b6a8efe0f | ||
|
f37062c9b6 | ||
|
cd865e048e | ||
|
f890e2fcd3 | ||
|
972af21bca | ||
|
967ec86d9d | ||
|
a8f0b18219 | ||
|
0773d6b1d3 | ||
|
b1e228ea40 | ||
|
5463476a00 | ||
|
aa2c77abfc | ||
|
1fbb788528 | ||
|
49930e16f7 | ||
|
5555557cec | ||
|
d0921fb7b6 | ||
|
e7f24527c3 | ||
|
c4c60efab2 | ||
|
bf757bdc90 | ||
|
256aab9877 | ||
|
3b0c7992d2 | ||
|
bb0d96a005 | ||
|
2244f1ff77 | ||
|
28b6126853 | ||
|
2ab485de8c | ||
|
3518e37087 | ||
|
e9e75ea36b | ||
|
4552936e89 | ||
|
bf0a4e0d1a | ||
|
ba8a59b26f | ||
|
a3aeb38611 | ||
|
9e99a2ea96 | ||
|
882721f859 | ||
|
03940ad598 | ||
|
a1f6d78e04 | ||
|
ba3fd2202f | ||
|
ffc8bdd5f1 | ||
|
5e45f305ac | ||
|
55dd9484c9 | ||
|
bbfa32a115 | ||
|
7a70bbc51e | ||
|
93d59edb67 | ||
|
06ee6db7e7 | ||
|
a354b13139 | ||
|
ae009099ed | ||
|
5057c290c1 | ||
|
e242aa4ced | ||
|
e082156779 | ||
|
99c19b2a64 | ||
|
f608e176c1 | ||
|
8cabf0f87a | ||
|
b4ccf44d3a | ||
|
c68c13ab6e | ||
|
54c9a058df | ||
|
b1ba88c138 | ||
|
533d770753 | ||
|
8cc891a77f | ||
|
156ddee97c | ||
|
b93b7cdfd6 | ||
|
9f5c2f6829 | ||
|
62dc037d80 | ||
|
ae1ad39806 | ||
|
d347dac19e | ||
|
44edb9bdee | ||
|
a98f3f78af | ||
|
1196b05ff6 | ||
|
7d8823da6d | ||
|
593245f3fd | ||
|
60c9ed124d | ||
|
4c0fb344d1 | ||
|
9f16cc42f5 | ||
|
614be34081 | ||
|
b932cfc9ad | ||
|
73806c2d30 | ||
|
79e3c94669 | ||
|
f67e8947a5 | ||
|
d968730adf | ||
|
fabdac887e | ||
|
6f6ee7b54f | ||
|
087b038d77 | ||
|
73145888e6 | ||
|
3615abd4e2 | ||
|
72f2d14427 | ||
|
8ccaf4ac1d | ||
|
778c1de6eb | ||
|
a32aefe541 | ||
|
3c561af93d | ||
|
7eaeef821e | ||
|
7030185e4f | ||
|
5abf974682 | ||
|
fa146b1434 | ||
|
c52e878c01 | ||
|
050c17ee06 | ||
|
7109374d42 | ||
|
08350b884b | ||
|
bacbf8109b | ||
|
3e5a55b1e9 | ||
|
9de723d014 | ||
|
a0a3ef669e | ||
|
1c63d94736 | ||
|
32b5a2b5d0 | ||
|
3b93e92960 | ||
|
5ec7dec9f0 | ||
|
8a64b77e63 | ||
|
b23c831da5 | ||
|
eaaa7bb0e1 | ||
|
c49853d724 | ||
|
642155292a | ||
|
c9c6e7efc1 | ||
|
1ba1b756c9 | ||
|
f158d7f266 | ||
|
cef3a98b4e | ||
|
31f782e333 | ||
|
3e95a46bd2 | ||
|
52c0a0ebf3 | ||
|
b896c85bf0 | ||
|
7b72b24ee2 | ||
|
e771c9da81 | ||
|
9bd395007f | ||
|
2723e1cfb5 | ||
|
56df419bea | ||
|
9e3153453e | ||
|
26484130c3 | ||
|
495956d907 | ||
|
d95dab9f0c | ||
|
f99411f2f9 | ||
|
cdad0d6289 | ||
|
c5a7d7027c | ||
|
54e5e96ad2 | ||
|
fd349fb0fc | ||
|
0c8efd5331 | ||
|
5572833652 | ||
|
b7b7bad2fb | ||
|
44b1762b7d | ||
|
512fb63e1f | ||
|
5d4d68fe32 | ||
|
7647755e96 | ||
|
ef25c50e16 | ||
|
4aabb73847 | ||
|
cda07bf8d9 | ||
|
e19fab0615 | ||
|
e0980cf396 | ||
|
3d29b6b6a1 | ||
|
8faa3ae32d | ||
|
d15e683955 | ||
|
3ddfb4d594 | ||
|
ce64d7973b | ||
|
36dc01bf30 | ||
|
148d3fdb18 | ||
|
a6a4417eff | ||
|
849ed16d67 | ||
|
11e0e84dcf | ||
|
430182a0a5 | ||
|
48c9d9fa16 | ||
|
3cc3e387c5 | ||
|
ef6ed93ecd | ||
|
3b044e3044 | ||
|
b8c7a7c7e5 | ||
|
e2bb8cf0de | ||
|
3d3bd91700 | ||
|
e720430aa4 | ||
|
c76ee8cd4f | ||
|
10b604ef89 | ||
|
6b27126c37 | ||
|
d0f92113f4 | ||
|
fe018ee241 | ||
|
1c78a30535 | ||
|
921ebdcdd5 | ||
|
5ad5bac786 | ||
|
40a1a871cb | ||
|
8754837f15 | ||
|
7ccadebca0 | ||
|
a529ee5d06 | ||
|
0c9b6cf85f | ||
|
f686e2b9f0 | ||
|
7e6dd354cf | ||
|
7d04fb904a | ||
|
72170304ef | ||
|
74ae5f039b | ||
|
be05f96c26 | ||
|
6b096761e0 | ||
|
e257194d17 | ||
|
7974f30261 | ||
|
c500730ae6 | ||
|
7657bd7221 | ||
|
3055cca767 | ||
|
0b50ae1be0 | ||
|
530190df7a | ||
|
1390493a29 | ||
|
ace6b8004c | ||
|
a9c539821f | ||
|
5e87ff1ce6 | ||
|
e84bb603eb | ||
|
6fc252b2c7 | ||
|
104fa2caca | ||
|
17c7f4c648 | ||
|
c6815621df | ||
|
b38d765759 | ||
|
f99387b3e5 | ||
|
b66ce460b6 | ||
|
36db809588 | ||
|
305127137a | ||
|
e23909881a | ||
|
0e9ae8ee8a | ||
|
f13a0b5849 | ||
|
a6362a993c | ||
|
4c5d59a5bc | ||
|
192c05ce0d | ||
|
dd74b14d76 | ||
|
1f19bd9f1e | ||
|
202859cea1 | ||
|
eebadd188a | ||
|
002a39db2a | ||
|
1e7d682df5 | ||
|
e76762dd4d | ||
|
6369026925 | ||
|
b357f935d4 | ||
|
09bcfa2242 | ||
|
50c053cf56 | ||
|
dc95b4c098 | ||
|
eb0c694cc8 | ||
|
e116a95e34 | ||
|
f849805a73 | ||
|
7bacf4a5f0 | ||
|
1c66ac2da2 | ||
|
98863bcc23 | ||
|
1714d1eeef | ||
|
df151d4dbc | ||
|
6639546413 | ||
|
06e5b4baf6 | ||
|
c3ddd7f2bf | ||
|
a3f4f6b944 | ||
|
b9a6d029e2 | ||
|
b9cf470871 | ||
|
5a44385a54 | ||
|
0560963c63 | ||
|
1d222d67d5 | ||
|
4f79803885 | ||
|
b99cebcee6 | ||
|
2ac4bab4d5 | ||
|
f7c78f63a0 | ||
|
67682e7e93 | ||
|
e6c0a16342 |
256 changed files with 15165 additions and 6412 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,2 +1,4 @@
|
|||
*.journal text eol=lf
|
||||
*.feature text eol=lf
|
||||
poetry.lock text eol=lf
|
||||
pyrpoject.toml text eol=lf
|
||||
|
|
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ":new:, bug"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
<!--
|
||||
Hello, and thank you for reporting an issue!
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
-->
|
||||
|
||||
<!-- Please tell us about your environment -->
|
||||
### Environment
|
||||
- `jrnl --diagnostic` output: <!-- Run the command and paste the output here -->
|
||||
- Install method: <!-- How did you install jrnl? (pipx, brew, etc) -->
|
||||
|
||||
### Current Behavior
|
||||
<!--
|
||||
Please put a short description of what is currently happening.
|
||||
-->
|
||||
|
||||
### Expected Behavior
|
||||
<!--
|
||||
Please write a short description of what you would expect to happen
|
||||
(instead of what is currently happening).
|
||||
-->
|
||||
|
||||
### Repro Steps
|
||||
<!--
|
||||
Please provide the steps to reproduce the problem. It is important for you to
|
||||
be as precise as possible here, since more info will let us help you faster.
|
||||
If applicable, please run jrnl with --debug and paste the output.
|
||||
-->
|
||||
|
||||
### Other Information
|
||||
<!-- (e.g. more detailed explanation, stacktraces, related
|
||||
issues, suggestions how to fix, links for us to have context, eg.
|
||||
stackoverflow, gitter, etc) -->
|
74
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: "Bug Report"
|
||||
labels: [ ":new:", "bug" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Bug Report
|
||||
Hello, and thank you for reporting an issue!
|
||||
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
|
||||
- type: textarea
|
||||
id: diagnostic
|
||||
attributes:
|
||||
label: Diagnostic output
|
||||
description: Run `jrnl --diagnostic` and paste the output below
|
||||
placeholder: Paste output here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: Please put a short description of what is currently happening.
|
||||
placeholder: Tell us what is happening
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Please write a short description of what you would expect to happen
|
||||
placeholder: Tell us what should be happening
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: repro-steps
|
||||
attributes:
|
||||
label: Repro Steps
|
||||
description: |
|
||||
Provide the steps to reproduce the problem.
|
||||
|
||||
Please be as precise as possible, since more info will let us help you faster.
|
||||
placeholder: Repro steps
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: debug-output
|
||||
attributes:
|
||||
label: Debug output
|
||||
description: |
|
||||
Please provide the output of your command with the `--debug` flag on.
|
||||
placeholder: "example: `jrnl --debug lorem ipsum`"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other Information
|
||||
description: >
|
||||
Is there anything else we should know?
|
||||
|
||||
(e.g. more detailed explanation, stacktraces, related
|
||||
issues, suggestions how to fix, links for us to have context, eg.
|
||||
stackoverflow, gitter, etc)
|
||||
validations:
|
||||
required: false
|
31
.github/ISSUE_TEMPLATE/documentation.md
vendored
31
.github/ISSUE_TEMPLATE/documentation.md
vendored
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: Documentation Change
|
||||
about: Request or report any updates to our documentation (https://jrnl.sh)
|
||||
title: ''
|
||||
labels: ":new:, documentation"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Documentation Change
|
||||
<!--
|
||||
Hello, and thank you for reporting an issue!
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
-->
|
||||
|
||||
### Affected Page(s)
|
||||
<!--
|
||||
Please tell us which page, or pages, from the documentation site (https://jrnl.sh) are
|
||||
affected in this issue
|
||||
-->
|
||||
- <!-- example: https://jrnl.sh/en/stable/overview -->
|
||||
|
||||
### What Could Be Better?
|
||||
<!--
|
||||
Please write a short description of what you hope can be clarified or further explained.
|
||||
-->
|
||||
|
||||
### Other Information
|
||||
<!--
|
||||
Is there anything else we should know that might be helpful?
|
||||
-->
|
41
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/documentation.yaml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
name: Documentation Change
|
||||
description: Request or report any updates to our documentation (https://jrnl.sh)
|
||||
title: Documentation Change
|
||||
labels: [ ":new:", "documentation" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Documentation Change
|
||||
Hello, and thank you for reporting an issue!
|
||||
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
|
||||
- type: textarea
|
||||
id: affected-pages
|
||||
attributes:
|
||||
label: Affected Page(s)
|
||||
description: >
|
||||
Please tell us which page, or pages, from the documentation site
|
||||
(https://jrnl.sh) are affected in this issue
|
||||
placeholder: "example: https://jrnl.sh/en/stable/overview"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-could-be-better
|
||||
attributes:
|
||||
label: What Could Be Better?
|
||||
description: >
|
||||
Please write a short description of what you hope can be clarified or
|
||||
further explained.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other Information
|
||||
description: Is there anything else we should know that might be helpful?
|
||||
validations:
|
||||
required: false
|
31
.github/ISSUE_TEMPLATE/feature_request.md
vendored
31
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for jrnl
|
||||
title: ''
|
||||
labels: ":new:, enhancement"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Feature Request
|
||||
<!--
|
||||
Hello, and thank you for reporting an issue!
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
-->
|
||||
|
||||
### Use Case/Motivation
|
||||
<!--
|
||||
What is the motivation / use case for changing the behavior?
|
||||
-->
|
||||
|
||||
### Example Usage
|
||||
<!--
|
||||
Please provide examples of the usage you would like to see.
|
||||
e.g `jrnl --new-flag="super cool new feature"`
|
||||
-->
|
||||
|
||||
### Other information
|
||||
<!--
|
||||
Please provide any other relevant info about this request (e.g. stacktraces,
|
||||
related issues, suggestions how to fix, etc)
|
||||
-->
|
43
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Feature Request
|
||||
description: Suggest an idea for jrnl
|
||||
title: "Feature Report"
|
||||
labels: [ ":new:", "enhancement" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Feature Request
|
||||
Hello, and thank you for reporting an issue!
|
||||
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
|
||||
- type: textarea
|
||||
id: user-case
|
||||
attributes:
|
||||
label: Use Case/Motivation
|
||||
description: What is the motivation / use case for changing the behavior?
|
||||
placeholder: Tell us about your idea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: example-usage
|
||||
attributes:
|
||||
label: Example Usage
|
||||
description: Please provide examples of the usage you would like to see.
|
||||
placeholder: e.g `jrnl --new-flag="super cool new feature"`
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other Information
|
||||
description: >
|
||||
Is there anything else we should know?
|
||||
|
||||
(e.g. more detailed explanation, stacktraces, related
|
||||
issues, suggestions how to fix, links for us to have context, eg.
|
||||
stackoverflow, gitter, etc)
|
||||
validations:
|
||||
required: false
|
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
name: Support Request
|
||||
about: Get help with jrnl
|
||||
title: ''
|
||||
labels: ":new:, support"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Support Request
|
||||
<!--
|
||||
Hello, and thank you for reporting an issue!
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
-->
|
||||
|
||||
### Environment
|
||||
<!--
|
||||
Please tell us about your environment
|
||||
-->
|
||||
- Jrnl `--diagnostic` output: <!-- Run `jrnl --diagnostic` and paste the output -->
|
||||
- Install method: <!-- How did you install jrnl? (pipx, brew, etc) -->
|
||||
|
||||
### What are you trying to do?
|
||||
<!--
|
||||
Please write a short description of what is happening.
|
||||
-->
|
||||
|
||||
### What have you tried?
|
||||
<!--
|
||||
Have you tried anything to fix the problem? This can help give us more
|
||||
information to help you with.
|
||||
-->
|
||||
|
||||
### Other Information
|
||||
<!--
|
||||
If applicable, please run jrnl with --debug and paste the output.
|
||||
|
||||
Is there anything else we should know that might be helpful?
|
||||
e.g. detailed explanation, stacktraces, related issues, suggestions how to fix
|
||||
-->
|
54
.github/ISSUE_TEMPLATE/support_request.yaml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/support_request.yaml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
name: Support Request
|
||||
description: Get help with jrnl
|
||||
title: Support Request
|
||||
labels: [ ":new:", "support" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Support Request
|
||||
Hello, and thank you for reporting an issue!
|
||||
|
||||
Please fill out the points below, as it will make our process much easier.
|
||||
|
||||
- type: textarea
|
||||
id: diagnostic
|
||||
attributes:
|
||||
label: Diagnostic output
|
||||
description: Run `jrnl --diagnostic` and paste the output below
|
||||
placeholder: Paste output here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: What are you trying to do?
|
||||
description: Please put a short description of what is happening.
|
||||
placeholder: Tell us what is happening
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: tried
|
||||
attributes:
|
||||
label: What have you tried?
|
||||
description: >
|
||||
Have you tried anything to fix the problem? This can help give us more
|
||||
information to help you with.
|
||||
placeholder: Tell us what should be happening
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: Other Information
|
||||
description: >
|
||||
Is there anything else we should know?
|
||||
|
||||
(e.g. more detailed explanation, stacktraces, related
|
||||
issues, suggestions how to fix, links for us to have context, eg.
|
||||
stackoverflow, gitter, etc)
|
||||
validations:
|
||||
required: false
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -12,15 +12,13 @@ Here are some key points to include in your description:
|
|||
|
||||
### Checklist
|
||||
|
||||
- [ ] I have read the [contributing doc](https://github.com/jrnl-org/jrnl/blob/develop/CONTRIBUTING.md).
|
||||
- [ ] I have read the [contributing doc](https://github.com/jrnl-org/jrnl/blob/develop/docs/contributing.md).
|
||||
- [ ] I have included a link to the relevant issue number.
|
||||
- [ ] I have tested this code locally.
|
||||
- [ ] I have checked to ensure there aren't other open [pull requests](../pulls)
|
||||
for the same issue.
|
||||
- [ ] I have written new tests for these changes, as needed.
|
||||
- [ ] All tests pass.
|
||||
<!--
|
||||
NOTE: Your PR may not be reviewed if there are any failing tests. You can run
|
||||
tests locally with `make test` (see the contributing doc if you need help with
|
||||
`make`), or use our automated tests after you submit your PR.
|
||||
tests locally with `poe test` (see the contributing doc if you need help with
|
||||
`poe`), or use our automated tests after you submit your PR.
|
||||
-->
|
||||
|
|
17
.github/actionlint-matcher.json
vendored
Normal file
17
.github/actionlint-matcher.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "actionlint",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4,
|
||||
"code": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
46
.github/actions/run_tests/action.yaml
vendored
Normal file
46
.github/actions/run_tests/action.yaml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: run jrnl tests
|
||||
description: Runs all jrnl tests on multiple platforms
|
||||
inputs:
|
||||
cache-string:
|
||||
description: 'Cache string secret. Change to bust the cache'
|
||||
required: true
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
shell: bash
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Capture full Python version in env
|
||||
run: echo "PYTHON_FULL_VERSION=$(python --version)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: poetry cache # Change CACHE_STRING secret to bust the cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_FULL_VERSION }}-${{ inputs.cache-string }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo '::group::poetry'
|
||||
pip --disable-pip-version-check install poetry
|
||||
poetry config --local virtualenvs.in-project true
|
||||
echo '::endgroup::'
|
||||
|
||||
echo '::group::Other dependencies'
|
||||
poetry sync
|
||||
echo '::endgroup::'
|
||||
|
||||
echo 'DEPS_INSTALLED=true' >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Linting & Testing
|
||||
if: ${{ env.DEPS_INSTALLED == 'true' }}
|
||||
run: poetry run poe test
|
||||
shell: bash
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- wren
|
||||
- micahellison
|
||||
labels:
|
||||
- packaging
|
14
.github/renovate.json
vendored
Normal file
14
.github/renovate.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"schedule": [ "at any time" ],
|
||||
"prConcurrentLimit": 10,
|
||||
"prHourlyLimit": 10,
|
||||
"reviewers": [
|
||||
"wren",
|
||||
"micahellison"
|
||||
],
|
||||
"labels": [ "packaging" ]
|
||||
}
|
50
.github/workflows/changelog.yaml
vendored
50
.github/workflows/changelog.yaml
vendored
|
@ -1,3 +1,6 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Changelog
|
||||
|
||||
on:
|
||||
|
@ -18,7 +21,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||
|
||||
|
@ -49,14 +52,14 @@ jobs:
|
|||
if [[ ! ${GITHUB_REF##*/} =~ $prerelease_regex ]]; then
|
||||
echo '::debug::Actual release (not a prerelease)'
|
||||
TAG_REGEX="$prerelease_regex"
|
||||
echo "FULL_RELEASE=true" >> $GITHUB_ENV
|
||||
echo "FULL_RELEASE=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
fi
|
||||
echo "::debug::TAG_REGEX: $TAG_REGEX"
|
||||
|
||||
if [[ "$(git rev-parse "origin/$BRANCH")" != $GITHUB_SHA ]]; then
|
||||
if [[ "$(git rev-parse "origin/$BRANCH")" != "$GITHUB_SHA" ]]; then
|
||||
# Normal build on a branch (no tag)
|
||||
echo "::debug::BRANCH: $BRANCH $(git rev-parse origin/$BRANCH)"
|
||||
echo "::debug::BRANCH: $BRANCH $(git rev-parse "origin/$BRANCH")"
|
||||
echo "::debug::GITHUB_SHA: $GITHUB_SHA"
|
||||
echo "::error::$BRANCH has been updated since build started. Aborting changelog."
|
||||
exit 1
|
||||
|
@ -69,10 +72,12 @@ jobs:
|
|||
echo "::debug::FILENAME: CHANGELOG.md"
|
||||
echo "::debug::SINCE_TAG: $SINCE_TAG"
|
||||
|
||||
echo "BRANCH=$BRANCH" >> $GITHUB_ENV
|
||||
echo "TAG_REGEX=$TAG_REGEX" >> $GITHUB_ENV
|
||||
echo "FILENAME=CHANGELOG.md" >> $GITHUB_ENV
|
||||
echo "SINCE_TAG=$SINCE_TAG" >> $GITHUB_ENV
|
||||
{
|
||||
echo "BRANCH=$BRANCH"
|
||||
echo "TAG_REGEX=$TAG_REGEX"
|
||||
echo "FILENAME=CHANGELOG.md"
|
||||
echo "SINCE_TAG=$SINCE_TAG"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prep changelog file (clear out old lines)
|
||||
run: |
|
||||
|
@ -85,7 +90,14 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "1,$(expr $tagline - 1)d" "$FILENAME"
|
||||
if [[ $tagline == 1 ]]; then
|
||||
echo "::error::Something is wrong."
|
||||
echo "::error::The latest release ${SINCE_TAG} is the first line in the changelog,"
|
||||
echo "::error::but the h1 '# Changelog' should always be the first line."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed -i "1,$(( tagline - 1 ))d" "$FILENAME"
|
||||
# delete generated line (or it will be added multiple times)
|
||||
sed -i '/This Changelog was automatically generated by/d' "$FILENAME"
|
||||
# delete trailing empty lines
|
||||
|
@ -124,25 +136,35 @@ jobs:
|
|||
git diff
|
||||
if [[ $(grep -c '^# Changelog$' "$FILENAME") != 1 ]]; then
|
||||
echo '::error::Something is wrong with the changelog.'
|
||||
exit 1
|
||||
fi
|
||||
SOMETHING_CHANGED=false
|
||||
git diff --exit-code || SOMETHING_CHANGED=true
|
||||
echo "::debug::SOMETHING_CHANGED: $SOMETHING_CHANGED"
|
||||
echo "SOMETHING_CHANGED=$SOMETHING_CHANGED" >> $GITHUB_ENV
|
||||
echo "SOMETHING_CHANGED=$SOMETHING_CHANGED" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Commit
|
||||
if: env.SOMETHING_CHANGED == 'true'
|
||||
run: |
|
||||
git config user.email "jrnl.bot@gmail.com"
|
||||
git config user.name "Jrnl Bot"
|
||||
git config --global user.name "${{ secrets.JRNL_BOT_NAME }}"
|
||||
git config --global user.email "${{ secrets.JRNL_BOT_EMAIL }}"
|
||||
git add "$FILENAME"
|
||||
git commit -m "Update changelog [ci skip]"
|
||||
git push origin $BRANCH
|
||||
git push origin "$BRANCH"
|
||||
|
||||
- name: Update tag to include changelog
|
||||
if: startsWith(env.GITHUB_REF, 'refs/tags/')
|
||||
run: |
|
||||
# This is a tag build (releases and prereleases)
|
||||
# update the tag to include the changelog
|
||||
git tag -fam "$GITHUB_REF_NAME" "$GITHUB_REF_NAME"
|
||||
git push --tags --force
|
||||
|
||||
- name: Merge to Release branch
|
||||
if: env.FULL_RELEASE == 'true'
|
||||
run: |
|
||||
git fetch --unshallow origin
|
||||
git checkout release
|
||||
git merge --ff-only $BRANCH
|
||||
git merge --ff-only "$BRANCH"
|
||||
git push origin release
|
||||
|
||||
|
|
44
.github/workflows/docs.yaml
vendored
44
.github/workflows/docs.yaml
vendored
|
@ -1,3 +1,6 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
|
@ -9,6 +12,8 @@ on:
|
|||
- 'mkdocs.yml'
|
||||
- 'readthedocs.yml'
|
||||
- '.github/workflows/docs.yaml'
|
||||
- 'tasks.py'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
|
@ -17,31 +22,41 @@ on:
|
|||
- 'mkdocs.yml'
|
||||
- 'readthedocs.yml'
|
||||
- '.github/workflows/docs.yaml'
|
||||
- 'tasks.py'
|
||||
- 'pyproject.toml'
|
||||
|
||||
jobs:
|
||||
accessibility:
|
||||
if: contains(toJson(github.event.commits), '[ci skip]') == false
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: [ '3.11' ]
|
||||
os: [ ubuntu-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@main
|
||||
|
||||
- name: Capture full Python version in env
|
||||
run: echo "PYTHON_FULL_VERSION=$(python --version)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: poetry cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .venv
|
||||
key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ matrix.python-version }}
|
||||
key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_FULL_VERSION }}-${{ secrets.CACHE_STRING }}
|
||||
|
||||
- name: npm cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-pa11y-v3
|
||||
|
@ -50,19 +65,12 @@ jobs:
|
|||
run: |
|
||||
pip install poetry
|
||||
poetry config --local virtualenvs.in-project true
|
||||
poetry install --no-root --remove-untracked
|
||||
npm install pa11y-ci
|
||||
echo "node_modules/.bin" >> $GITHUB_PATH
|
||||
poetry sync --no-root
|
||||
npm install
|
||||
echo "node_modules/.bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Start docs server
|
||||
run: poetry run mkdocs serve &
|
||||
|
||||
- name: Generate sitemap
|
||||
env:
|
||||
site_url: http://127.0.0.1:8000
|
||||
run: |
|
||||
select="{urls: [\"${site_url}/\", \"${site_url}/search.html?q=jrnl\", .urlset.url[].loc]}"
|
||||
curl -s "$site_url/sitemap.xml" | poetry run xq "$select" > list.json
|
||||
run: poetry run poe docs-run &
|
||||
|
||||
- name: Accessibility testing (Pa11y)
|
||||
run: pa11y-ci -c list.json
|
||||
run: poetry run poe docs-check
|
||||
|
|
156
.github/workflows/release.yaml
vendored
156
.github/workflows/release.yaml
vendored
|
@ -1,20 +1,22 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (e.g. v2.5, v2.5.1-beta, v2.6-beta2)'
|
||||
type: string
|
||||
required: true
|
||||
include_repo_version:
|
||||
description: 'Update version in repo? (true/false)'
|
||||
require: true
|
||||
default: true
|
||||
include_pypi:
|
||||
description: 'Publish to PyPI? (true/false)'
|
||||
description: 'Update version in repo?'
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
include_brew:
|
||||
description: 'Publish to Homebrew? (true/false)'
|
||||
include_pypi:
|
||||
description: 'Publish to PyPI?'
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
|
||||
|
@ -56,15 +58,15 @@ jobs:
|
|||
run: |
|
||||
JRNL_VERSION="${{ github.event.inputs.version }}"
|
||||
echo "::debug::version: $JRNL_VERSION"
|
||||
echo "JRNL_VERSION=$JRNL_VERSION" >> $GITHUB_ENV
|
||||
echo "JRNL_VERSION=$JRNL_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||
|
||||
|
@ -80,7 +82,7 @@ jobs:
|
|||
if: ${{ github.event.inputs.include_repo_version == 'true' }}
|
||||
run: |
|
||||
poetry version "$JRNL_VERSION"
|
||||
echo __version__ = \"$JRNL_VERSION\" > jrnl/__version__.py
|
||||
echo "__version__ = \"$JRNL_VERSION\"" > jrnl/__version__.py
|
||||
|
||||
- name: Commit updated files
|
||||
if: ${{ github.event.inputs.include_repo_version == 'true' && github.repository == env.HOME_REPO }}
|
||||
|
@ -103,131 +105,5 @@ jobs:
|
|||
- name: Get PyPI version
|
||||
id: pypi-version-getter
|
||||
run: |
|
||||
pypi_version="$(ls dist/jrnl-*.tar.gz | sed -r 's!dist/jrnl-(.*)\.tar\.gz!\1!')"
|
||||
echo "::set-output name=pypi_version::$pypi_version"
|
||||
|
||||
release_homebrew:
|
||||
if: ${{ github.event.inputs.include_brew == 'true' }}
|
||||
needs: release_pypi
|
||||
name: "Release to Homebrew"
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
HOMEBREW_NO_INSTALL_CLEANUP: 1
|
||||
HOME_REPO: ${{ secrets.HOME_REPO }}
|
||||
steps:
|
||||
|
||||
- name: Get version
|
||||
run: |
|
||||
JRNL_VERSION="${{ github.event.inputs.version }}"
|
||||
PYPI_VERSION="${{ needs.release_pypi.outputs.pypi_version }}"
|
||||
|
||||
echo "::debug::jrnl version: $JRNL_VERSION"
|
||||
echo "::debug::pypi version: $PYPI_VERSION"
|
||||
|
||||
echo "JRNL_VERSION=$JRNL_VERSION" >> $GITHUB_ENV
|
||||
echo "PYPI_VERSION=$PYPI_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine type of release
|
||||
env:
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
run: |
|
||||
if [[ $JRNL_VERSION =~ (alpha|beta) ]]; then
|
||||
echo '::debug::Prerelease (not a full release)'
|
||||
{
|
||||
echo "RELEASE_TYPE=pre"
|
||||
echo "FORMULA_REPO=${REPO_OWNER}/homebrew-prerelease"
|
||||
echo "BOT_REPO=jrnl-bot/homebrew-prerelease"
|
||||
echo "FORMULA_NAME=jrnl-beta"
|
||||
} >> $GITHUB_ENV
|
||||
else
|
||||
echo '::debug::Full release (not a prerelease)'
|
||||
if [[ "${{ github.repository }}" == "${HOME_REPO}" ]]; then
|
||||
REPO_OWNER="homebrew"
|
||||
fi
|
||||
{
|
||||
echo "RELEASE_TYPE=full"
|
||||
echo "FORMULA_REPO=${REPO_OWNER}/homebrew-core"
|
||||
echo "BOT_REPO=jrnl-bot/homebrew-core"
|
||||
echo "FORMULA_NAME=jrnl"
|
||||
} >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Checkout homebrew repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||
repository: ${{ env.FORMULA_REPO }}
|
||||
|
||||
- name: Config git user
|
||||
run: |
|
||||
git config --global user.name "${{ secrets.JRNL_BOT_NAME }}"
|
||||
git config --global user.email "${{ secrets.JRNL_BOT_EMAIL }}"
|
||||
|
||||
- name: Create branch
|
||||
run: |
|
||||
BRANCH_NAME="jrnl-${JRNL_VERSION}--${RANDOM}"
|
||||
git remote rename origin upstream
|
||||
git remote add origin "https://github.com/${BOT_REPO}.git"
|
||||
git fetch --unshallow upstream
|
||||
git fetch origin
|
||||
git checkout -b $BRANCH_NAME
|
||||
git push -u origin $BRANCH_NAME
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install pipgrip
|
||||
|
||||
- name: Query PyPI API
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_seconds: 10
|
||||
max_attempts: 30
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
curl -Ls https://pypi.org/pypi/jrnl/json > api_response.json
|
||||
# if query doesn't have our version yet, give it some time before trying again
|
||||
if [[ "null" == "$(jq ".releases[\"${PYPI_VERSION}\"][1].url" -r api_response.json)" ]]; then
|
||||
echo "::debug::PYPI_VERSION: $PYPI_VERSION"
|
||||
echo "::debug::JQ VALUE: $(jq ".releases[\"${PYPI_VERSION}\"][1].url" -r api_response.json)"
|
||||
echo "::group::cat api_response.json"
|
||||
cat api_response.json
|
||||
echo "::endgroup::"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Update Homebrew Formula
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 8
|
||||
max_attempts: 6
|
||||
retry_wait_seconds: 30
|
||||
command: >
|
||||
brew bump-formula-pr "Formula/${FORMULA_NAME}.rb"
|
||||
--url $(jq ".releases[\"${PYPI_VERSION}\"][1].url" -r api_response.json)
|
||||
--sha256 $(jq ".releases[\"${PYPI_VERSION}\"][1].digests.sha256" -r api_response.json)
|
||||
--version=$PYPI_VERSION
|
||||
--no-audit
|
||||
--write
|
||||
--commit
|
||||
--force
|
||||
--verbose
|
||||
|
||||
- name: Update commit message
|
||||
run: |
|
||||
git commit --amend \
|
||||
-m "jrnl ${JRNL_VERSION}" \
|
||||
-m "Update jrnl to ${JRNL_VERSION}" \
|
||||
-m '${{ secrets.RELEASE_COAUTHORS }}'
|
||||
|
||||
- name: Push commit
|
||||
run: |
|
||||
git show head
|
||||
git push
|
||||
|
||||
- name: Open pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||
run: >
|
||||
gh pr create
|
||||
--title "jrnl ${JRNL_VERSION}"
|
||||
--body 'Created with `brew bump-formula-pr`.'
|
||||
pypi_version="$(find dist/jrnl-*.tar.gz | sed -r 's!dist/jrnl-(.*)\.tar\.gz!\1!')"
|
||||
echo "pypi_version=$pypi_version" >> "$GITHUB_OUTPUT"
|
||||
|
|
72
.github/workflows/testing.yaml
vendored
72
.github/workflows/testing.yaml
vendored
|
@ -1,72 +0,0 @@
|
|||
name: Testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, release ]
|
||||
paths:
|
||||
- 'jrnl/**'
|
||||
- 'features/**'
|
||||
- 'tests/**'
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- 'jrnl/**'
|
||||
- 'features/**'
|
||||
- 'tests/**'
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: >
|
||||
! contains(github.event.head_commit.message, '[ci skip]')
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ 3.7, 3.8, 3.9 ]
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
exclude: # Added for GitHub Actions PR problem 2020-12-19 -- remove later!
|
||||
- os: windows-latest
|
||||
python-version: 3.9
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: poetry cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .venv
|
||||
key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ matrix.python-version }}-v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install poetry
|
||||
poetry config --local virtualenvs.in-project true
|
||||
poetry install --remove-untracked
|
||||
|
||||
- name: Code formatting (Black)
|
||||
if: success() || failure()
|
||||
run: |
|
||||
poetry run black --version
|
||||
poetry run black --check --diff .
|
||||
|
||||
- name: Code Style (PyFlakes)
|
||||
if: success() || failure()
|
||||
run: |
|
||||
poetry run pyflakes --version
|
||||
poetry run pyflakes jrnl features tests
|
||||
|
||||
- name: Test with pytest
|
||||
if: success() || failure()
|
||||
run: poetry run pytest --junitxml=reports/pytest/results.xml
|
||||
|
||||
- name: Test with behave
|
||||
if: success() || failure()
|
||||
run: poetry run behave --no-skipped --format progress2 --junit --junit-directory reports/behave
|
35
.github/workflows/testing_pipelines.yaml
vendored
Normal file
35
.github/workflows/testing_pipelines.yaml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Testing Pipeline Files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, release ]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- '.github/actions/**'
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- '.github/actions/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * SAT'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: >
|
||||
! contains(github.event.head_commit.message, '[ci skip]')
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check workflow files
|
||||
uses: docker://rhysd/actionlint:latest
|
||||
with:
|
||||
args: -color
|
48
.github/workflows/testing_prs.yaml
vendored
Normal file
48
.github/workflows/testing_prs.yaml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, release ]
|
||||
paths:
|
||||
- 'jrnl/**'
|
||||
- 'features/**'
|
||||
- 'tests/**'
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/testing_prs.yaml'
|
||||
- 'tasks.py'
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
paths:
|
||||
- 'jrnl/**'
|
||||
- 'features/**'
|
||||
- 'tests/**'
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/testing_prs.yaml'
|
||||
- 'tasks.py'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash # needed to prevent Windows from using PowerShell
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: >
|
||||
! contains(github.event.head_commit.message, '[ci skip]')
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
cache-string: ${{ secrets.CACHE_STRING }}
|
28
.github/workflows/testing_schedule.yaml
vendored
Normal file
28
.github/workflows/testing_schedule.yaml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
name: Testing
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * SAT'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash # needed to prevent Windows from using PowerShell
|
||||
|
||||
jobs:
|
||||
test_all:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
cache-string: ${{ secrets.CACHE_STRING }}
|
71
.gitignore
vendored
71
.gitignore
vendored
|
@ -1,56 +1,63 @@
|
|||
# Byte-compiled DLL and Shared Library files
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
.python-version
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
bin/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
.tox/
|
||||
var/
|
||||
node_modules/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.flakeheaven_cache/
|
||||
|
||||
# Versioning
|
||||
.python-version
|
||||
.tool-versions
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
.DS_Store
|
||||
.travis-solo
|
||||
Icon
|
||||
pip-log.txt
|
||||
|
||||
# Documentation
|
||||
_build
|
||||
_sources
|
||||
_static
|
||||
coverage.xml
|
||||
exp/
|
||||
objects.inv
|
||||
searchindex.js
|
||||
|
||||
# MS Visual Studio (PyTools)
|
||||
obj
|
||||
*.pyproj
|
||||
*.sln
|
||||
*.suo
|
||||
|
||||
# virtaulenv
|
||||
# virtualenv
|
||||
.venv*/
|
||||
env/
|
||||
env*/
|
||||
venv*/
|
||||
.venv*/
|
||||
|
||||
# PyCharm Project files
|
||||
.idea/
|
||||
|
||||
# export testing directories
|
||||
exp/
|
||||
|
||||
_extras/
|
||||
*.sublime-*
|
||||
site/
|
||||
# Editor and IDE specific files
|
||||
# Since contributors may want to user a variety of development tools it is
|
||||
# recommended that editor specific file types be ignored globally by each
|
||||
# contributor via a global gitignore. Instructions for setting up a global
|
||||
# ignore file can be found here:
|
||||
# https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
# (Configuring ignored files for all repositories on your computer)
|
||||
# Examples of such files are:
|
||||
# MS Visual Studio (PyTools)
|
||||
# obj
|
||||
# *.suo
|
||||
# PyCharm
|
||||
# .idea/
|
||||
# VS Code
|
||||
# .vscode/settings.json
|
|
@ -5,6 +5,13 @@
|
|||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3"
|
||||
|
||||
|
||||
# Build documentation in the docs/ directory
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
|
@ -17,6 +24,5 @@ formats:
|
|||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: docs_theme/requirements.txt
|
532
CHANGELOG.md
532
CHANGELOG.md
|
@ -2,16 +2,544 @@
|
|||
|
||||
## [Unreleased](https://github.com/jrnl-org/jrnl/)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.7...HEAD)
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.2.1...HEAD)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- poetry warning - "poetry.dev-dependencies" section is deprecated [\#1975](https://github.com/jrnl-org/jrnl/issues/1975)
|
||||
- Homebrew autobump error on jrnl release [\#1961](https://github.com/jrnl-org/jrnl/issues/1961)
|
||||
|
||||
**Build:**
|
||||
|
||||
- Remove release step to publish to Homebrew [\#1994](https://github.com/jrnl-org/jrnl/pull/1994) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Update dependency rich to v14 [\#1989](https://github.com/jrnl-org/jrnl/pull/1989) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency python to 3.13 [\#1988](https://github.com/jrnl-org/jrnl/pull/1988) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.25.0 [\#1986](https://github.com/jrnl-org/jrnl/pull/1986) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tzlocal to v5.3.1 [\#1984](https://github.com/jrnl-org/jrnl/pull/1984) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency jinja2 to v3.1.6 [\#1983](https://github.com/jrnl-org/jrnl/pull/1983) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.33.1 [\#1982](https://github.com/jrnl-org/jrnl/pull/1982) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest to v8.3.5 [\#1981](https://github.com/jrnl-org/jrnl/pull/1981) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency cryptography to v44.0.2 [\#1980](https://github.com/jrnl-org/jrnl/pull/1980) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruff to v0.11.3 [\#1978](https://github.com/jrnl-org/jrnl/pull/1978) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v4.2.1](https://pypi.org/project/jrnl/v4.2.1/) (2025-02-25)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.2...v4.2.1)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Typing animation in landing page is broken [\#1969](https://github.com/jrnl-org/jrnl/issues/1969)
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Update dependency pytest to \>=8.1.1 [\#1974](https://github.com/jrnl-org/jrnl/pull/1974) ([wren](https://github.com/wren))
|
||||
- Update dependency black to v25 [\#1973](https://github.com/jrnl-org/jrnl/pull/1973) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tzlocal to v5.3 [\#1972](https://github.com/jrnl-org/jrnl/pull/1972) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruamel.yaml to v0.18.10 [\#1967](https://github.com/jrnl-org/jrnl/pull/1967) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency jinja2 to v3.1.5 [\#1966](https://github.com/jrnl-org/jrnl/pull/1966) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency cryptography to v44 [\#1962](https://github.com/jrnl-org/jrnl/pull/1962) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-bdd to v8.1.0 [\#1952](https://github.com/jrnl-org/jrnl/pull/1952) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.32.2 [\#1951](https://github.com/jrnl-org/jrnl/pull/1951) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency keyring to v25.6.0 [\#1948](https://github.com/jrnl-org/jrnl/pull/1948) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruff to v0.9.7 [\#1947](https://github.com/jrnl-org/jrnl/pull/1947) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency rich to v13.9.4 [\#1946](https://github.com/jrnl-org/jrnl/pull/1946) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.24.1 [\#1945](https://github.com/jrnl-org/jrnl/pull/1945) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v4.2](https://pypi.org/project/jrnl/v4.2/) (2024-11-17)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.2-beta...v4.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Allow timestamps in command line for new entries with editor [\#1083](https://github.com/jrnl-org/jrnl/issues/1083)
|
||||
- Add Python 3.13 support [\#1893](https://github.com/jrnl-org/jrnl/issues/1893)
|
||||
- Add calendar heatmap display format [\#1759](https://github.com/jrnl-org/jrnl/pull/1759) ([alichtman](https://github.com/alichtman))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- -contains doesn't accept multiple search terms, doesn't work with -and [\#1877](https://github.com/jrnl-org/jrnl/issues/1877)
|
||||
- Tests failing on develop branch starting with pytest-bdd 7.1.2 [\#1875](https://github.com/jrnl-org/jrnl/issues/1875)
|
||||
- Ignore color when used in a pipeline [\#1839](https://github.com/jrnl-org/jrnl/issues/1839)
|
||||
- Fix -contains to allow multiple terms with "OR" logic unless -and is added [\#1890](https://github.com/jrnl-org/jrnl/pull/1890) ([eigenric](https://github.com/eigenric))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Recommend pipx as default installation method [\#1888](https://github.com/jrnl-org/jrnl/issues/1888)
|
||||
- Remove documentation recommendation to install pipx through brew or pip [\#1886](https://github.com/jrnl-org/jrnl/issues/1886)
|
||||
- Document security risks of using a computer that someone else has admin access to [\#1793](https://github.com/jrnl-org/jrnl/issues/1793)
|
||||
- Recommend pipx as easiest installation method for all OSes and remove warning about apt [\#1889](https://github.com/jrnl-org/jrnl/pull/1889) ([micahellison](https://github.com/micahellison))
|
||||
- Docs accessibility checker failure - contrast ratio [\#1934](https://github.com/jrnl-org/jrnl/issues/1934)
|
||||
- Docs accessibility test runner failing [\#1932](https://github.com/jrnl-org/jrnl/issues/1932)
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Update actions/cache action to v4 [\#1847](https://github.com/jrnl-org/jrnl/pull/1847) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update actions/setup-python action to v5 [\#1848](https://github.com/jrnl-org/jrnl/pull/1848) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency black to v24.8.0 [\#1923](https://github.com/jrnl-org/jrnl/pull/1923) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency cryptography to v43.0.3 [\#1942](https://github.com/jrnl-org/jrnl/pull/1942) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency jinja2 to v3.1.4 [\#1892](https://github.com/jrnl-org/jrnl/pull/1892) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency keyring to v25.4.1 [\#1924](https://github.com/jrnl-org/jrnl/pull/1924) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency mkdocs to v1.6.1 [\#1895](https://github.com/jrnl-org/jrnl/pull/1895) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pa11y-ci to v3.1.0 [\#1831](https://github.com/jrnl-org/jrnl/pull/1831) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency parse-type to v0.6.4 [\#1936](https://github.com/jrnl-org/jrnl/pull/1936) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update peter-evans/create-pull-request action to v7 [\#1929](https://github.com/jrnl-org/jrnl/pull/1929) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.29.0 [\#1925](https://github.com/jrnl-org/jrnl/pull/1925) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest to v7.4.4 [\#1845](https://github.com/jrnl-org/jrnl/pull/1845) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-bdd to v7.3.0 [\#1896](https://github.com/jrnl-org/jrnl/pull/1896) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-xdist to v3.6.1 [\#1897](https://github.com/jrnl-org/jrnl/pull/1897) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency python-dateutil to v2.9.0 [\#1898](https://github.com/jrnl-org/jrnl/pull/1898) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency requests to v2.32.3 [\#1899](https://github.com/jrnl-org/jrnl/pull/1899) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency rich to v13.9.2 [\#1937](https://github.com/jrnl-org/jrnl/pull/1937) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruamel.yaml to v0.18.6 [\#1855](https://github.com/jrnl-org/jrnl/pull/1855) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruff to v0.7.0 [\#1938](https://github.com/jrnl-org/jrnl/pull/1938) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.23.0 [\#1935](https://github.com/jrnl-org/jrnl/pull/1935) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency typed.js to v2.1.0 [\#1861](https://github.com/jrnl-org/jrnl/pull/1861) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency xmltodict to v0.14.2 [\#1940](https://github.com/jrnl-org/jrnl/pull/1940) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update nick-invision/retry action to v3 [\#1851](https://github.com/jrnl-org/jrnl/pull/1851) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update peter-evans/create-pull-request action to v6 [\#1852](https://github.com/jrnl-org/jrnl/pull/1852) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v4.1](https://pypi.org/project/jrnl/v4.1/) (2023-11-04)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.1-beta2...v4.1)
|
||||
|
||||
**Build:**
|
||||
|
||||
- Add Python 3.12 support [\#1761](https://github.com/jrnl-org/jrnl/pull/1761) ([micahellison](https://github.com/micahellison))
|
||||
- Set new required build fields in the ReadTheDocs config file [\#1803](https://github.com/jrnl-org/jrnl/pull/1803) ([micahellison](https://github.com/micahellison))
|
||||
- Replace flake8 and isort with ruff linter and add `black --check` to linting step [\#1763](https://github.com/jrnl-org/jrnl/pull/1763) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Add note about messages going to `stderr` and the implication for piping [\#1768](https://github.com/jrnl-org/jrnl/pull/1768) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Drop/replace ansiwrap dependency [\#1191](https://github.com/jrnl-org/jrnl/issues/1191)
|
||||
- Use rich instead of ansiwrap to wrap text [\#1693](https://github.com/jrnl-org/jrnl/pull/1693) ([micahellison](https://github.com/micahellison))
|
||||
- Update actions/checkout action to v4 [\#1788](https://github.com/jrnl-org/jrnl/pull/1788) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency black to v23.10.1 [\#1811](https://github.com/jrnl-org/jrnl/pull/1811) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency cryptography to v41.0.5 [\#1815](https://github.com/jrnl-org/jrnl/pull/1815) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency keyring to v24.2.0 [\#1760](https://github.com/jrnl-org/jrnl/pull/1760) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency mkdocs to v1.5.3 [\#1795](https://github.com/jrnl-org/jrnl/pull/1795) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency parse-type to v0.6.2 [\#1762](https://github.com/jrnl-org/jrnl/pull/1762) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.24.1 [\#1806](https://github.com/jrnl-org/jrnl/pull/1806) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest to v7.4.3 [\#1816](https://github.com/jrnl-org/jrnl/pull/1816) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-bdd to v7 [\#1807](https://github.com/jrnl-org/jrnl/pull/1807) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency rich to v13.6.0 [\#1794](https://github.com/jrnl-org/jrnl/pull/1794) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruamel.yaml to v0.18.3 [\#1813](https://github.com/jrnl-org/jrnl/pull/1813) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruff to v0.1.3 [\#1810](https://github.com/jrnl-org/jrnl/pull/1810) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.11.3 [\#1782](https://github.com/jrnl-org/jrnl/pull/1782) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tzlocal to v5.2 [\#1814](https://github.com/jrnl-org/jrnl/pull/1814) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
**Special thanks:**
|
||||
- jrnl uses UTC instead of local time for entries in WSL/Ubuntu [\#1607](https://github.com/jrnl-org/jrnl/issues/1607) investigated and reported upstream by [giuseppedandrea](https://github.com/giuseppedandrea)
|
||||
|
||||
## [v4.0.1](https://pypi.org/project/jrnl/v4.0.1/) (2023-06-20)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0.1-beta...v4.0.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- jrnl crashes when running `jrnl --list --format json` and `jrnl --list --format yaml` [\#1737](https://github.com/jrnl-org/jrnl/issues/1737)
|
||||
- Refactor --template code [\#1711](https://github.com/jrnl-org/jrnl/pull/1711) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Fix linting issue in CI pipeline [\#1743](https://github.com/jrnl-org/jrnl/pull/1743) ([wren](https://github.com/wren))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Update dependency ruamel.yaml to v0.17.28 [\#1749](https://github.com/jrnl-org/jrnl/pull/1749) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency requests to v2.31.0 [\#1748](https://github.com/jrnl-org/jrnl/pull/1748) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ruamel.yaml to v0.17.26 [\#1746](https://github.com/jrnl-org/jrnl/pull/1746) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tzlocal to v5 [\#1741](https://github.com/jrnl-org/jrnl/pull/1741) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-xdist to v3.3.1 [\#1740](https://github.com/jrnl-org/jrnl/pull/1740) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.20.0 [\#1735](https://github.com/jrnl-org/jrnl/pull/1735) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency mkdocs to v1.4.3 [\#1733](https://github.com/jrnl-org/jrnl/pull/1733) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency rich to v13.3.5 [\#1729](https://github.com/jrnl-org/jrnl/pull/1729) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency requests to v2.30.0 [\#1728](https://github.com/jrnl-org/jrnl/pull/1728) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.5.1 [\#1727](https://github.com/jrnl-org/jrnl/pull/1727) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update peter-evans/create-pull-request action to v5 [\#1719](https://github.com/jrnl-org/jrnl/pull/1719) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency flake8-simplify to v0.20.0 [\#1716](https://github.com/jrnl-org/jrnl/pull/1716) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v4.0](https://pypi.org/project/jrnl/v4.0/) (2023-05-20)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0-beta3...v4.0)
|
||||
|
||||
🚨 **BREAKING CHANGES** 🚨
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add message with config location and docs location when installation is complete [\#1695](https://github.com/jrnl-org/jrnl/pull/1695) ([micahellison](https://github.com/micahellison))
|
||||
- Prompt to include colors in config when first running jrnl [\#1687](https://github.com/jrnl-org/jrnl/pull/1687) ([micahellison](https://github.com/micahellison))
|
||||
- Add ability to use template with `--template` [\#1667](https://github.com/jrnl-org/jrnl/pull/1667) ([alichtman](https://github.com/alichtman))
|
||||
- Search for entries with no tags or stars with `-not -starred` and `-not -tagged` [\#1663](https://github.com/jrnl-org/jrnl/pull/1663) ([cjcon90](https://github.com/cjcon90))
|
||||
- Refactor flow for easier access to some files \(avoid things like `jrnl.Journal.Journal` and `jrnl.jrnl` co-existing\) [\#1662](https://github.com/jrnl-org/jrnl/pull/1662) ([wren](https://github.com/wren))
|
||||
- Add more type hints [\#1642](https://github.com/jrnl-org/jrnl/pull/1642) ([outa](https://github.com/outa))
|
||||
- Add `rich` handler to debug logging [\#1627](https://github.com/jrnl-org/jrnl/pull/1627) ([wren](https://github.com/wren))
|
||||
- Rework Encryption to enable future support of other encryption methods [\#1602](https://github.com/jrnl-org/jrnl/pull/1602) ([wren](https://github.com/wren))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Only read text files that look like entries when opening folder journal [\#1697](https://github.com/jrnl-org/jrnl/pull/1697) ([micahellison](https://github.com/micahellison))
|
||||
- Save empty journal on install instead of just creating a zero-length file [\#1690](https://github.com/jrnl-org/jrnl/pull/1690) ([micahellison](https://github.com/micahellison))
|
||||
- Allow combinations of `--change-time`, `--delete`, and `--edit` while correctly counting the number of entries affected [\#1669](https://github.com/jrnl-org/jrnl/pull/1669) ([wren](https://github.com/wren))
|
||||
- Don't save templated journal entries if the received raw text is the same as the template itself [\#1653](https://github.com/jrnl-org/jrnl/pull/1653) ([Briscoooe](https://github.com/Briscoooe))
|
||||
- Add tag to XML file when edited DayOne entry and is searchable afterward [\#1648](https://github.com/jrnl-org/jrnl/pull/1648) ([jonakeys](https://github.com/jonakeys))
|
||||
- Update version key in config file after version changes [\#1646](https://github.com/jrnl-org/jrnl/pull/1646) ([jonakeys](https://github.com/jonakeys))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Update copyright notices for 2023 [\#1660](https://github.com/jrnl-org/jrnl/pull/1660) ([wren](https://github.com/wren))
|
||||
- Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren))
|
||||
- Add `simplify` plugin to linting checks [\#1630](https://github.com/jrnl-org/jrnl/pull/1630) ([wren](https://github.com/wren))
|
||||
- Add type hints [\#1614](https://github.com/jrnl-org/jrnl/pull/1614) ([outa](https://github.com/outa))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Update contributing.md links in documentation [\#1726](https://github.com/jrnl-org/jrnl/pull/1726) ([ahosking](https://github.com/ahosking))
|
||||
- Fix various typos [\#1718](https://github.com/jrnl-org/jrnl/pull/1718) ([hezhizhen](https://github.com/hezhizhen))
|
||||
- Update documentation front page text [\#1698](https://github.com/jrnl-org/jrnl/pull/1698) ([micahellison](https://github.com/micahellison))
|
||||
- Support mkdocs 1.4.2 and fix its missing breadcrumb [\#1691](https://github.com/jrnl-org/jrnl/pull/1691) ([micahellison](https://github.com/micahellison))
|
||||
- Document temporary file extension behavior when using template [\#1686](https://github.com/jrnl-org/jrnl/pull/1686) ([micahellison](https://github.com/micahellison))
|
||||
- Document `-tagged`, `-not -tagged`, and `-not -starred` [\#1684](https://github.com/jrnl-org/jrnl/pull/1684) ([micahellison](https://github.com/micahellison))
|
||||
- Update documentation about privacy and security in VSCode [\#1680](https://github.com/jrnl-org/jrnl/pull/1680) ([giuseppedandrea](https://github.com/giuseppedandrea))
|
||||
- Update documentation on temporary files naming [\#1673](https://github.com/jrnl-org/jrnl/pull/1673) ([giuseppedandrea](https://github.com/giuseppedandrea))
|
||||
- Update docs to include time and title in arguments with `--edit` [\#1657](https://github.com/jrnl-org/jrnl/pull/1657) ([pconrad-fb](https://github.com/pconrad-fb))
|
||||
- Fix markup in "Advanced Usage" doc [\#1655](https://github.com/jrnl-org/jrnl/pull/1655) ([multani](https://github.com/multani))
|
||||
- Remove Windows 7 known issue since Windows 7 is no longer supported [\#1636](https://github.com/jrnl-org/jrnl/pull/1636) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Lock ruamel.yaml version to v0.17.21 until bug is fixed [\#1738](https://github.com/jrnl-org/jrnl/pull/1738) ([wren](https://github.com/wren))
|
||||
- Update dependency black to v23.3.0 [\#1715](https://github.com/jrnl-org/jrnl/pull/1715) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency cryptography to v40.0.2 [\#1723](https://github.com/jrnl-org/jrnl/pull/1723) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency flake8-type-checking to v2.4.0 [\#1714](https://github.com/jrnl-org/jrnl/pull/1714) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency flakeheaven to v3.3.0 [\#1722](https://github.com/jrnl-org/jrnl/pull/1722) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency ipdb to v0.13.13 [\#1703](https://github.com/jrnl-org/jrnl/pull/1703) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency poethepoet to v0.19.0 [\#1709](https://github.com/jrnl-org/jrnl/pull/1709) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest to v7.3.1 [\#1720](https://github.com/jrnl-org/jrnl/pull/1720) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency pytest-xdist to v3.2.1 [\#1705](https://github.com/jrnl-org/jrnl/pull/1705) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency rich to v13.3.4 [\#1713](https://github.com/jrnl-org/jrnl/pull/1713) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tox to v4.4.7 [\#1707](https://github.com/jrnl-org/jrnl/pull/1707) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency tzlocal to v4.3 [\#1708](https://github.com/jrnl-org/jrnl/pull/1708) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3-beta2...v3.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Change default config to use journal key [\#1594](https://github.com/jrnl-org/jrnl/pull/1594) ([micahellison](https://github.com/micahellison))
|
||||
- Add machine readable --list output [\#1592](https://github.com/jrnl-org/jrnl/pull/1592) ([apainintheneck](https://github.com/apainintheneck))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix bug for new `--list --format` options when no default journal is specified [\#1621](https://github.com/jrnl-org/jrnl/pull/1621) ([wren](https://github.com/wren))
|
||||
- Don't create empty file when attempting a YAML export to a non-existing folder [\#1600](https://github.com/jrnl-org/jrnl/pull/1600) ([outa](https://github.com/outa))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Update `.gitignore` [\#1604](https://github.com/jrnl-org/jrnl/pull/1604) ([wren](https://github.com/wren))
|
||||
- Fix Docs Accessibility Testing [\#1588](https://github.com/jrnl-org/jrnl/pull/1588) ([wren](https://github.com/wren))
|
||||
- Update to use renamed flag for `brew bump-formula-pr` [\#1587](https://github.com/jrnl-org/jrnl/pull/1587) ([wren](https://github.com/wren))
|
||||
- Update peter-evans/create-pull-request action to v4 [\#1585](https://github.com/jrnl-org/jrnl/pull/1585) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update actions/setup-python action to v4 [\#1583](https://github.com/jrnl-org/jrnl/pull/1583) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update actions/checkout action to v3 [\#1582](https://github.com/jrnl-org/jrnl/pull/1582) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update actions/cache action to v3 [\#1581](https://github.com/jrnl-org/jrnl/pull/1581) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Replace Dependabot with Renovate [\#1575](https://github.com/jrnl-org/jrnl/pull/1575) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Add documentation about how the editor must be a blocking process [\#1456](https://github.com/jrnl-org/jrnl/issues/1456)
|
||||
- Document that editors must be blocking processes [\#1624](https://github.com/jrnl-org/jrnl/pull/1624) ([micahellison](https://github.com/micahellison))
|
||||
- Remove wrong option in configuration file reference [\#1618](https://github.com/jrnl-org/jrnl/pull/1618) ([DSiekmeier](https://github.com/DSiekmeier))
|
||||
- Update YAML export description in docs [\#1591](https://github.com/jrnl-org/jrnl/pull/1591) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Update dependency jinja2 to v3.1.2 [\#1579](https://github.com/jrnl-org/jrnl/pull/1579) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Update dependency typed.js to v2.0.12 [\#1578](https://github.com/jrnl-org/jrnl/pull/1578) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
- Add hint for how to get vi to go to end-of-file [\#1563](https://github.com/jrnl-org/jrnl/pull/1563) ([pjz](https://github.com/pjz))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Pin dependencies [\#1577](https://github.com/jrnl-org/jrnl/pull/1577) ([renovate[bot]](https://github.com/apps/renovate))
|
||||
|
||||
## [v3.2](https://pypi.org/project/jrnl/v3.2/) (2022-09-03)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.2-beta...v3.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Update issue & PR templates to use forms [\#1559](https://github.com/jrnl-org/jrnl/issues/1559)
|
||||
- Suppress "Entry added" message if using default journal [\#1561](https://github.com/jrnl-org/jrnl/pull/1561) ([micahellison](https://github.com/micahellison))
|
||||
- Add message showing the number of search results [\#1524](https://github.com/jrnl-org/jrnl/pull/1524) ([apainintheneck](https://github.com/apainintheneck))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Quick fix follow up for actionlint [\#1565](https://github.com/jrnl-org/jrnl/pull/1565) ([wren](https://github.com/wren))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Remove note in contributing docs about gh-pages branch that we no longer use [\#1566](https://github.com/jrnl-org/jrnl/pull/1566) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
## [v3.1](https://pypi.org/project/jrnl/v3.1/) (2022-08-21)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.1-beta2...v3.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Update tzlocal to v4.x and remove pytz dependency [\#1528](https://github.com/jrnl-org/jrnl/pull/1528) ([outa](https://github.com/outa))
|
||||
- Add linewrap option 'auto' [\#1507](https://github.com/jrnl-org/jrnl/pull/1507) ([jonakeys](https://github.com/jonakeys))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Update formatting function to better account for indentation [\#1541](https://github.com/jrnl-org/jrnl/pull/1541) ([wren](https://github.com/wren))
|
||||
- Export to file\(s\) when first line/title of an entry is very long [\#1527](https://github.com/jrnl-org/jrnl/pull/1527) ([jonakeys](https://github.com/jonakeys))
|
||||
- Fixed index out of range error in fancy exporter [\#1522](https://github.com/jrnl-org/jrnl/pull/1522) ([apainintheneck](https://github.com/apainintheneck))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Add actionlint to testing pipelines [\#1555](https://github.com/jrnl-org/jrnl/pull/1555) ([wren](https://github.com/wren))
|
||||
- Fix docs pipeline, make docs tests easier to run locally and on different OSes [\#1554](https://github.com/jrnl-org/jrnl/pull/1554) ([wren](https://github.com/wren))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Reformat contributor appreciation on `--help` screen [\#1556](https://github.com/jrnl-org/jrnl/pull/1556) ([xeruf](https://github.com/xeruf))
|
||||
- Clean up copyright notices and version screen [\#1553](https://github.com/jrnl-org/jrnl/pull/1553) ([wren](https://github.com/wren))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump pyproject-flake8 from 0.0.1a4 to 0.0.1a5 [\#1552](https://github.com/jrnl-org/jrnl/pull/1552) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.7.0 to 23.8.2 [\#1551](https://github.com/jrnl-org/jrnl/pull/1551) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump yq from 3.0.2 to 3.1.0 [\#1546](https://github.com/jrnl-org/jrnl/pull/1546) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump poethepoet from 0.15.0 to 0.16.0 [\#1542](https://github.com/jrnl-org/jrnl/pull/1542) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.6.0 to 23.7.0 [\#1539](https://github.com/jrnl-org/jrnl/pull/1539) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump rich from 12.4.4 to 12.5.1 [\#1538](https://github.com/jrnl-org/jrnl/pull/1538) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v3.0](https://pypi.org/project/jrnl/v3.0/) (2022-07-09)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.0-beta2...v3.0)
|
||||
|
||||
🚨 **BREAKING CHANGES** 🚨
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Drop support for Python 3.7 and 3.8 [\#1412](https://github.com/jrnl-org/jrnl/pull/1412) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Show name of journal when creating a password/encrypting [\#1478](https://github.com/jrnl-org/jrnl/pull/1478) ([jonakeys](https://github.com/jonakeys))
|
||||
- Rework how all output and messaging works in jrnl [\#1475](https://github.com/jrnl-org/jrnl/pull/1475) ([wren](https://github.com/wren))
|
||||
- Implement --change-time flag [\#1452](https://github.com/jrnl-org/jrnl/pull/1452) ([richardjs](https://github.com/richardjs))
|
||||
- Reformat additional messages and finish centralizing exception handling [\#1424](https://github.com/jrnl-org/jrnl/pull/1424) ([wren](https://github.com/wren))
|
||||
- Reformat messages and add new centralized exception handling [\#1417](https://github.com/jrnl-org/jrnl/pull/1417) ([wren](https://github.com/wren))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Display message when no edits take place [\#1510](https://github.com/jrnl-org/jrnl/pull/1510) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Fixed error related to display\_format in config file for some values [\#1495](https://github.com/jrnl-org/jrnl/pull/1495) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Create folder if config ends with \(back\)slash [\#1492](https://github.com/jrnl-org/jrnl/pull/1492) ([jonakeys](https://github.com/jonakeys))
|
||||
- `-not` search parameter no longer opens editor [\#1490](https://github.com/jrnl-org/jrnl/pull/1490) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Fix TypeError when using debug flag [\#1484](https://github.com/jrnl-org/jrnl/pull/1484) ([jonakeys](https://github.com/jonakeys))
|
||||
- Prompt for password change when using 'jrnl --encrypt' on already encrypted journal [\#1477](https://github.com/jrnl-org/jrnl/pull/1477) ([jonakeys](https://github.com/jonakeys))
|
||||
- Always expand all paths \(journals, templates, etc\) [\#1468](https://github.com/jrnl-org/jrnl/pull/1468) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- The `-not` option with no arguments now outputs error instead of stack trace [\#1466](https://github.com/jrnl-org/jrnl/pull/1466) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Give a proper message when trying to use an empty config file [\#1461](https://github.com/jrnl-org/jrnl/pull/1461) ([jonakeys](https://github.com/jonakeys))
|
||||
- Display "No entry to save, because no text was received" after empty entry on cmdline [\#1459](https://github.com/jrnl-org/jrnl/pull/1459) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Yaml export errors now don't show stack trace [\#1449](https://github.com/jrnl-org/jrnl/pull/1449) ([apainintheneck](https://github.com/apainintheneck))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Pin `pytest-bdd` to \<6.0 to temporarily avoid breaking changes [\#1536](https://github.com/jrnl-org/jrnl/pull/1536) ([wren](https://github.com/wren))
|
||||
- Reduce difference between local and CI environments [\#1518](https://github.com/jrnl-org/jrnl/pull/1518) ([wren](https://github.com/wren))
|
||||
- Add bdd tests for jrnl installation [\#1513](https://github.com/jrnl-org/jrnl/pull/1513) ([apainintheneck](https://github.com/apainintheneck))
|
||||
- Stop hardcoding bot info in changelog pipeline [\#1506](https://github.com/jrnl-org/jrnl/pull/1506) ([wren](https://github.com/wren))
|
||||
- Fix Poetry caching for accessibility tests [\#1505](https://github.com/jrnl-org/jrnl/pull/1505) ([wren](https://github.com/wren))
|
||||
- Implement Tox for testing [\#1504](https://github.com/jrnl-org/jrnl/pull/1504) ([wren](https://github.com/wren))
|
||||
- Replace `make` with python alternative \(`poe`\) [\#1503](https://github.com/jrnl-org/jrnl/pull/1503) ([wren](https://github.com/wren))
|
||||
- Update copyright year [\#1502](https://github.com/jrnl-org/jrnl/pull/1502) ([wren](https://github.com/wren))
|
||||
- Add Python 3.11 to PR tests [\#1500](https://github.com/jrnl-org/jrnl/pull/1500) ([micahellison](https://github.com/micahellison))
|
||||
- Pin jinja2 in docs requirements to keep readthedocs builds from failing [\#1439](https://github.com/jrnl-org/jrnl/pull/1439) ([micahellison](https://github.com/micahellison))
|
||||
- Tidy up git ignore [\#1414](https://github.com/jrnl-org/jrnl/pull/1414) ([nelnog](https://github.com/nelnog))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Document --change-time in web-based docs' command line reference [\#1471](https://github.com/jrnl-org/jrnl/pull/1471) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump cryptography from 37.0.2 to 37.0.3 [\#1516](https://github.com/jrnl-org/jrnl/pull/1516) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump poethepoet from 0.13.1 to 0.14.0 [\#1514](https://github.com/jrnl-org/jrnl/pull/1514) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.5.1 to 23.6.0 [\#1499](https://github.com/jrnl-org/jrnl/pull/1499) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pyxdg from 0.27 to 0.28 [\#1497](https://github.com/jrnl-org/jrnl/pull/1497) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.5.0 to 23.5.1 [\#1487](https://github.com/jrnl-org/jrnl/pull/1487) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump cryptography from 37.0.1 to 37.0.2 [\#1467](https://github.com/jrnl-org/jrnl/pull/1467) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump cryptography from 36.0.2 to 37.0.1 [\#1462](https://github.com/jrnl-org/jrnl/pull/1462) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytest from 7.1.1 to 7.1.2 [\#1458](https://github.com/jrnl-org/jrnl/pull/1458) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pyproject-flake8 from 0.0.1a3 to 0.0.1a4 [\#1447](https://github.com/jrnl-org/jrnl/pull/1447) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump black from 22.1.0 to 22.3.0 [\#1442](https://github.com/jrnl-org/jrnl/pull/1442) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump mkdocs from 1.2.3 to 1.3.0 [\#1441](https://github.com/jrnl-org/jrnl/pull/1441) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pyproject-flake8 from 0.0.1a2 to 0.0.1a3 [\#1440](https://github.com/jrnl-org/jrnl/pull/1440) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytz from 2021.3 to 2022.1 [\#1438](https://github.com/jrnl-org/jrnl/pull/1438) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytest from 7.0.1 to 7.1.1 [\#1430](https://github.com/jrnl-org/jrnl/pull/1430) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump cryptography from 36.0.1 to 36.0.2 [\#1427](https://github.com/jrnl-org/jrnl/pull/1427) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump yq from 2.13.0 to 2.14.0 [\#1418](https://github.com/jrnl-org/jrnl/pull/1418) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Replace PyYAML with ruamel.yaml [\#1416](https://github.com/jrnl-org/jrnl/pull/1416) ([micahellison](https://github.com/micahellison))
|
||||
- Bump pytest from 6.2.5 to 7.0.0 [\#1407](https://github.com/jrnl-org/jrnl/pull/1407) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.8.4](https://pypi.org/project/jrnl/v2.8.4/) (2022-02-12)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.8.4-beta2...v2.8.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add hash as a default tag symbol for new jrnl config file [\#1398](https://github.com/jrnl-org/jrnl/pull/1398) ([micahellison](https://github.com/micahellison))
|
||||
- Add --config-file argument to use alternate config file at runtime [\#1290](https://github.com/jrnl-org/jrnl/pull/1290) ([samuelgregorovic](https://github.com/samuelgregorovic))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Certs broken on website [\#1408](https://github.com/jrnl-org/jrnl/issues/1408)
|
||||
- Add added option to \_print\_edited\_summary [\#1366](https://github.com/jrnl-org/jrnl/pull/1366) ([piero-vic](https://github.com/piero-vic))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Improve handling of mocking logic in pytest [\#1382](https://github.com/jrnl-org/jrnl/pull/1382) ([wren](https://github.com/wren))
|
||||
- Use full Python version for GitHub Actions cache key [\#1373](https://github.com/jrnl-org/jrnl/pull/1373) ([micahellison](https://github.com/micahellison))
|
||||
- Use Python 3.10 stable in CI [\#1362](https://github.com/jrnl-org/jrnl/pull/1362) ([micahellison](https://github.com/micahellison))
|
||||
- Switch from poetry to poetry-core [\#1359](https://github.com/jrnl-org/jrnl/pull/1359) ([fabaff](https://github.com/fabaff))
|
||||
- Add more steps to `pytest`, fully remove `behave` [\#1347](https://github.com/jrnl-org/jrnl/pull/1347) ([wren](https://github.com/wren))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Fix styling on documentation sidebar [\#1395](https://github.com/jrnl-org/jrnl/pull/1395) ([wren](https://github.com/wren))
|
||||
- Added Recipe for visualizing Markdown in the CLI [\#1354](https://github.com/jrnl-org/jrnl/pull/1354) ([viegasfh](https://github.com/viegasfh))
|
||||
- Fix recipe 'Launch a terminal for rapid logging' [\#1351](https://github.com/jrnl-org/jrnl/pull/1351) ([zapateo](https://github.com/zapateo))
|
||||
- Fix readme splash and add changelog link to readme [\#1339](https://github.com/jrnl-org/jrnl/pull/1339) ([micahellison](https://github.com/micahellison))
|
||||
- Add reference documentation to docs site and separate out "Tips and Tricks" and "External Editors" from "Recipes" [\#1332](https://github.com/jrnl-org/jrnl/pull/1332) ([micahellison](https://github.com/micahellison))
|
||||
- Document journal types [\#1331](https://github.com/jrnl-org/jrnl/pull/1331) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump asteval from 0.9.25 to 0.9.26 [\#1400](https://github.com/jrnl-org/jrnl/pull/1400) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump black from 21.7b0 to 22.1.0 [\#1404](https://github.com/jrnl-org/jrnl/pull/1404) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump cryptography from 3.4.8 to 36.0.1 [\#1389](https://github.com/jrnl-org/jrnl/pull/1389) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump ipython from 7.28.0 to 7.31.1 [\#1401](https://github.com/jrnl-org/jrnl/pull/1401) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.1.0 to 23.5.0 [\#1392](https://github.com/jrnl-org/jrnl/pull/1392) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump mkdocs from 1.2.2 to 1.2.3 [\#1355](https://github.com/jrnl-org/jrnl/pull/1355) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytest from 6.2.4 to 6.2.5 [\#1334](https://github.com/jrnl-org/jrnl/pull/1334) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytest-bdd from 4.1.0 to 5.0.0 [\#1368](https://github.com/jrnl-org/jrnl/pull/1368) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytz from 2021.1 to 2021.3 [\#1348](https://github.com/jrnl-org/jrnl/pull/1348) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump yq from 2.12.2 to 2.13.0 [\#1385](https://github.com/jrnl-org/jrnl/pull/1385) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.8.3](https://pypi.org/project/jrnl/v2.8.3/) (2021-09-06)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.8.3-beta...v2.8.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix deletion of entries on folder journal through `--delete` flag [\#1328](https://github.com/jrnl-org/jrnl/pull/1328) ([micahellison](https://github.com/micahellison))
|
||||
- Warn when DayOne/directory journals have encrypt: true in config [\#1325](https://github.com/jrnl-org/jrnl/pull/1325) ([micahellison](https://github.com/micahellison))
|
||||
- Fix failure to import into directory journal [\#1314](https://github.com/jrnl-org/jrnl/pull/1314) ([micahellison](https://github.com/micahellison))
|
||||
- Allow emoji in config file in Windows by always opening it as unicode [\#1313](https://github.com/jrnl-org/jrnl/pull/1313) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Set bash as default shell [\#1324](https://github.com/jrnl-org/jrnl/pull/1324) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump cryptography from 3.4.7 to 3.4.8 [\#1329](https://github.com/jrnl-org/jrnl/pull/1329) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.0.1 to 23.1.0 [\#1318](https://github.com/jrnl-org/jrnl/pull/1318) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.8.2](https://pypi.org/project/jrnl/v2.8.2/) (2021-07-31)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.8.2-beta...v2.8.2)
|
||||
|
||||
**Build:**
|
||||
|
||||
- Add CI tests for latest dev Python build [\#1273](https://github.com/jrnl-org/jrnl/issues/1273)
|
||||
- Fix lock file from stable Poetry version [\#1298](https://github.com/jrnl-org/jrnl/pull/1298) ([wren](https://github.com/wren))
|
||||
- Change all YAML FullLoader calls to SafeLoader [\#1285](https://github.com/jrnl-org/jrnl/pull/1285) ([micahellison](https://github.com/micahellison))
|
||||
- Remove useless shebangs and executable permissions [\#1283](https://github.com/jrnl-org/jrnl/pull/1283) ([musicinmybrain](https://github.com/musicinmybrain))
|
||||
- Add Python 3.10 support [\#1271](https://github.com/jrnl-org/jrnl/pull/1271) ([micahellison](https://github.com/micahellison))
|
||||
- Ensure that line endings in all py files are Linux style instead of Windows [\#1250](https://github.com/jrnl-org/jrnl/pull/1250) ([micahellison](https://github.com/micahellison))
|
||||
- Remove `--version` from brew release workflow [\#1233](https://github.com/jrnl-org/jrnl/pull/1233) ([wren](https://github.com/wren))
|
||||
- Move test suite to Pytest \(replace Behave\) [\#1193](https://github.com/jrnl-org/jrnl/pull/1193) ([wren](https://github.com/wren))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Add documentation about saved passwords in Windows [\#1301](https://github.com/jrnl-org/jrnl/pull/1301) ([micahellison](https://github.com/micahellison))
|
||||
- Add security.md [\#1299](https://github.com/jrnl-org/jrnl/pull/1299) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump mkdocs from 1.2.1 to 1.2.2 [\#1307](https://github.com/jrnl-org/jrnl/pull/1307) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump python-dateutil from 2.8.1 to 2.8.2 [\#1302](https://github.com/jrnl-org/jrnl/pull/1302) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump black from 21.5b1 to 21.5b2 [\#1254](https://github.com/jrnl-org/jrnl/pull/1254) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump black from 21.5b0 to 21.5b1 [\#1244](https://github.com/jrnl-org/jrnl/pull/1244) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump black from 20.8b1 to 21.5b0 [\#1241](https://github.com/jrnl-org/jrnl/pull/1241) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pytest from 6.2.3 to 6.2.4 [\#1240](https://github.com/jrnl-org/jrnl/pull/1240) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.8.1](https://pypi.org/project/jrnl/v2.8.1/) (2021-04-24)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.8.1-beta...v2.8.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- More graceful handling of low linewrap values [\#1219](https://github.com/jrnl-org/jrnl/pull/1219) ([sriniv27](https://github.com/sriniv27))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Update absolute URLs to preview images in metatags [\#1229](https://github.com/jrnl-org/jrnl/pull/1229) ([maebert](https://github.com/maebert))
|
||||
- Docs: Add emacs as external editor to recipes [\#1220](https://github.com/jrnl-org/jrnl/pull/1220) ([mandarvaze](https://github.com/mandarvaze))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump pytest from 6.2.2 to 6.2.3 [\#1228](https://github.com/jrnl-org/jrnl/pull/1228) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump cryptography from 3.4.6 to 3.4.7 [\#1223](https://github.com/jrnl-org/jrnl/pull/1223) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 23.0.0 to 23.0.1 [\#1222](https://github.com/jrnl-org/jrnl/pull/1222) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump pyflakes from 2.3.0 to 2.3.1 [\#1221](https://github.com/jrnl-org/jrnl/pull/1221) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.8](https://pypi.org/project/jrnl/v2.8/) (2021-03-27)
|
||||
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.8-beta...v2.8)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add `--config-override` feature [\#1169](https://github.com/jrnl-org/jrnl/pull/1169) ([sriniv27](https://github.com/sriniv27))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix bug that prevented --format pretty and --format short from working [\#1177](https://github.com/jrnl-org/jrnl/pull/1177) ([sriniv27](https://github.com/sriniv27))
|
||||
|
||||
**Build:**
|
||||
|
||||
- Fix broken brew release process [\#1211](https://github.com/jrnl-org/jrnl/pull/1211) ([micahellison](https://github.com/micahellison))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump pyflakes from 2.2.0 to 2.3.0 [\#1215](https://github.com/jrnl-org/jrnl/pull/1215) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 22.3.0 to 23.0.0 [\#1213](https://github.com/jrnl-org/jrnl/pull/1213) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump keyring from 22.0.1 to 22.3.0 [\#1210](https://github.com/jrnl-org/jrnl/pull/1210) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Bump asteval from 0.9.22 to 0.9.23 [\#1209](https://github.com/jrnl-org/jrnl/pull/1209) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v2.7.1](https://pypi.org/project/jrnl/v2.7.1/) (2021-02-27)
|
||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.7...v2.7.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Make journal selection behavior more consistent when there's a colon with no date [\#1164](https://github.com/jrnl-org/jrnl/pull/1164) ([wren](https://github.com/wren))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Update documentation about journal-level config values [\#1196](https://github.com/jrnl-org/jrnl/issues/1196)
|
||||
- update per-journal config documentation [\#1199](https://github.com/jrnl-org/jrnl/pull/1199) ([sriniv27](https://github.com/sriniv27))
|
||||
|
||||
**Packaging:**
|
||||
|
||||
- Bump cryptography from 3.4.4 to 3.4.6 [\#1195](https://github.com/jrnl-org/jrnl/pull/1195) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
|
|
@ -59,7 +59,7 @@ representative at an online or offline event.
|
|||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by [emailing the maintainers](mailto:jrnl-sh@googlegroups.com).
|
||||
reported by [emailing the maintainers](mailto:maintainers@jrnl.sh).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
|
130
CONTRIBUTING.md
130
CONTRIBUTING.md
|
@ -1,126 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Contributing
|
||||
|
||||
We welcome contributions to jrnl, whether it's through reporting bugs, improving the documentation, testing releases, engaging in discussion on features and bugs, or writing code.
|
||||
|
||||
## Table of Contents
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Editing Documentation](#editing-documentation)
|
||||
* [Testing](#testing)
|
||||
* [Submitting feature requests and ideas](#submitting-feature-requests-and-ideas)
|
||||
* [Developing](#developing)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Before starting, please read the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Please report bugs by [opening a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describing it as well as possible. Many bugs are specific to a particular operating system and Python version, so please include that information!
|
||||
|
||||
## Editing Documentation
|
||||
|
||||
If you find a typo or a mistake in the docs, please fix it right away and send a pull request.
|
||||
|
||||
To edit the documentation, edit the `docs/*.md` files on the **develop** branch. You can see the result if you run `make html` (or `poetry run mkdocs serve` if on Windows) inside the project's root directory, then navigating your browser to [localhost:8000](http://localhost:8000).
|
||||
|
||||
The `gh-pages` branch is automatically maintained and generated after your changes are merged. You should never have to edit that branch.
|
||||
|
||||
### Recipes and external editors
|
||||
|
||||
If you'd like to share a jrnl command line trick that you find useful, or advice on how to integrate a particular external editor, you may find it worthwhile to add it to the ["Recipes" section](docs/recipes.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Much of the work of maintaining jrnl involves testing rather than coding.
|
||||
|
||||
The nature of jrnl means we deal with extremely sensitive data, and can't risk data loss. While jrnl does have a comprehensive automated testing suite, user testing is crucial to mitigating this risk.
|
||||
|
||||
### Prereleases
|
||||
|
||||
[Prereleases are deployed through PyPi much like normal releases](https://pypi.org/project/jrnl/#history). You can use [pipx](https://pypi.org/project/pipx/) to fetch them and test them. See the [changelog](CHANGELOG.md) for information on what has changed with each release.
|
||||
|
||||
### Pull requests
|
||||
|
||||
If you are comfortable enough with git, feel free to fetch particular [pull requests](https://github.com/jrnl-org/jrnl/pulls), test them yourself, and report back your findings. Bonus points if you can add a screencast of how the new feature works.
|
||||
|
||||
### Confirm bug reports
|
||||
|
||||
There are always [open bugs among our GitHub issues](https://github.com/jrnl-org/jrnl/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and many are specific to a particular OS, Python version, or jrnl version. A simple comment like "Confirmed on jrnl v2.2, MacOS 10.15, Python 3.8.1" would be extremely helpful in tracking down bugs.
|
||||
|
||||
### Automate tests
|
||||
|
||||
See the develop section below for information on how to contribute new automated tests.
|
||||
|
||||
## Submitting feature requests and ideas
|
||||
|
||||
If you have a feature request or idea for jrnl, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe the goal of the feature, and any relevant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
|
||||
|
||||
When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means:
|
||||
|
||||
* be _slim_
|
||||
* have a simple interface
|
||||
* avoid duplicating functionality
|
||||
|
||||
## Developing
|
||||
|
||||
### Getting your environment set up
|
||||
|
||||
You will need to install [poetry](https://poetry.eustace.io/) to develop jrnl. It will take care of all of the project's other dependencies.
|
||||
|
||||
### Understanding the branches
|
||||
|
||||
jrnl uses two primary branches:
|
||||
|
||||
* `develop` - for ongoing development
|
||||
* `release` - for releases
|
||||
|
||||
In general, pull requests should be made on the `develop` branch.
|
||||
|
||||
### Common development commands
|
||||
|
||||
You can find an inventory of commands in the `makefile`. \*nix users can run the commands by typing `make` followed by the name of the command; however, Windows users will need to type out the commands directly, or install a third-party make tool such as [GNU Make for Windows](http://gnuwin32.sourceforge.net/packages/make.htm).
|
||||
|
||||
A typical development workflow includes:
|
||||
|
||||
* Installing dependencies: `poetry install`
|
||||
* Running tests: `make test`
|
||||
* Running the source in a virtual environment:
|
||||
* `poetry shell`
|
||||
* `jrnl` (with or without arguments as necessary)
|
||||
* Linting the code to standardize its style: `make lint`
|
||||
|
||||
### Updating automated tests
|
||||
|
||||
When resolving bugs or adding new functionality, please add tests to prevent that functionality from breaking in the future. If you notice any functionality that isn't covered in the tests, feel free to submit a test-only pull request as well.
|
||||
|
||||
For integration testing, jrnl uses [behave](https://behave.readthedocs.io/) tests, which are all in the `features` folder.
|
||||
|
||||
Many tests can be created by only editing `feature` files with the same format as other tests. For more complicated functionality, you may need to implement steps in `features/steps` which are then executed by your tests in the `feature` files.
|
||||
|
||||
Starting in 2020, jrnl is also using [pytest](https://docs.pytest.org/) for unit tests. These tests are in the `tests` folder.
|
||||
|
||||
### Submitting pull requests
|
||||
|
||||
When you're ready, feel free to submit a pull request (PR). The jrnl maintainers generally review the pull requests every two weeks, but the continuous integration pipeline will run on automated tests on it within a matter of minutes and will report back any issues it has found with your code across a variety of environments.
|
||||
|
||||
The pull request template contains a checklist full of housekeeping items. Please fill them out as necessary when you submit.
|
||||
|
||||
If a pull request contains failing tests, it probably will not be reviewed, and it definitely will not be approved. However, if you need help resolving a failing test, please mention that in your PR.
|
||||
|
||||
### Finding things to work on
|
||||
|
||||
You can search the [jrnl GitHub issues](https://github.com/jrnl-org/jrnl/issues) by [label](https://github.com/jrnl-org/jrnl/labels) for things to work on. Here are some labels worth searching:
|
||||
|
||||
* [critical](https://github.com/jrnl-org/jrnl/labels/critical)
|
||||
* [help wanted](https://github.com/jrnl-org/jrnl/labels/help%20wanted)
|
||||
* [bug](https://github.com/jrnl-org/jrnl/labels/bug)
|
||||
* [enhancement](https://github.com/jrnl-org/jrnl/labels/enhancement)
|
||||
|
||||
### A note for new programmers and programmers new to python
|
||||
|
||||
Although jrnl has grown quite a bit since its inception, the overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
|
||||
|
||||
If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together!
|
||||
See "[Contributing](docs/contributing.md)" in the `docs` directory.
|
||||
|
|
32
Makefile
32
Makefile
|
@ -1,32 +0,0 @@
|
|||
# A Makefile for commands I run frequently:
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
rm -rf _static
|
||||
rm -rf jrnl.egg-info
|
||||
rm -rf _build
|
||||
rm -rf _sources
|
||||
rm -rf _static
|
||||
rm -rf site/
|
||||
rm -f *.html
|
||||
|
||||
html:
|
||||
poetry run mkdocs serve
|
||||
|
||||
format: ## Format files to match style
|
||||
poetry run black .
|
||||
|
||||
lint: ## Check style with various tools
|
||||
poetry check
|
||||
poetry run pyflakes .
|
||||
poetry run black --check --diff .
|
||||
|
||||
test: lint ## Run unit tests and behave tests
|
||||
poetry run pytest
|
||||
poetry run behave --no-skipped --format progress2
|
||||
|
||||
build:
|
||||
poetry build
|
||||
|
||||
install: clean ## install the package to the active Python's site-packages
|
||||
poetry install
|
12
README.md
12
README.md
|
@ -1,6 +1,11 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jrnl.sh">
|
||||
<img align="center" src="https://github.com/jrnl-org/jrnl/blob/develop/docs_theme/assets/readme-header.png"/>
|
||||
<img align="center" src="https://raw.githubusercontent.com/jrnl-org/jrnl/develop/docs_theme/assets/readme-header.png"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
@ -10,10 +15,11 @@ jrnl
|
|||
[](https://pypi.python.org/pypi/jrnl/)
|
||||
[](https://formulae.brew.sh/formula/jrnl)
|
||||
[](https://gitter.im/jrnl-org/jrnl)
|
||||
[](https://github.com/jrnl-org/jrnl/blob/develop/CHANGELOG.md)
|
||||
====
|
||||
|
||||
_To get help, [submit an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) on
|
||||
Github._
|
||||
GitHub._
|
||||
|
||||
`jrnl` is a simple journal application for the command line.
|
||||
|
||||
|
@ -64,7 +70,7 @@ src="https://opencollective.com/jrnl/contributors.svg?width=890&button=false"
|
|||
/></a>
|
||||
|
||||
If you'd also like to help make `jrnl` better, please see our [contributing
|
||||
documentation](CONTRIBUTING.md).
|
||||
documentation](docs/contributing.md).
|
||||
|
||||
### Financial Backers
|
||||
|
||||
|
|
7
SECURITY.md
Normal file
7
SECURITY.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Security
|
||||
|
||||
If you've discovered a potential security issue in jrnl, please contact the maintainers at [maintainers@jrnl.sh](mailto:maintainers@jrnl.sh).
|
||||
|
||||
You can also feel free to [open an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) (but please don't disclose the vulnerability) in case the email goes to spam.
|
||||
|
||||
You can find [known privacy and security issues in our documentation](https://jrnl.sh/en/stable/privacy-and-security/).
|
148
docs/advanced.md
148
docs/advanced.md
|
@ -1,76 +1,27 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Advanced Usage
|
||||
|
||||
## Configuration File
|
||||
|
||||
You can configure the way jrnl behaves in a configuration file. By
|
||||
default, this is `~/.config/jrnl/jrnl.yaml`. If you have the `XDG_CONFIG_HOME`
|
||||
variable set, the configuration file will be saved as
|
||||
`$XDG_CONFIG_HOME/jrnl/jrnl.yaml`.
|
||||
`jrnl` has a wide variety of options that can be customized through the config file,
|
||||
including templates, formats, multiple journals, and more. See
|
||||
the [configuration file reference](./reference-config-file.md) for details
|
||||
or read on for some common use cases.
|
||||
|
||||
!!! note
|
||||
On Windows, the configuration file is typically found at `%USERPROFILE%\.config\jrnl\jrnl.yaml`.
|
||||
|
||||
The configuration file is a YAML file with the following options
|
||||
and can be edited with a plain text editor.
|
||||
|
||||
!!! note
|
||||
Backup your journal and config file before editing. Changes to the config file
|
||||
can have destructive effects on your journal!
|
||||
|
||||
- `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 `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.
|
||||
- `colors`
|
||||
dictionary that controls the colors used to display journal entries. It has four subkeys, which are: `body`, `date`, `tags`, and `title`. Current valid values are: `BLACK`, `RED`, `GREEN`, `YELLOW`, `BLUE`, `MAGENTA`, `CYAN`, `WHITE`, and `NONE`. `colorama.Fore` is used for colorization, and you can find the [docs here](https://github.com/tartley/colorama#colored-output). To disable colored output, set the value to `NONE`. If you set the value of any color subkey to an invalid color, no color will be used.
|
||||
- `display_format`
|
||||
specifies formatter to use, formatters available are:
|
||||
`boxed`, `fancy`, `json`, `markdown`, `md`, `tags`, `text`, `txt`, `xml`, or `yaml`.
|
||||
|
||||
!!! 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
|
||||
|
||||
> `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.
|
||||
|
||||
## Multiple journal files
|
||||
### Multiple journal files
|
||||
|
||||
You can configure `jrnl`to use with multiple journals (eg.
|
||||
`private` and `work`) by defining more journals in your `jrnl.yaml`,
|
||||
`private` and `work`) by defining more journals in your [config file](./reference-config-file.md),
|
||||
for example:
|
||||
|
||||
``` yaml
|
||||
journals:
|
||||
default: ~\journal.txt
|
||||
work: ~\work.txt
|
||||
default: ~/journal.txt
|
||||
work: ~/work.txt
|
||||
```
|
||||
|
||||
The `default` journal gets created the first time you start `jrnl`
|
||||
|
@ -91,31 +42,76 @@ If your `jrnl.yaml` looks like this:
|
|||
``` yaml
|
||||
encrypt: false
|
||||
journals:
|
||||
default: ~/journal.txt
|
||||
work:
|
||||
default: ~/journal.txt
|
||||
work:
|
||||
journal: ~/work.txt
|
||||
encrypt: true
|
||||
food: ~/my_recipes.txt
|
||||
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
|
||||
`work` journal will!
|
||||
|
||||
You can override all options that are present at
|
||||
the top level of `jrnl.yaml`, just make sure that at the very least
|
||||
you specify a `journal: ...` key that points to the journal file of
|
||||
that journal.
|
||||
|
||||
Consider the following example configuration
|
||||
|
||||
``` yaml
|
||||
editor: vi -c startinsert
|
||||
journals:
|
||||
default: ~/journal.txt
|
||||
work:
|
||||
journal: ~/work.txt
|
||||
encrypt: true
|
||||
display_format: json
|
||||
editor: code -rw
|
||||
food:
|
||||
display_format: markdown
|
||||
journal: ~/recipes.txt
|
||||
```
|
||||
|
||||
The `work` journal is encrypted, prints to `json` by default, and is edited using an existing window of VSCode. Similarly, the `food` journal prints to markdown by default, but uses all the other defaults.
|
||||
|
||||
### Modifying Configurations from the Command line
|
||||
|
||||
You can override a configuration field for the current instance of `jrnl` using `--config-override CONFIG_KEY CONFIG_VALUE` where `CONFIG_KEY` is a valid configuration field, specified in dot notation and `CONFIG_VALUE` is the (valid) desired override value. The dot notation can be used to change config keys within other keys, such as `colors.title` for the `title` key within the `colors` key.
|
||||
|
||||
You can specify multiple overrides as multiple calls to `--config-override`.
|
||||
|
||||
!!! 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.
|
||||
These overrides allow you to modify ***any*** field of your jrnl configuration. We trust that you know what you are doing.
|
||||
|
||||
## Known Issues
|
||||
#### Examples:
|
||||
|
||||
### Unicode on Windows
|
||||
``` sh
|
||||
# Create an entry using the `stdin` prompt, for rapid logging
|
||||
jrnl --config-override editor ""
|
||||
|
||||
The Windows shell prior to Windows 7 has issues with unicode encoding.
|
||||
To use non-ascii characters, first tweak Python to recognize the encoding by adding `'cp65001': 'utf_8'`, to `Lib/encoding/aliases.py`. Then, change the codepage with `chcp 1252` before using `jrnl`.
|
||||
# Populate a project's log
|
||||
jrnl --config-override journals.todo "$(git rev-parse --show-toplevel)/todo.txt" todo find my towel
|
||||
|
||||
(Related issue: [#486](https://github.com/jrnl-org/jrnl/issues/486))
|
||||
# Pass multiple overrides
|
||||
jrnl --config-override display_format fancy --config-override linewrap 20 \
|
||||
--config-override colors.title green
|
||||
```
|
||||
|
||||
### Using an alternate config
|
||||
|
||||
You can specify an alternate configuration file for the current instance of `jrnl` using `--config-file CONFIG_FILE_PATH` where
|
||||
`CONFIG_FILE_PATH` is a path to an alternate `jrnl` configuration file.
|
||||
|
||||
#### Examples:
|
||||
|
||||
``` sh
|
||||
# Use personalised configuration file for personal journal entries
|
||||
jrnl --config-file ~/foo/jrnl/personal-config.yaml
|
||||
|
||||
# Use alternate configuration file for work-related entries
|
||||
jrnl --config-file ~/foo/jrnl/work-config.yaml
|
||||
|
||||
# Use default configuration file (created on first run)
|
||||
jrnl
|
||||
```
|
||||
|
|
132
docs/contributing.md
Normal file
132
docs/contributing.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Contributing to jrnl
|
||||
|
||||
We welcome contributions to jrnl, whether it's through reporting bugs, improving the documentation, testing releases, engaging in discussion on features and bugs, or writing code.
|
||||
|
||||
## Table of Contents
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Editing Documentation](#editing-documentation)
|
||||
* [Testing](#testing)
|
||||
* [Submitting feature requests and ideas](#submitting-feature-requests-and-ideas)
|
||||
* [Developing jrnl](#developing)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Before starting, please read the [Code of Conduct](https://github.com/jrnl-org/jrnl/blob/develop/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Please report bugs by [opening a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describing it as well as possible. Many bugs are specific to a particular operating system and Python version, so please include that information!
|
||||
|
||||
## Editing Documentation
|
||||
|
||||
If you find a typo or a mistake in the docs, please fix it right away and send a pull request. If you're unsure what to change but still see a problem, you can [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) with the "Documentation change" type.
|
||||
|
||||
To edit the documentation, edit the `docs/*.md` files on the **develop** branch. You can see the result by running `poe docs-run` inside the project's root directory, then navigating your browser to [localhost:8000](http://localhost:8000).
|
||||
|
||||
### External editors and tips and tricks
|
||||
|
||||
If you'd like to share a jrnl command line trick that you find useful, you may find it worthwhile to add it to the ["Tips and Tricks" section](tips-and-tricks.md). For advice on how to integrate a particular external editor, you can add to the ["External Editors" section](external-editors.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Much of the work of maintaining jrnl involves testing rather than coding.
|
||||
|
||||
The nature of jrnl means we deal with extremely sensitive data, and can't risk data loss. While jrnl does have a comprehensive automated testing suite, user testing is crucial to mitigating this risk.
|
||||
|
||||
### Prereleases
|
||||
|
||||
[Prereleases are deployed through PyPi much like normal releases](https://pypi.org/project/jrnl/#history). You can use [pipx](https://pypi.org/project/pipx/) to fetch them and test them. See the [changelog](https://github.com/jrnl-org/jrnl/blob/develop/CHANGELOG.md) for information on what has changed with each release.
|
||||
|
||||
### Pull requests
|
||||
|
||||
If you are comfortable enough with git, feel free to fetch particular [pull requests](https://github.com/jrnl-org/jrnl/pulls), test them yourself, and report back your findings. Bonus points if you can add a screencast of how the new feature works.
|
||||
|
||||
### Confirm bug reports
|
||||
|
||||
There are always [open bugs among our GitHub issues](https://github.com/jrnl-org/jrnl/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and many are specific to a particular OS, Python version, or jrnl version. A simple comment like "Confirmed on jrnl v2.2, MacOS 10.15, Python 3.8.1" would be extremely helpful in tracking down bugs.
|
||||
|
||||
### Automate tests
|
||||
|
||||
See the develop section below for information on how to contribute new automated tests.
|
||||
|
||||
## Submitting feature requests and ideas
|
||||
|
||||
If you have a feature request or idea for jrnl, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe the goal of the feature, and any relevant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
|
||||
|
||||
When discussing new features, please keep in mind our design goals. jrnl strives to
|
||||
[do one thing well](https://en.wikipedia.org/wiki/Unix_philosophy). To us, that means:
|
||||
|
||||
* being _nimble_
|
||||
* having a simple interface
|
||||
* avoiding duplicating functionality
|
||||
|
||||
## Developing
|
||||
|
||||
### Getting your environment set up
|
||||
|
||||
You will need to install [poetry](https://python-poetry.org/) to develop jrnl. It will take care of all of the project's other dependencies.
|
||||
|
||||
### Understanding the branches
|
||||
|
||||
jrnl uses two primary branches:
|
||||
|
||||
* `develop` - for ongoing development
|
||||
* `release` - for releases
|
||||
|
||||
In general, pull requests should be made on the `develop` branch.
|
||||
|
||||
### Common development commands
|
||||
|
||||
You can find an inventory of commands in the `pyproject.toml`. Users can run the commands by typing `poe` followed by the name of the command ([Poe the Poet](https://github.com/nat-n/poethepoet) can be installed on its own, or as part of `poetry install`).
|
||||
|
||||
A typical development workflow includes:
|
||||
|
||||
* Installing dependencies:
|
||||
* `poetry install`
|
||||
* Activate virtual environment:
|
||||
* `poetry shell`
|
||||
* Running the source in a virtual environment:
|
||||
* `jrnl` (with or without arguments as necessary)
|
||||
* Running tests:
|
||||
* `poe test`
|
||||
* Formatting the code to standardize its style:
|
||||
* `poe format`
|
||||
|
||||
### Updating automated tests
|
||||
|
||||
When resolving bugs or adding new functionality, please add tests to prevent that functionality from breaking in the future. If you notice any functionality that isn't covered in the tests, feel free to submit a test-only pull request as well.
|
||||
|
||||
For testing, jrnl uses [pytest](https://docs.pytest.org) for unit tests, and [pytest-bdd](https://pytest-bdd.readthedocs.io/) for integration testing. All tests are in the `tests` folder.
|
||||
|
||||
Many tests can be created by only editing `*.feature` files with the same format as other tests. For more complicated functionality, you may need to implement steps in `tests/lib/` which are then executed by your tests in the `feature` files.
|
||||
|
||||
### Submitting pull requests
|
||||
|
||||
When you're ready, feel free to submit a pull request (PR). The jrnl maintainers generally review the pull requests every two weeks, but the continuous integration pipeline will run on automated tests on it within a matter of minutes and will report back any issues it has found with your code across a variety of environments.
|
||||
|
||||
The pull request template contains a checklist full of housekeeping items. Please fill them out as necessary when you submit.
|
||||
|
||||
If a pull request contains failing tests, it probably will not be reviewed, and it definitely will not be approved. However, if you need help resolving a failing test, please mention that in your PR.
|
||||
|
||||
### Finding things to work on
|
||||
|
||||
You can search the [jrnl GitHub issues](https://github.com/jrnl-org/jrnl/issues) by [label](https://github.com/jrnl-org/jrnl/labels) for things to work on. Here are some labels worth searching:
|
||||
|
||||
* [critical](https://github.com/jrnl-org/jrnl/labels/critical)
|
||||
* [help wanted](https://github.com/jrnl-org/jrnl/labels/help%20wanted)
|
||||
* [bug](https://github.com/jrnl-org/jrnl/labels/bug)
|
||||
* [enhancement](https://github.com/jrnl-org/jrnl/labels/enhancement)
|
||||
|
||||
You can also get a feel for the project's priorities by reviewing the [milestones](https://github.com/jrnl-org/jrnl/milestones).
|
||||
|
||||
### A note for new programmers and programmers new to python
|
||||
|
||||
Although jrnl has grown quite a bit since its inception, the overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
|
||||
|
||||
If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together!
|
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Encryption
|
||||
|
||||
## A Note on Security
|
||||
|
@ -35,6 +38,14 @@ replaces the encrypted journal file with a plain text file. You can also specify
|
|||
a filename, e.g., `jrnl --decrypt plain_text_copy.txt`, to leave the original
|
||||
encrypted file untouched and create a new plain text file next to it.
|
||||
|
||||
!!! note
|
||||
Changing `encrypt` in your [config file](./reference-config-file.md) 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. This is why the above commands are necessary.
|
||||
|
||||
## Storing Passwords in Your Keychain
|
||||
|
||||
Nobody can recover or reset your `jrnl` password. If you lose it,
|
||||
|
@ -89,16 +100,16 @@ something like `pip3 install crytography`)
|
|||
import base64
|
||||
import getpass
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
|
||||
filepath = input("journal file path: ")
|
||||
password = getpass.getpass("Password: ")
|
||||
|
||||
with open(Path(filepath),"rb") as f:
|
||||
with open(Path(filepath), "rb") as f:
|
||||
ciphertext = f.read()
|
||||
|
||||
password = password.encode("utf-8")
|
||||
|
@ -112,7 +123,7 @@ kdf = PBKDF2HMAC(
|
|||
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password))
|
||||
|
||||
print(Fernet(key).decrypt(ciphertext).decode('utf-8'))
|
||||
print(Fernet(key).decrypt(ciphertext).decode("utf-8"))
|
||||
```
|
||||
|
||||
**Example for jrnl v1 files**:
|
||||
|
@ -126,19 +137,19 @@ like `pip3 install pycrypto`)
|
|||
"""
|
||||
|
||||
import argparse
|
||||
from Crypto.Cipher import AES
|
||||
import getpass
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("filepath", help="journal file to decrypt")
|
||||
args = parser.parse_args()
|
||||
|
||||
pwd = getpass.getpass()
|
||||
key = hashlib.sha256(pwd.encode('utf-8')).digest()
|
||||
key = hashlib.sha256(pwd.encode("utf-8")).digest()
|
||||
|
||||
with open(args.filepath, 'rb') as f:
|
||||
with open(args.filepath, "rb") as f:
|
||||
ciphertext = f.read()
|
||||
|
||||
crypto = AES.new(key, AES.MODE_CBC, ciphertext[:16])
|
||||
|
|
137
docs/external-editors.md
Normal file
137
docs/external-editors.md
Normal file
|
@ -0,0 +1,137 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# External editors
|
||||
|
||||
Configure your preferred external editor by updating the `editor` option
|
||||
in your [configuration file](./reference-config-file.md#editor). If your editor is not
|
||||
in your operating system's `PATH` environment variable, then you will have to
|
||||
enter the full path of your editor.
|
||||
|
||||
Once it's configured, you can create an entry as a new document in your editor using the `jrnl`
|
||||
command by itself:
|
||||
|
||||
``` text
|
||||
jrnl
|
||||
```
|
||||
|
||||
You can specify the time and title of the entry as usual on the first line of the document.
|
||||
|
||||
If you want, you can skip the editor by including a quick entry with the `jrnl` command:
|
||||
|
||||
``` text
|
||||
jrnl yesterday: All my troubles seemed so far away.
|
||||
```
|
||||
|
||||
If you want to start the entry on the command line and continue writing in your chosen editor,
|
||||
use the `--edit` flag. For example:
|
||||
|
||||
``` text
|
||||
jrnl yesterday: All my troubles seemed so far away. --edit
|
||||
```
|
||||
|
||||
!!! note
|
||||
To save and log any entry edits, save and close the file.
|
||||
|
||||
All editors must be [blocking processes](https://en.wikipedia.org/wiki/Blocking_(computing)) to work with jrnl. Some editors, such as [micro](https://micro-editor.github.io/), are blocking by default, though others can be made to block with additional arguments, such as many of those documented below. If jrnl opens your editor but finishes running immediately, then your editor is not a blocking process, and you may be able to correct that with one of the suggestions below.
|
||||
|
||||
Please see [this section](./privacy-and-security.md#editor-history) about how
|
||||
your editor might leak sensitive information and how to mitigate that risk.
|
||||
|
||||
## Sublime Text
|
||||
|
||||
To use [Sublime Text](https://www.sublimetext.com/), install the command line
|
||||
tools for Sublime Text and configure your `jrnl.yaml` like this:
|
||||
|
||||
```yaml
|
||||
editor: "subl -w"
|
||||
```
|
||||
|
||||
Note the `-w` flag to make sure `jrnl` waits for Sublime Text to close the
|
||||
file before writing into the journal.
|
||||
|
||||
## Visual Studio Code
|
||||
|
||||
[Visual Studio Code](https://code.visualstudio.com) also requires a flag
|
||||
that tells the process to wait until the file is closed before exiting:
|
||||
|
||||
```yaml
|
||||
editor: "code --wait"
|
||||
```
|
||||
|
||||
On Windows, `code` is not added to the path by default, so you'll need to
|
||||
enter the full path to your `code.exe` file, or add it to the `PATH` variable.
|
||||
|
||||
## MacVim
|
||||
|
||||
Also 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`:
|
||||
|
||||
```yaml
|
||||
editor: "mvim -f"
|
||||
```
|
||||
|
||||
## Vim/Neovim
|
||||
|
||||
To use any of the Vim derivatives as editor in Linux, simply set the `editor`
|
||||
to the executable:
|
||||
|
||||
```yaml
|
||||
editor: "vim"
|
||||
# or
|
||||
editor: "nvim"
|
||||
```
|
||||
|
||||
## iA Writer
|
||||
|
||||
On OS X, you can use the fabulous [iA
|
||||
Writer](http://www.iawriter.com/mac) to write entries. Configure your
|
||||
`jrnl.yaml` like this:
|
||||
|
||||
```yaml
|
||||
editor: "open -b pro.writer.mac -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.
|
||||
|
||||
If the `pro.writer.mac` bundle identifier is not found on your system,
|
||||
you can find the right string to use by inspecting iA Writer's
|
||||
`Info.plist` file in your shell:
|
||||
|
||||
```sh
|
||||
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
|
||||
```
|
||||
|
||||
## Notepad++ on Windows
|
||||
|
||||
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
|
||||
the `jrnl` config file (`jrnl.yaml`) like this:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
|
||||
|
||||
## emacs
|
||||
|
||||
To use `emacs` as your editor, edit the `jrnl` config file (`jrnl.yaml`) like this:
|
||||
|
||||
```yaml
|
||||
editor: emacsclient -a "" -c
|
||||
```
|
||||
|
||||
When you're done editing the message, save and `C-x #` to close the buffer and stop the emacsclient process.
|
||||
|
||||
## Other editors
|
||||
|
||||
If you're using another editor and would like to share, feel free to [contribute documentation](./contributing.md#editing-documentation) on it.
|
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Formats
|
||||
|
||||
`jrnl` supports a variety of alternate formats. These can be used to display your
|
||||
|
@ -114,6 +117,11 @@ These formats are mainly intended for piping or exporting your journal to other
|
|||
programs. Even so, they can still be used in the same way as any other format (like
|
||||
written to a file, or displayed in your terminal, if you want).
|
||||
|
||||
!!! note
|
||||
You may see boxed messages like "2 entries found" when using these formats, but
|
||||
those messages are written to `stderr` instead of `stdout`, and won't be piped when
|
||||
using the `|` operator.
|
||||
|
||||
### JSON
|
||||
|
||||
``` sh
|
||||
|
@ -268,13 +276,14 @@ supported by many programs and programming languages.
|
|||
|
||||
### YAML
|
||||
``` sh
|
||||
jrnl --format yaml
|
||||
jrnl --format yaml --file 'my_directory/'
|
||||
```
|
||||
|
||||
This outputs your journal into YAML format. YAML is a commonly used data format and is
|
||||
supported by many programs and programming languages.
|
||||
supported by many programs and programming languages. [Exporting to directories](#exporting-to-directories) is the
|
||||
only supported YAML export option and each entry will be written to a separate file.
|
||||
|
||||
**Example output**:
|
||||
**Example file**:
|
||||
``` yaml
|
||||
title: This is the second sample entry
|
||||
date: 2020-07-01 20:00
|
||||
|
@ -332,14 +341,14 @@ jrnl --format json > myjournal.json
|
|||
If the `--file` argument is a directory, jrnl will export each entry into an individual file:
|
||||
|
||||
``` sh
|
||||
jrnl --format json --file my_entries/
|
||||
jrnl --format yaml --file my_entries/
|
||||
```
|
||||
|
||||
The contents of `my_entries/` will then look like this:
|
||||
|
||||
``` output
|
||||
my_entries/
|
||||
|- 2013_06_03_a-beautiful-day.json
|
||||
|- 2013_06_07_dinner-with-gabriel.json
|
||||
|- 2013_06_03_a-beautiful-day.yaml
|
||||
|- 2013_06_07_dinner-with-gabriel.yaml
|
||||
|- ...
|
||||
```
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Getting started
|
||||
|
||||
## Installation
|
||||
|
||||
On Mac and Linux, the easiest way to install `jrnl` is using
|
||||
[Homebrew](http://brew.sh/):
|
||||
|
||||
``` sh
|
||||
brew install jrnl
|
||||
```
|
||||
|
||||
On other platforms, install `jrnl` using [Python](https://www.python.org/) 3.6+ and [pipx](https://pipxproject.github.io/pipx/):
|
||||
The easiest way to install `jrnl` is using
|
||||
[pipx](https://pipx.pypa.io/stable/installation/)
|
||||
with [Python](https://www.python.org/) 3.10+:
|
||||
|
||||
``` sh
|
||||
pipx install jrnl
|
||||
```
|
||||
|
||||
!!! note
|
||||
`pipx` should be installed through either `brew` or `pip`. Missing dependencies and other issues
|
||||
may occur when installing `pipx` through `apt` or another package manager. Further installation
|
||||
instructions can be found in [pipx's documentation](https://pipxproject.github.io/pipx/installation/).
|
||||
|
||||
!!! tip
|
||||
Do not use `sudo` while installing `jrnl`. This may lead to path issues.
|
||||
|
||||
|
|
65
docs/journal-types.md
Normal file
65
docs/journal-types.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Journal Types
|
||||
`jrnl` can store your journal in a few different ways:
|
||||
|
||||
- a single text file (encrypted or otherwise)
|
||||
- a folder structure organized by date containing unencrypted text files
|
||||
- the DayOne Classic format
|
||||
|
||||
There is no need to specify what type of journal you'd like to use. Instead,
|
||||
`jrnl` will automatically detect the journal type based on whether you're
|
||||
referencing a file or a folder in your [config file](advanced.md),
|
||||
and if it's a folder, whether or not DayOne Classic content exists in it.
|
||||
|
||||
## Single File
|
||||
The single file format is the most flexible, as it can be [encrypted](encryption.md).
|
||||
To use it, enter any path that is a file or does not already exist. You can
|
||||
use any extension. `jrnl` will automatically create the file when you save
|
||||
your first entry.
|
||||
|
||||
## Folder
|
||||
The folder journal format organizes your entries into subfolders for the year
|
||||
and month and `.txt` files for each day. If there are multiple entries in a day,
|
||||
they all appear in the same `.txt` file.
|
||||
|
||||
The directory tree structure is in this format: `YYYY/MM/DD.txt`. For instance, if
|
||||
you have an entry on May 5th, 2021 in a folder journal at `~/folderjournal`, it will
|
||||
be located in: `~/folderjournal/2021/05/05.txt`
|
||||
|
||||
!!! note
|
||||
Creating a new folder journal can be done in two ways:
|
||||
|
||||
* Create a folder with the name of the journal before running `jrnl`. Otherwise, when you run `jrnl` for the first time, it will assume that you are creating a single file journal instead, and it will create a file at that path.
|
||||
* Create a new journal in your [config_file](advanced.md) and end the path with a ``/`` (on a POSIX system like Linux or MacOSX) or a ``\`` (on a Windows system). The folder will be created automatically if it doesn't exist.
|
||||
|
||||
!!! note
|
||||
Folder journals can't be encrypted.
|
||||
|
||||
## Day One Classic
|
||||
`jrnl` supports the original data format used by DayOne. It's similar to the folder
|
||||
journal format, except it's identified by either of these characteristics:
|
||||
|
||||
* the folder has a `.dayone` extension
|
||||
* the folder has a subfolder named `entries`
|
||||
|
||||
This is not to be confused with the DayOne 2.0 format, [which is very different](https://help.dayoneapp.com/en/articles/1187337-day-one-classic-is-retired).
|
||||
|
||||
!!! note
|
||||
DayOne Classic journals can't be encrypted.
|
||||
|
||||
## Changing your journal type
|
||||
You can't simply modify a journal's configuration to change its type. Instead,
|
||||
define a new journal as the type you'd like, and use
|
||||
[piping](https://en.wikipedia.org/wiki/Redirection_(computing)#Piping)
|
||||
to export your old journal as `txt` to an import command on your new journal.
|
||||
|
||||
For instance, if you have a `projects` journal you would like to import into
|
||||
a `new` journal, you would run the following after setting up the configuration
|
||||
for your `new` journal:
|
||||
```
|
||||
jrnl projects --format txt | jrnl new --import
|
||||
```
|
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Overview
|
||||
|
||||
`jrnl` is a simple journal application for the command line.
|
||||
|
@ -36,7 +39,7 @@ read them or edit them.
|
|||
`jrnl` plays nicely with your favorite text editor. You may prefer to write
|
||||
journal entries in an editor. Or you may want to make changes that require a
|
||||
more comprehensive application. `jrnl` can filter specific entries and pass them
|
||||
to the external editor of your choice.
|
||||
to the [external editor](./external-editors.md) of your choice.
|
||||
|
||||
## Encryption
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Privacy and Security
|
||||
|
||||
`jrnl` is designed with privacy and security in mind, but like any other
|
||||
|
@ -11,6 +14,35 @@ program there are some limitations to be aware of.
|
|||
passwords can be easily circumvented by someone with basic security skills
|
||||
to access to your encrypted `jrnl` file.
|
||||
|
||||
## Plausible deniability
|
||||
|
||||
You may be able to hide the contents of your journal behind a layer of encryption,
|
||||
but if someone has access to your configuration file, then they can figure out that
|
||||
you have a journal, where that journal file is, and when you last edited it.
|
||||
With a sufficient power imbalance, someone may be able to force you to unencrypt
|
||||
it through non-technical means.
|
||||
|
||||
## Spying
|
||||
|
||||
While `jrnl` can protect against unauthorized access to your journal entries while
|
||||
it isn't open, it cannot protect you against an unsafe computer/location.
|
||||
For example:
|
||||
|
||||
- Someone installs a keylogger, tracking what you type into your journal.
|
||||
- Someone watches your screen while you write your entry.
|
||||
- Someone installs a backdoor into `jrnl` or poisons your journal into revealing your entries.
|
||||
|
||||
## Saved Passwords
|
||||
|
||||
When creating an encrypted journal, you'll be prompted as to whether or not you
|
||||
want to "store the password in your keychain." This keychain is accessed using
|
||||
the [Python keyring library](https://pypi.org/project/keyring/), which has different
|
||||
behavior depending on your operating system.
|
||||
|
||||
In Windows, the keychain is the Windows Credential Manager (WCM), which can't be locked
|
||||
and can be accessed by any other application running under your username. If this is
|
||||
a concern for you, you may not want to store your password.
|
||||
|
||||
## Shell history
|
||||
|
||||
Since you can enter entries from the command line, any tool that logs command
|
||||
|
@ -75,16 +107,125 @@ unencrypted temporary remains on your disk. If your computer were to shut off
|
|||
during this time, or the `jrnl` process were killed unexpectedly, then the
|
||||
unencrypted temporary file will remain on your disk. You can mitigate this
|
||||
issue by only saving with your editor right before closing it. You can also
|
||||
manually delete these files (i.e. files named `jrnl_*.txt`) from your temporary
|
||||
folder.
|
||||
manually delete these files from your temporary folder. By default, they
|
||||
are named `jrnl*.jrnl`, but if you use a
|
||||
[template](reference-config-file.md#template), they will have the same
|
||||
extension as the template.
|
||||
|
||||
## Plausible deniability
|
||||
## Editor history
|
||||
|
||||
You may be able to hide the contents of your journal behind a layer of encryption,
|
||||
but if someone has access to your configuration file, then they can figure out that
|
||||
you have a journal, where that journal file is, and when you last edited it.
|
||||
With a sufficient power imbalance, someone may be able to force you to unencrypt
|
||||
it through non-technical means.
|
||||
Some editors keep usage history stored on disk for future use. This can be a
|
||||
security risk in the sense that sensitive information can leak via recent
|
||||
search patterns or editor commands.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
Visual Studio Code stores the contents of saved files to allow you to restore or
|
||||
review the contents later. You can disable this feature for all files by unchecking
|
||||
the `workbench.localHistory.enabled` setting in the
|
||||
[Settings editor](https://code.visualstudio.com/docs/getstarted/settings#_settings-editor).
|
||||
|
||||
Alternatively, you can disable this feature for specific files by configuring a
|
||||
[pattern](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)
|
||||
in the `workbench.localHistory.exclude` setting. To exclude unencrypted temporary files generated
|
||||
by `jrnl`, you can set the `**/jrnl*.jrnl` (unless you are using a
|
||||
[template](reference-config-file.md#template)) pattern for the `workbench.localHistory.exclude` setting
|
||||
in the [Settings editor](https://code.visualstudio.com/docs/getstarted/settings#_settings-editor).
|
||||
|
||||
!!! note
|
||||
On Windows, the history location is typically found at
|
||||
`%APPDATA%\Code\User\History`.
|
||||
|
||||
Visual Studio Code also creates a copy of all unsaved files that are open.
|
||||
It stores these copies in a backup location that's automatically cleaned when
|
||||
you save the file. However, if your computer shuts off before you save the file,
|
||||
or the Visual Studio Code process stops unexpectedly, then an unencrypted
|
||||
temporary file may remain on your disk. You can manually delete these files
|
||||
from the backup location.
|
||||
|
||||
!!! note
|
||||
On Windows, the backup location is typically found at
|
||||
`%APPDATA%\Code\Backups`.
|
||||
|
||||
### Vim
|
||||
|
||||
Vim stores progress data in a so called Viminfo file located at `~/.viminfo`
|
||||
which contains all sorts of user data including command line history, search
|
||||
string history, search/substitute patterns, contents of register etc. Also to
|
||||
be able to recover opened files after an unexpected application close Vim uses
|
||||
swap files.
|
||||
|
||||
These options as well as other leaky features can be disabled by setting the
|
||||
`editor` key in the Jrnl settings like this:
|
||||
|
||||
``` yaml
|
||||
editor: "vim -c 'set viminfo= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure'"
|
||||
```
|
||||
|
||||
To disable all plugins and custom configurations and start Vim with the default
|
||||
configuration `-u NONE` can be passed on the command line as well. This will
|
||||
ensure that any rogue plugins or other difficult to catch information leaks are
|
||||
eliminated. The downside to this is that the editor experience will decrease
|
||||
quite a bit.
|
||||
|
||||
To instead let Vim automatically detect when a Jrnl file is being edited an
|
||||
autocommand can be used. Place this in your `~/.vimrc`:
|
||||
|
||||
``` vim
|
||||
autocmd BufNewFile,BufReadPre *.jrnl setlocal viminfo= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you're using a [template](reference-config-file.md#template), you will
|
||||
have to use the template's file extension instead of `.jrnl`.
|
||||
|
||||
See `:h <option>` in Vim for more information about the options mentioned.
|
||||
|
||||
### Neovim
|
||||
|
||||
Neovim strives to be mostly compatible with Vim and has therefore similar
|
||||
functionality as Vim. One difference in Neovim is that the Viminfo file is
|
||||
instead called the ShaDa ("shared data") file which resides in
|
||||
`~/.local/state/nvim` (`~/.local/share/nvim` pre Neovim v0.8.0). The ShaDa file
|
||||
can be disabled in the same way as for Vim.
|
||||
|
||||
``` yaml
|
||||
editor: "nvim -c 'set shada= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure'"
|
||||
```
|
||||
|
||||
`-u NONE` can be passed here as well to start a session with the default configs.
|
||||
|
||||
As for Vim above we can create an autocommand in Vimscript:
|
||||
|
||||
``` vim
|
||||
autocmd BufNewFile,BufReadPre *.jrnl setlocal shada= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure
|
||||
```
|
||||
|
||||
or the same but in Lua:
|
||||
|
||||
``` lua
|
||||
vim.api.nvim_create_autocmd( {"BufNewFile","BufReadPre" }, {
|
||||
group = vim.api.nvim_create_augroup("PrivateJrnl", {}),
|
||||
pattern = "*.jrnl",
|
||||
callback = function()
|
||||
vim.o.shada = ""
|
||||
vim.o.swapfile = false
|
||||
vim.o.undofile = false
|
||||
vim.o.backup = false
|
||||
vim.o.writebackup = false
|
||||
vim.o.shelltemp = false
|
||||
vim.o.history = 0
|
||||
vim.o.modeline = false
|
||||
vim.o.secure = true
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you're using a [template](reference-config-file.md#template), you will
|
||||
have to use the template's file extension instead of `.jrnl`.
|
||||
|
||||
Please see `:h <option>` in Neovim for more information about the options mentioned.
|
||||
|
||||
## Notice any other risks?
|
||||
|
||||
|
|
236
docs/recipes.md
236
docs/recipes.md
|
@ -1,236 +0,0 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
# 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
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
jrnl @fixed -starred -n 10 -to "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?
|
||||
|
||||
```sh
|
||||
jrnl -from "jan 1 2013" -to "dec 31 2013" | wc -w
|
||||
```
|
||||
|
||||
Will give you the number of words you wrote in 2013. How long is my
|
||||
average entry?
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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`
|
||||
|
||||
```sh
|
||||
jrnlimport () {
|
||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
|
||||
}
|
||||
```
|
||||
|
||||
### Using templates
|
||||
|
||||
!!! note
|
||||
Templates require an [external editor](./advanced.md) be configured.
|
||||
|
||||
A template is a code snippet that makes it easier to use repeated text
|
||||
each time a new journal entry is started. There are two ways you can utilize
|
||||
templates in your entries.
|
||||
|
||||
#### 1. Command line arguments
|
||||
|
||||
If you had a `template.txt` file with the following contents:
|
||||
|
||||
```sh
|
||||
My Personal Journal
|
||||
Title:
|
||||
|
||||
Body:
|
||||
```
|
||||
|
||||
The `template.txt` file could be used to create a new entry with these
|
||||
command line arguements:
|
||||
|
||||
```sh
|
||||
jrnl < template.txt # Imports template.txt as the most recent entry
|
||||
jrnl -1 --edit # Opens the most recent entry in the editor
|
||||
```
|
||||
|
||||
#### 2. Include the template file in `jrnl.yaml`
|
||||
|
||||
A more efficient way to work with a template file is to declare the file
|
||||
in your config file by changing the `template` setting from `false` to the
|
||||
template file's path in double quotes:
|
||||
|
||||
```sh
|
||||
...
|
||||
template: "/path/to/template.txt"
|
||||
...
|
||||
```
|
||||
|
||||
Changes can be saved as you continue writing the journal entry and will be
|
||||
logged as a new entry in the journal you specified in the original argument.
|
||||
|
||||
!!! tip
|
||||
To read your journal entry or to verify the entry saved, you can use this
|
||||
command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options).
|
||||
|
||||
```sh
|
||||
jrnl -n 1
|
||||
```
|
||||
|
||||
### Prompts on shell reload
|
||||
|
||||
If you'd like to be prompted each time you refresh your shell, you can include
|
||||
this in your `.bash_profile`:
|
||||
|
||||
```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?'
|
||||
```
|
||||
|
||||
Whenever your shell is reloaded, you will be prompted to answer each of the
|
||||
questions in the example above. Each answer will be logged as a separate
|
||||
journal entry at the `default_hour` and `default_minute` listed in your
|
||||
`jrnl.yaml` [config file](../advanced/#configuration-file).
|
||||
|
||||
### Display random entry
|
||||
|
||||
You can use this to select one title at random and then display the whole
|
||||
entry. The invocation of `cut` needs to match the format of the timestamp.
|
||||
For timestamps that have a space between data and time components, select
|
||||
fields 1 and 2 as shown. For timestamps that have no whitespace, select
|
||||
only field 1.
|
||||
|
||||
```sh
|
||||
jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
|
||||
```
|
||||
|
||||
## External editors
|
||||
|
||||
Configure your preferred external editor by updating the `editor` option
|
||||
in your `jrnl.yaml` file. (See [advanced usage](./advanced.md) for details).
|
||||
|
||||
!!! note
|
||||
To save and log any entry edits, save and close the file.
|
||||
|
||||
If your editor is not in your operating system's `PATH` environment variable,
|
||||
then you will have to enter in the full path of your editor.
|
||||
|
||||
### Sublime Text
|
||||
|
||||
To use [Sublime Text](https://www.sublimetext.com/), install the command line
|
||||
tools for Sublime Text and configure your `jrnl.yaml` like this:
|
||||
|
||||
```yaml
|
||||
editor: "subl -w"
|
||||
```
|
||||
|
||||
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
|
||||
file before writing into the journal.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
[Visual Studio Code](https://code.visualstudio.com) also requires a flag
|
||||
that tells the process to wait until the file is closed before exiting:
|
||||
|
||||
```yaml
|
||||
editor: "code --wait"
|
||||
```
|
||||
|
||||
On Windows, `code` is not added to the path by default, so you'll need to
|
||||
enter the full path to your `code.exe` file, or add it to the `PATH` variable.
|
||||
|
||||
### MacVim
|
||||
|
||||
Also 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`:
|
||||
|
||||
```yaml
|
||||
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.yaml` like this:
|
||||
|
||||
```yaml
|
||||
editor: "open -b pro.writer.mac -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.
|
||||
|
||||
If the `pro.writer.mac` bundle identifier is not found on your system,
|
||||
you can find the right string to use by inspecting iA Writer's
|
||||
`Info.plist` file in your shell:
|
||||
|
||||
```sh
|
||||
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
|
||||
```
|
||||
|
||||
### Notepad++ on Windows
|
||||
|
||||
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
|
||||
the jrnl config file (`jrnl.yaml`) like this:
|
||||
|
||||
```yaml
|
||||
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.
|
141
docs/reference-command-line.md
Normal file
141
docs/reference-command-line.md
Normal file
|
@ -0,0 +1,141 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Command Line Reference
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
usage: jrnl [--debug] [--help] [--version] [--list] [--encrypt] [--decrypt]
|
||||
[--import] [-on DATE] [-today-in-history] [-month DATE]
|
||||
[-day DATE] [-year DATE] [-from DATE] [-to DATE] [-contains TEXT]
|
||||
[-and] [-starred] [-n [NUMBER]] [-not [TAG]] [--edit] [--delete]
|
||||
[--format TYPE] [--tags] [--short]
|
||||
[--config-override CONFIG_KEY CONFIG_VALUE]
|
||||
[--config-file CONFIG_FILE_PATH]
|
||||
[[...]]
|
||||
```
|
||||
|
||||
## Standalone Commands
|
||||
|
||||
These commands will exit after they complete. You may only run one at a time.
|
||||
|
||||
### --help
|
||||
Show a help message.
|
||||
|
||||
### --version
|
||||
Print version and license information.
|
||||
|
||||
### --list
|
||||
List the config file location, all configured journals, and their locations.
|
||||
|
||||
### ---encrypt
|
||||
Encrypt a journal. See [encryption](encryption.md) for more information.
|
||||
|
||||
### --decrypt
|
||||
Decrypt a journal. See [encryption](encryption.md) for more information.
|
||||
|
||||
|
||||
### --import
|
||||
Import entries from another journal. If any entries have the exact same content
|
||||
and timestamp, they will be deduplicated.
|
||||
|
||||
Optional parameters:
|
||||
```sh
|
||||
--file FILENAME
|
||||
```
|
||||
Specify a file to import. If not provided, `jrnl` will use STDIN as the data source.
|
||||
|
||||
```sh
|
||||
--format TYPE
|
||||
```
|
||||
Specify the format of the file that is being imported. Defaults to the same data
|
||||
storage method that jrnl uses. See [formats](formats.md) for more information.
|
||||
|
||||
## Writing new entries
|
||||
See [Basic Usage](usage.md).
|
||||
|
||||
## Searching
|
||||
|
||||
To find entries from your journal, use any combination of the below filters.
|
||||
Only entries that match all the filters will be displayed.
|
||||
|
||||
When specifying dates, you can use the same kinds of dates you use for new
|
||||
entries, such as `yesterday`, `today`, `Tuesday`, or `2021-08-01`.
|
||||
|
||||
| Search Argument | Description |
|
||||
| --- | --- |
|
||||
| -on DATE | Show entries on this date |
|
||||
| -today-in-history | Show entries of today over the years |
|
||||
| -month DATE | Show entries on this month of any year |
|
||||
| -day DATE | Show entries on this day of any month |
|
||||
| -year DATE | Show entries of a specific year |
|
||||
| -from DATE | Show entries after, or on, this date |
|
||||
| -to DATE | Show entries before, or on, this date (alias: -until) |
|
||||
| -contains TEXT | Show entries containing specific text (put quotes around text with spaces) |
|
||||
| -and | Show only entries that match all conditions, like saying "x AND y" (default: OR) |
|
||||
| -starred | Show only starred entries (marked with *) |
|
||||
| -tagged | Show only tagged entries (marked with the [configured tagsymbols](reference-config-file.md#tagsymbols)) |
|
||||
| -n [NUMBER] | Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect) |
|
||||
| -not [TAG] | Exclude entries with this tag |
|
||||
| -not -starred | Exclude entries that are starred |
|
||||
| -not -tagged | Exclude entries that are tagged |
|
||||
|
||||
## Searching Options
|
||||
These help you do various tasks with the selected entries from your search.
|
||||
If used on their own (with no search), they will act on your entire journal.
|
||||
|
||||
### --edit
|
||||
Opens the selected entries in your configured editor. It will fail if the
|
||||
`editor` key is not set in your config file.
|
||||
|
||||
Once you begin editing, you can add multiple entries and delete entries
|
||||
by modifying the text in your editor. When your editor closes, jrnl reads
|
||||
the temporary file you were editing and makes the changes to your journal.
|
||||
|
||||
### --delete
|
||||
Interactively deletes selected entries. You'll be asked to confirm deletion of
|
||||
each entry.
|
||||
|
||||
### --change-time DATE
|
||||
Interactively changes the time of the selected entries to the date specified,
|
||||
or to right now if no date is specified. You'll be asked to confirm each entry,
|
||||
unless you are using this with `--edit` on a single entry.
|
||||
|
||||
### --format TYPE
|
||||
Display selected entries in an alternate format. See [formats](formats.md).
|
||||
|
||||
#### Optional parameters
|
||||
```sh
|
||||
--file FILENAME
|
||||
```
|
||||
Write output to file instead of STDOUT. In most shells, the
|
||||
same effect can be achieved using `>`.
|
||||
|
||||
### --tags
|
||||
|
||||
Alias for '--format tags'. Returns a list of all tags and the number of times
|
||||
they occur within the searched entries. If there are no tags found, `jrnl` will output a message saying so.
|
||||
|
||||
### --short
|
||||
Only shows the date and titles of the searched entries.
|
||||
|
||||
## Configuration arguments
|
||||
|
||||
### --config-override CONFIG_KEY CONFIG_VALUE
|
||||
|
||||
Override configured key-value pair with CONFIG_KV_PAIR for this command invocation only. To access config keys that aren't at the top level, separate the keys with a dot, such as `colors.title` to access the `title` key within the `colors` key. Read [advanced usage](./advanced.md) for examples.
|
||||
|
||||
### --config-file CONFIG_FILE_PATH
|
||||
|
||||
Use the config file at CONFIG_FILE_PATH for this command invocation only.
|
||||
Read [advanced usage](./advanced.md) for examples.
|
||||
|
||||
## Other Arguments
|
||||
|
||||
### --debug
|
||||
Prints information useful for troubleshooting while `jrnl` executes.
|
||||
|
||||
### --diagnostic
|
||||
Prints diagnostic information useful for [reporting issues](https://github.com/jrnl-org/jrnl/issues).
|
123
docs/reference-config-file.md
Normal file
123
docs/reference-config-file.md
Normal file
|
@ -0,0 +1,123 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Configuration File Reference
|
||||
|
||||
`jrnl` stores its information in a YAML configuration file.
|
||||
|
||||
!!! note
|
||||
Backup your journal and config file before editing. Changes to the config file
|
||||
can have destructive effects on your journal!
|
||||
|
||||
## Config location
|
||||
You can find your configuration file location by running:
|
||||
`jrnl --list`
|
||||
|
||||
By default, the configuration file is `~/.config/jrnl/jrnl.yaml`.
|
||||
If you have the `XDG_CONFIG_HOME` variable set, the configuration
|
||||
file will be saved as `$XDG_CONFIG_HOME/jrnl/jrnl.yaml`.
|
||||
|
||||
!!! note
|
||||
On Windows, the configuration file is typically found at
|
||||
`%USERPROFILE%\.config\jrnl\jrnl.yaml`.
|
||||
|
||||
|
||||
## Config format
|
||||
The configuration file is a [YAML](https://yaml.org/) file and can be edited with
|
||||
a text editor.
|
||||
|
||||
## Config keys
|
||||
|
||||
### journals
|
||||
|
||||
Describes each journal used by `jrnl`. Each indented key after this key is
|
||||
the name of a journal.
|
||||
|
||||
If a journal key has a value, that value will be interpreted as the path
|
||||
to the journal. Otherwise, the journal needs the additional indented key
|
||||
`journal` to specify its path.
|
||||
|
||||
All keys below can be specified for each journal at the same level as the
|
||||
`journal` key. If a key conflicts with a top-level key, the journal-specific
|
||||
key will be used instead.
|
||||
|
||||
### editor
|
||||
If set, executes this command to launch an external editor for
|
||||
writing and editing your entries. The path to a temporary file
|
||||
is passed after it, and `jrnl` processes the file once
|
||||
the editor returns control to `jrnl`.
|
||||
|
||||
Some editors require special options to work properly, since they must be
|
||||
blocking processes to work with `jrnl`. See [External Editors](external-editors.md)
|
||||
for details.
|
||||
|
||||
### encrypt
|
||||
If `true`, encrypts your journal using AES. Do not change this
|
||||
value for journals that already have data in them.
|
||||
|
||||
### template
|
||||
The path to a text file to use as a template for new entries. Only works when you
|
||||
have the `editor` field configured. If you use a template, the editor's
|
||||
[temporary files](privacy-and-security.md#files-in-transit-from-editor-to-jrnl)
|
||||
will have the same extension as the template.
|
||||
|
||||
### tagsymbols
|
||||
Symbols to be interpreted as tags.
|
||||
|
||||
!!! 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
|
||||
|
||||
> `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.
|
||||
|
||||
### default_hour and default_minute
|
||||
Entries will be created at this time if you supply a date but no specific time (for example, `last thursday`).
|
||||
|
||||
### timeformat
|
||||
Defines how to format the timestamps as they are stored in your journal.
|
||||
See the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference.
|
||||
|
||||
Do not change this for an existing journal, since that might lead
|
||||
to data loss.
|
||||
|
||||
!!! note
|
||||
`jrnl` doesn't support the `%z` or `%Z` time zone identifiers.
|
||||
|
||||
### 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. Set to `auto` to let `jrnl` automatically determine
|
||||
the terminal width.
|
||||
|
||||
### colors
|
||||
A dictionary that controls the colors used to display journal entries.
|
||||
It has four subkeys, which are: `body`, `date`, `tags`, and `title`.
|
||||
|
||||
Current valid values are: `BLACK`, `RED`, `GREEN`, `YELLOW`, `BLUE`,
|
||||
`MAGENTA`, `CYAN`, `WHITE`, and `NONE`.
|
||||
|
||||
`colorama.Fore` is used for colorization, and you can find the [docs here](https://github.com/tartley/colorama#colored-output).
|
||||
|
||||
To disable colored output, set the value to `NONE`.
|
||||
|
||||
### display_format
|
||||
Specifies formatter to use by default. See [formats](formats.md).
|
||||
|
||||
### version
|
||||
`jrnl` automatically updates this field to the version that it is running.
|
||||
There is no need to change this field manually.
|
224
docs/tips-and-tricks.md
Normal file
224
docs/tips-and-tricks.md
Normal file
|
@ -0,0 +1,224 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Tips and Tricks
|
||||
|
||||
This page contains tips and tricks for using `jrnl`, often in conjunction
|
||||
with other tools, including external editors.
|
||||
|
||||
## 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
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
jrnl @fixed -starred -n 10 -to "jan 2013" --short
|
||||
```
|
||||
|
||||
To get a short summary of the 10 most recent, favourite entries before
|
||||
January 1, 2013 that are tagged with `@fixed`.
|
||||
|
||||
## Statistics
|
||||
|
||||
How much did I write last year?
|
||||
|
||||
```sh
|
||||
jrnl -from "jan 1 2013" -to "dec 31 2013" | wc -w
|
||||
```
|
||||
|
||||
Will give you the number of words you wrote in 2013. How long is my
|
||||
average entry?
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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`
|
||||
|
||||
```sh
|
||||
jrnlimport () {
|
||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
|
||||
}
|
||||
```
|
||||
|
||||
## Using Templates
|
||||
|
||||
!!! note
|
||||
Templates require an [external editor](./advanced.md) be configured.
|
||||
|
||||
Templates are text files that are used for creating structured journals.
|
||||
There are three ways you can use templates:
|
||||
|
||||
### 1. Use the `--template` command line argument and the default $XDG_DATA_HOME/jrnl/templates directory
|
||||
|
||||
`$XDG_DATA_HOME/jrnl/templates` is created by default to store your templates! Create a template (like `default.md`) in this directory and pass `--template FILE_IN_DIR`.
|
||||
|
||||
```sh
|
||||
jrnl --template default.md
|
||||
```
|
||||
|
||||
### 2. Use the `--template` command line argument with a local / absolute path
|
||||
|
||||
You can create a template file with any text. Here is an example:
|
||||
|
||||
```sh
|
||||
# /tmp/template.txt
|
||||
My Personal Journal
|
||||
Title:
|
||||
|
||||
Body:
|
||||
```
|
||||
|
||||
Then, pass the absolute or relative path to the template file as an argument, and your external
|
||||
editor will open and have your template pre-populated.
|
||||
|
||||
```sh
|
||||
jrnl --template /tmp/template.md
|
||||
```
|
||||
|
||||
### 3. Set a default template file in `jrnl.yaml`
|
||||
|
||||
If you want a template by default, change the value of `template` in the [config file](./reference-config-file.md)
|
||||
from `false` to the template file's path, wrapped in double quotes:
|
||||
|
||||
```sh
|
||||
...
|
||||
template: "/path/to/template.txt"
|
||||
...
|
||||
```
|
||||
|
||||
!!! tip
|
||||
To read your journal entry or to verify the entry saved, you can use this
|
||||
command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options).
|
||||
|
||||
```sh
|
||||
jrnl -n 1
|
||||
```
|
||||
|
||||
## Prompts on shell reload
|
||||
|
||||
If you'd like to be prompted each time you refresh your shell, you can include
|
||||
this in your `.bash_profile`:
|
||||
|
||||
```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?'
|
||||
```
|
||||
|
||||
Whenever your shell is reloaded, you will be prompted to answer each of the
|
||||
questions in the example above. Each answer will be logged as a separate
|
||||
journal entry at the `default_hour` and `default_minute` listed in your
|
||||
`jrnl.yaml` [config file](../advanced/#configuration-file).
|
||||
|
||||
## Display random entry
|
||||
|
||||
You can use this to select one title at random and then display the whole
|
||||
entry. The invocation of `cut` needs to match the format of the timestamp.
|
||||
For timestamps that have a space between data and time components, select
|
||||
fields 1 and 2 as shown. For timestamps that have no whitespace, select
|
||||
only field 1.
|
||||
|
||||
```sh
|
||||
jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
|
||||
```
|
||||
|
||||
|
||||
## Launch a terminal for rapid logging
|
||||
You can use this to launch a terminal that is the `jrnl` stdin prompt so you can start typing away immediately.
|
||||
|
||||
```bash
|
||||
jrnl --config-override editor ""
|
||||
```
|
||||
|
||||
Bind this to a keyboard shortcut.
|
||||
|
||||
Map `Super+Alt+J` to launch the terminal with `jrnl` prompt
|
||||
|
||||
- **xbindkeys**
|
||||
In your `.xbindkeysrc`
|
||||
|
||||
```ini
|
||||
Mod4+Mod1+j
|
||||
alacritty -t floating-jrnl -e jrnl --config-override editor "",
|
||||
```
|
||||
|
||||
- **I3 WM** Launch a floating terminal with the `jrnl` prompt
|
||||
|
||||
```ini
|
||||
bindsym Mod4+Mod1+j exec --no-startup-id alacritty -t floating-jrnl -e jrnl --config-override editor ""
|
||||
for_window[title="floating *"] floating enable
|
||||
```
|
||||
## Visualize Formatted Markdown in the CLI
|
||||
|
||||
Out of the box, `jrnl` can output journal entries in Markdown. To visualize it, you can pipe to [mdless](https://github.com/ttscoff/mdless), which is a [less](https://en.wikipedia.org/wiki/Less_(Unix))-like tool that allows you to visualize your Markdown text with formatting and syntax highlighting from the CLI. You can use this in any shell that supports piping.
|
||||
|
||||
The simplest way to visualize your Markdown output with `mdless` is as follows:
|
||||
```sh
|
||||
jrnl --export md | mdless
|
||||
```
|
||||
|
||||
This will render your Markdown output in the whole screen.
|
||||
|
||||
Fortunately, `mdless` has an option that allows you to adjust the screen width by using the `-w` option as follows:
|
||||
|
||||
```sh
|
||||
jrnl --export md | mdless -w 70
|
||||
```
|
||||
|
||||
If you want Markdown to be your default display format, you can define this in your config file as follows:
|
||||
|
||||
```yaml
|
||||
display_format: md
|
||||
# or
|
||||
display_format: markdown
|
||||
```
|
||||
|
||||
For more information on how `jrnl` outputs your entries in Markdown, please visit the [Formats](./formats.md) section.
|
||||
|
||||
|
||||
## Jump to end of buffer (with vi)
|
||||
|
||||
To cause vi to jump to the end of the last line of the entry you edit, in your config file set:
|
||||
|
||||
```yaml
|
||||
editor: vi + -c "call cursor('.',strwidth(getline('.')))"
|
||||
```
|
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
# Basic Usage #
|
||||
|
||||
`jrnl` has two modes: **composing** and **viewing**. Whenever you don't enter
|
||||
|
@ -55,14 +58,15 @@ If you don't specify a date and time (e.g., `jrnl finished writing letter to bro
|
|||
|
||||
If you don't use a timestamp, `jrnl` will create an entry using the current
|
||||
time. If you use a date only (no time), `jrnl` will use the default time
|
||||
specified in your configuration file (see [advanced usage](./advanced.md)).
|
||||
specified in your [configuration file](./reference-config-file.md#default_hour-and-default_minute).
|
||||
Behind the scenes, `jrnl` reorganizes entries in chronological order.
|
||||
|
||||
### Using Tags ###
|
||||
|
||||
`jrnl` supports tags. The default tag symbol is `@` (largely because `#` is a
|
||||
reserved character). You can specify your own tag symbol in the configuration
|
||||
file. To use tags, preface the desired tag with the symbol:
|
||||
reserved character). You can specify your own tag symbol in the
|
||||
[configuration file](./reference-config-file.md#tagsymbols). To use tags, preface the
|
||||
desired tag with the symbol:
|
||||
|
||||
```sh
|
||||
jrnl Had a wonderful day at the @beach with @Tom and @Anna.
|
||||
|
@ -157,7 +161,7 @@ jrnl -n 5 @pinkie -and @WorldDomination
|
|||
|
||||
displays the last five entries containing _both_ `@pinkie` _and_
|
||||
`@worldDomination`. You can change which symbols you'd like to use for tagging
|
||||
in the configuration.
|
||||
in the [configuration file](./reference-config-file.md#tagsymbols).
|
||||
|
||||
!!! note
|
||||
Entering `jrnl @pinkie @WorldDomination` will display entries in which both
|
||||
|
@ -183,8 +187,8 @@ jrnl -starred
|
|||
|
||||
You can edit entries after writing them. This is particularly useful when your
|
||||
journal file is encrypted. To use this feature, you need to have an external
|
||||
editor configured in your configuration file. You can also edit only the entries
|
||||
that match specific search criteria. For example,
|
||||
editor configured in your [configuration file](./reference-config-file.md#editor). You
|
||||
can also edit only the entries that match specific search criteria. For example,
|
||||
|
||||
```sh
|
||||
jrnl -to 1950 @texas -and @history --edit
|
||||
|
@ -248,5 +252,5 @@ To list all of your journals:
|
|||
jrnl --list
|
||||
```
|
||||
|
||||
The journals displayed correspond to those specified in the `jrnl` configuration
|
||||
file.
|
||||
The journals displayed correspond to those specified in the `jrnl`
|
||||
[configuration file](./reference-config-file.md#journals).
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html */
|
||||
/*
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* For dark bg */
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/*
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Atom One Dark With support for ReasonML by Gidi Morris, based off work by
|
||||
Daniel Gamage
|
||||
|
||||
|
@ -136,3 +139,9 @@ Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
|||
.rst-content .tip .admonition {
|
||||
background: var(--light-blue);
|
||||
}
|
||||
|
||||
/* hack to bypass a11y issue with conflicting highlight.css files */
|
||||
code.language-xml span.hljs-meta span.hljs-string {
|
||||
color: var(--green) !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/* Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html */
|
||||
/*
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
/* reset */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}
|
||||
|
||||
body {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html */
|
||||
/*
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Overrides for jrnl theme */
|
||||
|
@ -51,6 +53,14 @@ h2 {
|
|||
margin-top: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p,
|
||||
td,
|
||||
tr,
|
||||
|
@ -71,9 +81,26 @@ div.rst-content {
|
|||
max-width: 54em;
|
||||
}
|
||||
|
||||
|
||||
.wy-side-nav-search,
|
||||
.wy-menu-vertical li.current {
|
||||
.wy-menu-vertical li.current,
|
||||
.wy-menu-vertical li.toctree-l1.current > a,
|
||||
.wy-menu-vertical li.toctree-l2.current > a,
|
||||
.wy-menu-vertical li.toctree-l3.current > a {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3,
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.toctree-l4,
|
||||
.wy-menu-vertical li.toctree-l5,
|
||||
.wy-menu-vertical li.toctree-l6,
|
||||
.wy-menu-vertical li.toctree-l7 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wy-nav-top {
|
||||
|
@ -91,7 +118,7 @@ div.rst-content {
|
|||
background-size: 100%;
|
||||
}
|
||||
|
||||
a.icon-home:before {
|
||||
.wy-side-nav-search a.icon-home:before {
|
||||
display: block;
|
||||
width: 84px;
|
||||
height: 70px;
|
||||
|
@ -107,7 +134,9 @@ a.icon-home:before {
|
|||
line-height: 2em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical a:hover {
|
||||
.wy-menu-vertical a:hover,
|
||||
.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:hover,
|
||||
.wy-menu-vertical li.current a:hover {
|
||||
background-color: var(--black-shadow);
|
||||
color: var(--white);
|
||||
}
|
||||
|
@ -119,14 +148,10 @@ a.icon-home:before {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current>a {
|
||||
.wy-menu-vertical li.toctree-l1.current > a {
|
||||
background: var(--darkest-purple);
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.current>a:hover {
|
||||
background: var(--darkest-purple);
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.on a,
|
||||
|
@ -135,7 +160,7 @@ a.icon-home:before {
|
|||
}
|
||||
|
||||
.wy-menu-vertical li.on a,
|
||||
.wy-menu-vertical li.current>a:after {
|
||||
.wy-menu-vertical li > a.current:after {
|
||||
position: absolute;
|
||||
right: 0em;
|
||||
z-index: 999;
|
||||
|
@ -146,11 +171,6 @@ a.icon-home:before {
|
|||
border-bottom: 1em solid transparent;
|
||||
border-right: 1em solid var(--white);
|
||||
}
|
||||
.wy-menu-vertical li li.toctree-l2 {
|
||||
font-weight: 800 !important;
|
||||
font-size: 50px;
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.toctree-expand:before {
|
||||
display: none !important;
|
||||
|
@ -161,10 +181,14 @@ a.icon-home:before {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical p.caption {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.wy-menu-vertical span {
|
||||
color: var(--white);
|
||||
font-size: 1.2em;
|
||||
font-weight: 300;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li a {
|
||||
|
@ -175,7 +199,6 @@ a.icon-home:before {
|
|||
|
||||
.wy-nav-side {
|
||||
background-color: var(--mid-purple);
|
||||
background-image: linear-gradient(211deg, var(--mid-purple) 0%, var(--dark-purple) 100%);
|
||||
font-weight: 300;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -206,10 +229,11 @@ form .search-query::placeholder {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
.toctree-l2 a:first-child {
|
||||
display: none;
|
||||
.wy-menu-vertical li.current ul {
|
||||
background-color: var(--mid-purple);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Logo: ; */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
|
49
docs_theme/breadcrumbs.html
Normal file
49
docs_theme/breadcrumbs.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!--
|
||||
Copied from https://github.com/mkdocs/mkdocs/blob/master/mkdocs/themes/readthedocs/breadcrumbs.html
|
||||
Then lightly modified for accessibility
|
||||
-->
|
||||
|
||||
<div role="navigation" aria-label="breadcrumbs navigation">
|
||||
<ul class="wy-breadcrumbs">
|
||||
<li><a href="{{ nav.homepage.url|url }}" class="icon icon-home" aria-label="{% trans %}Docs{% endtrans %}"></a> »</li>
|
||||
{%- if page %}
|
||||
{%- for doc in page.ancestors[::-1] %}
|
||||
{%- if doc.link %}
|
||||
<li><a href="{{ doc.link|e }}">{{ doc.title }}</a> »</li>
|
||||
{%- else %}
|
||||
<li>{{ doc.title }} »</li>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
<li>{{ page.title }}</li>
|
||||
{%- endif %}
|
||||
<li class="wy-breadcrumbs-aside">
|
||||
{%- block repo %}
|
||||
{%- if page and page.edit_url %}
|
||||
{%- if config.repo_name|lower == 'github' %}
|
||||
<a href="{{ page.edit_url }}" class="icon icon-github"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
|
||||
{%- elif config.repo_name|lower == 'bitbucket' %}
|
||||
<a href="{{ page.edit_url }}" class="icon icon-bitbucket"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
|
||||
{%- elif config.repo_name|lower == 'gitlab' %}
|
||||
<a href="{{ page.edit_url }}" class="icon icon-gitlab"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
|
||||
{%- elif config.repo_name %}
|
||||
<a href="{{ page.edit_url }}">{% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
|
||||
{%- else %}
|
||||
<a href="{{ page.edit_url }}">{% trans %}Edit{% endtrans %}</a>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
{%- if config.theme.prev_next_buttons_location|lower in ['top', 'both']
|
||||
and page and (page.next_page or page.previous_page) %}
|
||||
<div class="rst-breadcrumbs-buttons" role="navigation" aria-label="{% trans %}Breadcrumb Navigation{% endtrans %}">
|
||||
{%- if page.previous_page %}
|
||||
<a href="{{ page.previous_page.url|url }}" class="btn btn-neutral float-left" title="{{ page.previous_page.title }}"><span class="icon icon-circle-arrow-left" aria-hidden="true"></span> {% trans %}Previous{% endtrans %}</a>
|
||||
{%- endif %}
|
||||
{%- if page.next_page %}
|
||||
<a href="{{ page.next_page.url|url }}" class="btn btn-neutral float-right" title="{{ page.next_page.title }}">{% trans %}Next{% endtrans %} <span class="icon icon-circle-arrow-right" aria-hidden="true"></span></a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
<hr/>
|
||||
</div>
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
47
docs_theme/index.html
Executable file → Normal file
47
docs_theme/index.html
Executable file → Normal file
|
@ -1,5 +1,8 @@
|
|||
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
@ -7,18 +10,18 @@
|
|||
<meta charset="utf-8">
|
||||
<title>jrnl - The Command Line Journal</title>
|
||||
<meta name="description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="image" content="https://jrnl.sh/img/banner.png">
|
||||
<meta name="image" content="https://jrnl.sh/en/stable/img/banner_og.png">
|
||||
<meta itemprop="name" content="jrnl - The Command Line Journal">
|
||||
<meta itemprop="description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta itemprop="image" content="https://jrnl.sh/img/banner_og.png">
|
||||
<meta itemprop="image" content="https://jrnl.sh/en/stable/img/banner_og.png">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="jrnl - The Command Line Journal">
|
||||
<meta name="twitter:description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="twitter:creator" content="jrnl">
|
||||
<meta name="twitter:image:src" content="https://jrnl.sh/img/banner_twitter.png">
|
||||
<meta name="twitter:image:src" content="https://jrnl.sh/en/stable/img/banner_twitter.png">
|
||||
<meta name="og:title" content="jrnl - The Command Line Journal">
|
||||
<meta name="og:description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="og:image" content="https://jrnl.sh/img/banner_og.png">
|
||||
<meta name="og:image" content="https://jrnl.sh/en/stable/img/banner_og.png">
|
||||
<meta name="og:url" content="https://jrnl.sh">
|
||||
<meta name="og:site_name" content="jrnl - The Command Line Journal">
|
||||
<meta name="og:type" content="website">
|
||||
|
@ -35,8 +38,8 @@
|
|||
"name": "jrnl",
|
||||
"description": "Collect your thoughts and notes without leaving the command line.",
|
||||
"operatingSystem": ["macOS", "Windows", "Linux"],
|
||||
"thumbnailUrl": "https://jrnl.sh/img/banner_og.png",
|
||||
"installUrl": "https://jrnl.sh/installation",
|
||||
"thumbnailUrl": "https://jrnl.sh/en/stable/img/banner_og.png",
|
||||
"installUrl": "https://jrnl.sh/en/stable/installation",
|
||||
"softwareVersion": "2.5"
|
||||
}
|
||||
</script>
|
||||
|
@ -62,7 +65,7 @@
|
|||
<a href="overview">Documentation</a>
|
||||
<a href="installation" class="cta">Get Started</a>
|
||||
<a href="http://github.com/jrnl-org/jrnl" title="View on Github">Fork on GitHub</a>
|
||||
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Collect+your+thoughts+and+notes+without+leaving+the+command+line.+https%3A%2F%2Fjrnl.sh+via+@JrnlSh"><i class="icon twitter"></i>Tell your friends on Twitter</a>
|
||||
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Collect+your+thoughts+and+notes+without+leaving+the+command+line.+https%3A%2F%2Fjrnl.sh+via+@JrnlSh">Tell your friends on X</a>
|
||||
</nav>
|
||||
<div class="flex">
|
||||
<section>
|
||||
|
@ -73,22 +76,22 @@
|
|||
<section>
|
||||
<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>
|
||||
<p>Your journals are stored in plain-text files that will still be readable in 50 years when your fancy proprietary apps will have gone the way of the dodo.</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon secure"></i>
|
||||
<h3>Secure.</h3>
|
||||
<p>Encrypt your journals with industry-strength AES encryption. The NSA won't be able to read your dirty secrets.</p>
|
||||
<p>Encrypt your journals with industry-strength AES encryption. Nobody will be able to read your dirty secrets—not even you, if you lose your password!</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon sync"></i>
|
||||
<h3>Accessible anywhere.</h3>
|
||||
<p>Sync your journals with Dropbox and capture your thoughts where ever you are.</p>
|
||||
<p>Sync your journal files with other tools like Dropbox to capture your thoughts wherever you are.</p>
|
||||
</section>
|
||||
<section>
|
||||
<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/jrnl-org/jrnl" title="Fork jrnl on GitHub">you</a>?</p>
|
||||
<p>jrnl is made by a bunch of really friendly and remarkably amazing people. Maybe even <a href="https://www.github.com/jrnl-org/jrnl" title="Fork jrnl on GitHub">you</a>?</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon folders"></i>
|
||||
|
@ -100,21 +103,21 @@
|
|||
<footer>
|
||||
jrnl is made with love by <a href="https://github.com/jrnl-org/jrnl/graphs/contributors" title="Contributors">many fabulous people</a>. If you need help, <a href="https://github.com/jrnl-org/jrnl/issues/new/choose" title="Open a new issue on Github">submit an issue</a> on Github.
|
||||
</footer>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/typed.js/2.0.10/typed.min.js"></script>
|
||||
<script src="https://unpkg.com/typed.js@2.1.0/dist/typed.umd.js"></script>
|
||||
<script>
|
||||
new Typed("#typed", {
|
||||
strings: [
|
||||
"jrnl today: Started writing my memoirs. On the command line. Like a boss.",
|
||||
"jrnl Started writing my memoirs on the command line. 🎉🔥💻🔥🎉",
|
||||
"jrnl yesterday 2pm: used jrnl to keep track of accomplished tasks. The done.txt for my todo.txt",
|
||||
"jrnl <b>-from</b> 2009 <b>-until</b> may<br /><i>`(Displays all entries from January 2009 to last may)`</i>",
|
||||
"jrnl A day on the beach with @beth and @frank. Taggidy-tag-tag.",
|
||||
"jrnl <b>--tags</b><br /><i>`@idea 7<br />@beth 5</i>`",
|
||||
"jrnl <b>-from</b> 2019 <b>-until</b> may<br /><i>`(displays all entries from January 2019 to last May)`</i>",
|
||||
"jrnl A day on the beach with @beth and @frank. Tagging them so I can easily look this up later.",
|
||||
"jrnl <b>--tags</b><br /><i>`@frank 7<br />@beth 5</i>`",
|
||||
"jrnl <b>--format</b> json<br /><i>`(Outputs your entire journal as json)</i>`",
|
||||
"jrnl <b>--encrypt</b><br /><i>`(AES encryption. Crack this, NSA)</i>`"
|
||||
"jrnl <b>--encrypt</b><br /><i>`(AES encryption. Don't lose your password!)</i>`"
|
||||
],
|
||||
typeSpeed: 35,
|
||||
backSpeed: 15,
|
||||
backDelay: 2000,
|
||||
typeSpeed: 20, // less is faster
|
||||
backSpeed: 10,
|
||||
backDelay: 2500,
|
||||
loop: true,
|
||||
showCursor: false
|
||||
});
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{%- block search_button %}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
mkdocs==1.1
|
||||
mkdocs>=1.4
|
||||
jinja2==3.1.6
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<!--
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
-->
|
||||
|
||||
{% extends "main.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
Feature: Reading and writing to journal with custom date formats
|
||||
|
||||
Scenario: Dates can include a time
|
||||
# https://github.com/jrnl-org/jrnl/issues/117
|
||||
Given we use the config "simple.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: Dates can be in the future
|
||||
# https://github.com/jrnl-org/jrnl/issues/185
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl 26/06/2099: Planet? Earth. Year? 2099."
|
||||
Then we should see the message "Entry added"
|
||||
And the journal should contain "[2099-06-26 09:00] Planet?"
|
||||
|
||||
Scenario: Loading a sample journal with custom date
|
||||
Given we use the config "little_endian_dates.yaml"
|
||||
When we run "jrnl -n 2"
|
||||
Then we should get no error
|
||||
And the output should be
|
||||
"""
|
||||
09.06.2013 15:39 My first entry.
|
||||
| Everything is alright
|
||||
|
||||
10.07.2013 15:40 Life is good.
|
||||
| But I'm better.
|
||||
"""
|
||||
|
||||
Scenario Outline: Writing an entry from command line with custom date
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl <input>"
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should contain "<output>"
|
||||
|
||||
Examples: Day-first Dates
|
||||
| config | input | output |
|
||||
| little_endian_dates | 2020-09-19: My first entry. | 19.09.2020 09:00 My first entry. |
|
||||
| little_endian_dates | 2020-08-09: My second entry. | 09.08.2020 09:00 My second entry. |
|
||||
| little_endian_dates | 2020-02-29: Test. | 29.02.2020 09:00 Test. |
|
||||
| little_endian_dates | 2019-02-29: Test. | 2019-02-29: Test. |
|
||||
| little_endian_dates | 2020-08-32: Test. | 2020-08-32: Test. |
|
||||
| little_endian_dates | 2032-02-01: Test. | 01.02.2032 09:00 Test. |
|
||||
| little_endian_dates | 2020-01-01: Test. | 01.01.2020 09:00 Test. |
|
||||
| little_endian_dates | 2020-12-31: Test. | 31.12.2020 09:00 Test. |
|
||||
|
||||
Scenario Outline: Searching for dates with custom date
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -on '<input>' --short"
|
||||
Then the output should be "<output>"
|
||||
|
||||
Examples: Day-first Dates
|
||||
| config | input | output |
|
||||
| little_endian_dates | 2013-07-10 | 10.07.2013 15:40 Life is good. |
|
||||
| little_endian_dates | june 9 2013 | 09.06.2013 15:39 My first entry. |
|
||||
| little_endian_dates | july 10 2013 | 10.07.2013 15:40 Life is good. |
|
||||
| little_endian_dates | june 2013 | 09.06.2013 15:39 My first entry. |
|
||||
| little_endian_dates | july 2013 | 10.07.2013 15:40 Life is good. |
|
||||
# @todo month alone with no year should work
|
||||
# | little_endian_dates | june | 09.06.2013 15:39 My first entry. |
|
||||
# | little_endian_dates | july | 10.07.2013 15:40 Life is good. |
|
||||
|
||||
Scenario: Writing an entry at the prompt with custom date
|
||||
Given we use the config "little_endian_dates.yaml"
|
||||
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
|
||||
Then we should get no error
|
||||
And the journal should contain "[10.05.2013 09:00] I saw Elvis."
|
||||
And the journal should contain "He's alive."
|
||||
|
||||
Scenario: Viewing today's entries does not print the entire journal
|
||||
# https://github.com/jrnl-org/jrnl/issues/741
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl -on today"
|
||||
Then the output should not contain "Life is good"
|
||||
And the output should not contain "But I'm better."
|
||||
|
||||
Scenario Outline: Create entry using day of the week as entry date.
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl <day>: This is an entry on a <day>."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should contain "<day> at 9am" in the local time
|
||||
And the output should contain "This is an entry on a <day>."
|
||||
|
||||
Examples: Days of the week
|
||||
| day |
|
||||
| Monday |
|
||||
| Tuesday |
|
||||
| Wednesday |
|
||||
| Thursday |
|
||||
| Friday |
|
||||
| Saturday |
|
||||
| Sunday |
|
||||
| sunday |
|
||||
| sUndAy |
|
||||
|
||||
Scenario Outline: Create entry using day of the week abbreviations as entry date.
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl <day>: This is an entry on a <weekday>."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should contain "<weekday> at 9am" in the local time
|
||||
|
||||
Examples: Days of the week
|
||||
| day | weekday |
|
||||
| mon | Monday |
|
||||
| tue | Tuesday |
|
||||
| wed | Wednesday |
|
||||
| thu | Thursday |
|
||||
| fri | Friday |
|
||||
| sat | Saturday |
|
||||
| sun | Sunday |
|
||||
|
||||
Scenario: Journals with unreadable dates should still be loaded
|
||||
Given we use the config "unreadabledates.yaml"
|
||||
When we run "jrnl -2"
|
||||
Then the output should contain "I've lost track of time."
|
||||
And the output should contain "Time has no meaning."
|
||||
|
||||
Scenario: Journals with readable dates AND unreadable dates should still contain all data.
|
||||
Given we use the config "mostlyreadabledates.yaml"
|
||||
When we run "jrnl -3"
|
||||
Then the output should contain "Time machines are possible."
|
||||
Then the output should contain "I'm going to activate the machine."
|
||||
And the output should contain "I've crossed so many timelines. Is there any going back?"
|
||||
And the journal should have 3 entries
|
||||
|
||||
Scenario: Update near-valid dates after journal is edited
|
||||
Given we use the config "mostlyreadabledates.yaml"
|
||||
When we run "jrnl 2222-08-19: I have made it exactly one month into the future."
|
||||
Then the journal should contain "[2019-07-01 14:23] Entry subject"
|
||||
|
||||
Scenario: Integers in square brackets should not be read as dates
|
||||
Given we use the config "brackets.yaml"
|
||||
When we run "jrnl -1"
|
||||
Then the output should contain "[1] line starting with 1"
|
||||
|
||||
# broken still
|
||||
@skip
|
||||
Scenario: Dayone entries without timezone information are interpreted in 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: Loading entry with ambiguous time stamp in timezone-aware journal (like Dayone)
|
||||
#https://github.com/jrnl-org/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.
|
||||
"""
|
|
@ -1,229 +0,0 @@
|
|||
Feature: Delete entries from journal
|
||||
Scenario Outline: Delete flag allows deletion of single entry
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -1"
|
||||
Then the output should contain "2020-09-24 09:14 The third entry finally"
|
||||
When we run "jrnl --delete" and enter
|
||||
"""
|
||||
N
|
||||
N
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Backing out of interactive delete does not change journal
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -n 1" and enter
|
||||
"""
|
||||
N
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
|
||||
Scenario Outline: Delete flag with nonsense input deletes nothing (issue #932)
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete asdfasdf"
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Delete flag with tag only deletes tagged entries
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete @ipsum" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
|
||||
Scenario Outline: Delete flag with multiple tags deletes all entries matching any of the tags
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete @ipsum @tagthree" and enter
|
||||
"""
|
||||
Y
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Delete flag with -and deletes boolean AND of tagged entries
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -and @tagone @tagtwo" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Delete flag with -not does not delete entries from given tag
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete @tagone -not @ipsum" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Delete flag with -from search operator only deletes entries since that date
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -from 2020-09-01" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Delete flag with -to only deletes entries up to specified date
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -to 2020-08-31" and enter
|
||||
"""
|
||||
Y
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
|
||||
Scenario Outline: Delete flag with -starred only deletes starred entries
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -starred" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Delete flag with -contains only entries containing expression
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --delete -contains dignissim" and enter
|
||||
"""
|
||||
Y
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -99 --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
2020-09-24 09:14 The third entry finally after weeks without writing.
|
||||
"""
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
Feature: Encrypting and decrypting journals
|
||||
|
||||
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"
|
||||
And we should see the message "Journal decrypted"
|
||||
And the journal should have 2 entries
|
||||
|
||||
@todo
|
||||
Scenario: Trying to decrypt an already unencrypted journal
|
||||
# This should warn the user that the journal is already encrypted
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl --decrypt"
|
||||
Then the config for journal "default" should have "encrypt" set to "bool:False"
|
||||
And the journal should have 2 entries
|
||||
|
||||
@todo
|
||||
Scenario: Trying to encrypt an already encrypted journal
|
||||
# This should warn the user that the journal is already encrypted
|
||||
|
||||
Scenario: Encrypting a journal
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl --encrypt" and enter
|
||||
"""
|
||||
swordfish
|
||||
swordfish
|
||||
n
|
||||
"""
|
||||
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 be prompted for a password
|
||||
And the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from jrnl.os_compat import on_windows
|
||||
|
||||
CWD = os.getcwd()
|
||||
|
||||
# @see https://behave.readthedocs.io/en/latest/tutorial.html#debug-on-error-in-case-of-step-failures
|
||||
BEHAVE_DEBUG_ON_ERROR = False
|
||||
|
||||
|
||||
def setup_debug_on_error(userdata):
|
||||
global BEHAVE_DEBUG_ON_ERROR
|
||||
BEHAVE_DEBUG_ON_ERROR = userdata.getbool("BEHAVE_DEBUG_ON_ERROR")
|
||||
|
||||
|
||||
def before_all(context):
|
||||
setup_debug_on_error(context.config.userdata)
|
||||
|
||||
|
||||
# def after_step(context, step):
|
||||
# if BEHAVE_DEBUG_ON_ERROR and step.status == "failed":
|
||||
# -- ENTER DEBUGGER: Zoom in on failure location.
|
||||
# NOTE: Use IPython debugger, same for pdb (basic python debugger).
|
||||
# import ipdb
|
||||
# ipdb.post_mortem(step.exc_traceback)
|
||||
|
||||
|
||||
def clean_all_working_dirs():
|
||||
if os.path.exists("test.txt"):
|
||||
os.remove("test.txt")
|
||||
for folder in ("configs", "journals", "cache"):
|
||||
working_dir = os.path.join("features", folder)
|
||||
if os.path.exists(working_dir):
|
||||
shutil.rmtree(working_dir)
|
||||
|
||||
|
||||
def before_feature(context, feature):
|
||||
# add "skip" tag
|
||||
# https://stackoverflow.com/a/42721605/4276230
|
||||
if "skip" in feature.tags:
|
||||
feature.skip()
|
||||
return
|
||||
|
||||
if "skip_win" in feature.tags and on_windows():
|
||||
feature.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
||||
def before_scenario(context, scenario):
|
||||
"""Before each scenario, backup all config and journal test data."""
|
||||
# Clean up in case something went wrong
|
||||
clean_all_working_dirs()
|
||||
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)
|
||||
|
||||
# add "skip" tag
|
||||
# https://stackoverflow.com/a/42721605/4276230
|
||||
if "skip" in scenario.effective_tags:
|
||||
scenario.skip()
|
||||
return
|
||||
|
||||
if "skip_win" in scenario.effective_tags and on_windows():
|
||||
scenario.skip("Skipping on Windows")
|
||||
return
|
||||
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
"""After each scenario, restore all test data and remove working_dirs."""
|
||||
if os.getcwd() != CWD:
|
||||
os.chdir(CWD)
|
||||
|
||||
# only clean up if debugging is off and the scenario passed
|
||||
if BEHAVE_DEBUG_ON_ERROR and scenario.status != "failed":
|
||||
clean_all_working_dirs()
|
||||
else:
|
||||
clean_all_working_dirs()
|
|
@ -1,56 +0,0 @@
|
|||
Feature: Journals iteracting with the file system in a way that users can see
|
||||
|
||||
Scenario: Adding entries to a Folder journal should generate date files
|
||||
Given we use the config "empty_folder.yaml"
|
||||
When we run "jrnl 23 July 2013: Testing folder journal."
|
||||
Then we should see the message "Entry added"
|
||||
When the journal directory is listed
|
||||
Then the output should contain "2013/07/23.txt" or "2013\07\23.txt"
|
||||
|
||||
Scenario: Adding multiple entries to a Folder journal should generate multiple date files
|
||||
Given we use the config "empty_folder.yaml"
|
||||
When we run "jrnl 23 July 2013: Testing folder journal."
|
||||
And we run "jrnl 3/7/2014: Second entry of journal."
|
||||
Then we should see the message "Entry added"
|
||||
When the journal directory is listed
|
||||
Then the output should contain "2013/07/23.txt" or "2013\07\23.txt"
|
||||
Then the output should contain "2014/03/07.txt" or "2014\03\07.txt"
|
||||
|
||||
Scenario: If the journal and its parent directory don't exist, they should be created
|
||||
Given we use the config "missing_directory.yaml"
|
||||
Then the journal should not exist
|
||||
When we run "jrnl This is a new entry in my journal"
|
||||
Then the journal should exist
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should contain "This is a new entry in my journal"
|
||||
And the journal should have 1 entry
|
||||
|
||||
Scenario: If the journal file doesn't exist, then it should be created
|
||||
Given we use the config "missing_journal.yaml"
|
||||
Then the journal should not exist
|
||||
When we run "jrnl This is a new entry in my journal"
|
||||
Then the journal should exist
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should contain "This is a new entry in my journal"
|
||||
And the journal should have 1 entry
|
||||
|
||||
Scenario: Creating journal with relative path should update to absolute path
|
||||
Given we use the config "missingconfig"
|
||||
When we run "jrnl hello world" and enter
|
||||
"""
|
||||
test.txt
|
||||
n
|
||||
"""
|
||||
And we change directory to "features"
|
||||
And we run "jrnl -n 1"
|
||||
Then the output should contain "hello world"
|
||||
|
||||
Scenario: the temporary filename suffix should default to ".jrnl"
|
||||
Given we use the config "editor.yaml"
|
||||
When we run "jrnl --edit"
|
||||
Then the temporary filename suffix should be ".jrnl"
|
||||
|
||||
Scenario: the temporary filename suffix should be "-{template_filename}"
|
||||
Given we use the config "editor_markdown_extension.yaml"
|
||||
When we run "jrnl --edit"
|
||||
Then the temporary filename suffix should be "-extension.md"
|
|
@ -1,579 +0,0 @@
|
|||
Feature: Custom formats
|
||||
|
||||
Scenario Outline: JSON format
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --format json"
|
||||
Then we should get no error
|
||||
And the output should be parsable as json
|
||||
And "entries" in the json output should have 3 elements
|
||||
And "tags" in the json output should contain "@ipsum"
|
||||
And "tags" in the json output should contain "@tagone"
|
||||
And "tags" in the json output should contain "@tagthree"
|
||||
And "tags" in the json output should contain "@tagtwo"
|
||||
And entry 1 should have an array "tags" with 3 elements
|
||||
And entry 2 should have an array "tags" with 1 elements
|
||||
And entry 3 should have an array "tags" with 2 elements
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
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 Outline: Printing a journal that has multiline entries with tags
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -n 1 @ipsum"
|
||||
Then we should get no error
|
||||
And the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
| Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
|
||||
| quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus
|
||||
| pellentesque
|
||||
| augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu
|
||||
| consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In
|
||||
| commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget
|
||||
| venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo.
|
||||
|
|
||||
| Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo
|
||||
| ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. Suspendisse
|
||||
| potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas eget
|
||||
| molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed lectus
|
||||
| hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis
|
||||
| feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, vestibulum
|
||||
| urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod enim.
|
||||
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget
|
||||
| velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac
|
||||
| porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per
|
||||
| conubia nostra, per inceptos himenaeos.
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Exporting using filters should only export parts of the journal
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -until 'August 2020' --format json"
|
||||
Then the output should be parsable as 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 "@ipsum"
|
||||
And "tags" in the json output should contain "@tagone"
|
||||
And "tags" in the json output should contain "@tagtwo"
|
||||
And entry 1 should have an array "tags" with 3 elements
|
||||
And entry 2 should have an array "tags" with 1 elements
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Exporting using custom templates
|
||||
Given we use the config "<config>.yaml"
|
||||
And we load template "sample.template"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -1 --format sample"
|
||||
Then the output should be
|
||||
"""
|
||||
The third entry finally after weeks without writing.
|
||||
----------------------------------------------------
|
||||
|
||||
I'm so excited about emojis. 💯 🎶 💩
|
||||
|
||||
Donec semper pellentesque iaculis. Nullam cursus et justo sit amet venenatis.
|
||||
Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. Nulla
|
||||
eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis
|
||||
dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh malesuada.
|
||||
Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis
|
||||
vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan justo.
|
||||
Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at
|
||||
ante eget fringilla. @tagthree and also @tagone
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Increasing Headings on Markdown export
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we open the editor and append
|
||||
"""
|
||||
[2020-10-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
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -1 --export markdown"
|
||||
Then the output should be
|
||||
"""
|
||||
# 2020
|
||||
|
||||
## October
|
||||
|
||||
### 2020-10-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
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: Add a blank line to Markdown export if there isn't one already
|
||||
# https://github.com/jrnl-org/jrnl/issues/768
|
||||
# https://github.com/jrnl-org/jrnl/issues/881
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we open the editor and append
|
||||
"""
|
||||
[2020-10-29 11:11] First entry.
|
||||
[2020-10-29 11:11] Second entry.
|
||||
[2020-10-29 11:13] Third entry.
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -3 --format markdown"
|
||||
Then the output should be
|
||||
"""
|
||||
# 2020
|
||||
|
||||
## October
|
||||
|
||||
### 2020-10-29 11:11 First entry.
|
||||
|
||||
|
||||
### 2020-10-29 11:11 Second entry.
|
||||
|
||||
|
||||
### 2020-10-29 11:13 Third entry.
|
||||
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
# | basic_dayone | @todo
|
||||
|
||||
@skip
|
||||
Scenario Outline: Exporting to XML
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --export xml"
|
||||
Then the output should be a valid XML string
|
||||
And "entries" node in the xml output should have 3 elements
|
||||
And "tags" in the xml output should contain ["@ipsum", "@tagone", "@tagtwo", "@tagthree"]
|
||||
And there should be 10 "tag" elements
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
# | basic_onefile | @todo
|
||||
# | basic_encrypted | @todo
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario: Exporting to XML
|
||||
Given we use the config "tags.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --export xml"
|
||||
Then the output should be a valid XML string
|
||||
And "entries" node in the xml output should have 2 elements
|
||||
And "tags" in the xml output should contain ["@idea", "@journal", "@dan"]
|
||||
And there should be 7 "tag" elements
|
||||
|
||||
Scenario Outline: Exporting tags
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --export tags"
|
||||
Then the output should be
|
||||
"""
|
||||
@tagtwo : 2
|
||||
@tagone : 2
|
||||
@tagthree : 1
|
||||
@ipsum : 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
@todo
|
||||
Scenario Outline: Exporting fancy
|
||||
# Needs better emoji support
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --export fancy"
|
||||
Then the output should be
|
||||
"""
|
||||
┎──────────────────────────────────────────────────────────────╮2020-08-29 11:11
|
||||
┃ Entry the first. ╘═══════════════╕
|
||||
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
┃ Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada │
|
||||
┃ quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus │
|
||||
┃ pellentesque │
|
||||
┃ augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu │
|
||||
┃ consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In │
|
||||
┃ commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget │
|
||||
┃ venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo. │
|
||||
┃ │
|
||||
┃ Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo │
|
||||
┃ ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. │
|
||||
┃ Suspendisse │
|
||||
┃ potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas │
|
||||
┃ eget │
|
||||
┃ molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed │
|
||||
┃ lectus │
|
||||
┃ hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis │
|
||||
┃ feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, │
|
||||
┃ vestibulum │
|
||||
┃ urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod │
|
||||
┃ enim. │
|
||||
┃ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget │
|
||||
┃ velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac │
|
||||
┃ porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per │
|
||||
┃ conubia nostra, per inceptos himenaeos. │
|
||||
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||
┎──────────────────────────────────────────────────────────────╮2020-08-31 14:32
|
||||
┃ A second entry in what I hope to be a long series. ╘═══════════════╕
|
||||
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
┃ Sed sit amet metus et sapien feugiat elementum. Aliquam bibendum lobortis │
|
||||
┃ leo │
|
||||
┃ vitae tempus. Donec eleifend nec mi non volutpat. Lorem ipsum dolor sit │
|
||||
┃ amet, │
|
||||
┃ consectetur adipiscing elit. Praesent ut sodales libero. Maecenas nisl │
|
||||
┃ lorem, │
|
||||
┃ vestibulum in tempus sit amet, fermentum ut arcu. Donec vel vestibulum │
|
||||
┃ lectus, │
|
||||
┃ eget pretium enim. Maecenas diam nunc, imperdiet vitae pharetra sed, pretium │
|
||||
┃ id │
|
||||
┃ lectus. Donec eu metus et turpis tempor tristique ac non ex. In tellus arcu, │
|
||||
┃ egestas at efficitur et, ultrices vel est. Sed commodo et nibh non │
|
||||
┃ elementum. │
|
||||
┃ Mauris tempus vitae neque vel viverra. @tagtwo all by its lonesome. │
|
||||
┃ │
|
||||
┃ Nulla mattis elementum magna, viverra pretium dui fermentum et. Cras vel │
|
||||
┃ vestibulum odio. Quisque sit amet turpis et urna finibus maximus. Interdum │
|
||||
┃ et │
|
||||
┃ malesuada fames ac ante ipsum primis in faucibus. Fusce porttitor iaculis │
|
||||
┃ sem, │
|
||||
┃ non dictum ipsum varius nec. Nulla eu erat at risus gravida blandit non vel │
|
||||
┃ ante. Nam egestas ipsum leo, eu ultricies ipsum tincidunt vel. Morbi a │
|
||||
┃ commodo │
|
||||
┃ eros. │
|
||||
┃ │
|
||||
┃ Nullam dictum, nisl ac varius tempus, ex tortor fermentum nisl, non │
|
||||
┃ tempus dolor neque a lorem. Suspendisse a faucibus ex, vel ornare tortor. │
|
||||
┃ Maecenas tincidunt id felis quis semper. Pellentesque enim libero, fermentum │
|
||||
┃ quis metus id, rhoncus euismod magna. Nulla finibus velit eu purus bibendum │
|
||||
┃ interdum. Integer id justo dui. Integer eu tellus in turpis bibendum │
|
||||
┃ blandit. │
|
||||
┃ Quisque auctor lacinia consectetur. │
|
||||
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||
┎──────────────────────────────────────────────────────────────╮2020-09-24 09:14
|
||||
┃ The third entry finally after weeks without writing. ╘═══════════════╕
|
||||
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
┃ I'm so excited about emojis. 💯 🎶 💩 │
|
||||
┃ │
|
||||
┃ Donec semper pellentesque iaculis. Nullam cursus et justo sit amet │
|
||||
┃ venenatis. │
|
||||
┃ Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. │
|
||||
┃ Nulla │
|
||||
┃ eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis │
|
||||
┃ dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh │
|
||||
┃ malesuada. │
|
||||
┃ Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis │
|
||||
┃ vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan │
|
||||
┃ justo. │
|
||||
┃ Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at │
|
||||
┃ ante eget fringilla. @tagthree and also @tagone │
|
||||
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
@skip_win
|
||||
Scenario Outline: Export to yaml
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
And we create a cache directory
|
||||
When we run "jrnl --export yaml -o {cache_dir}"
|
||||
Then the cache should contain the files
|
||||
"""
|
||||
2020-08-29_entry-the-first.md
|
||||
2020-08-31_a-second-entry-in-what-i-hope-to-be-a-long-series.md
|
||||
2020-09-24_the-third-entry-finally-after-weeks-without-writing.md
|
||||
"""
|
||||
And the content of file "2020-08-29_entry-the-first.md" in the cache should be
|
||||
"""
|
||||
---
|
||||
title: Entry the first.
|
||||
date: 2020-08-29 11:11
|
||||
starred: False
|
||||
tags: tagone, ipsum, tagtwo
|
||||
body: |
|
||||
Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
|
||||
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
|
||||
augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu
|
||||
consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In
|
||||
commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget
|
||||
venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo.
|
||||
|
||||
Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo
|
||||
ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. Suspendisse
|
||||
potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas eget
|
||||
molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed lectus
|
||||
hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis
|
||||
feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, vestibulum
|
||||
urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod enim.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget
|
||||
velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac
|
||||
porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per
|
||||
conubia nostra, per inceptos himenaeos.
|
||||
...
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
# | basic_dayone |
|
||||
|
||||
@skip_win # @todo YAML exporter does not correctly export emoji on Windows
|
||||
Scenario Outline: Add a blank line to YAML export if there isn't one already
|
||||
# https://github.com/jrnl-org/jrnl/issues/768
|
||||
# https://github.com/jrnl-org/jrnl/issues/881
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
And we create a cache directory
|
||||
When we run "jrnl --export yaml -o {cache_dir}"
|
||||
Then the cache should contain the files
|
||||
"""
|
||||
2020-08-29_entry-the-first.md
|
||||
2020-08-31_a-second-entry-in-what-i-hope-to-be-a-long-series.md
|
||||
2020-09-24_the-third-entry-finally-after-weeks-without-writing.md
|
||||
"""
|
||||
And the content of file "2020-09-24_the-third-entry-finally-after-weeks-without-writing.md" in the cache should be
|
||||
"""
|
||||
---
|
||||
title: The third entry finally after weeks without writing.
|
||||
date: 2020-09-24 09:14
|
||||
starred: False
|
||||
tags: tagone, tagthree
|
||||
body: |
|
||||
I'm so excited about emojis. 💯 🎶 💩
|
||||
|
||||
Donec semper pellentesque iaculis. Nullam cursus et justo sit amet venenatis.
|
||||
Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. Nulla
|
||||
eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis
|
||||
dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh malesuada.
|
||||
Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis
|
||||
vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan justo.
|
||||
Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at
|
||||
ante eget fringilla. @tagthree and also @tagone
|
||||
...
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario: Empty DayOne entry bodies should not error
|
||||
# https://github.com/jrnl-org/jrnl/issues/780
|
||||
Given we use the config "bug780.yaml"
|
||||
When we run "jrnl --short"
|
||||
Then we should get no error
|
||||
|
||||
Scenario Outline: --short displays the short version of entries (only the title)
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -on 2020-08-31 --short"
|
||||
Then the output should be "2020-08-31 14:32 A second entry in what I hope to be a long series."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: -s displays the short version of entries (only the title)
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -on 2020-08-31 -s"
|
||||
Then the output should be "2020-08-31 14:32 A second entry in what I hope to be a long series."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario: Markdown Support from config file
|
||||
Given we use the config "format_md.yaml"
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should be
|
||||
"""
|
||||
# 2013
|
||||
|
||||
## June
|
||||
|
||||
### 2013-06-10 15:40 Life is good.
|
||||
|
||||
But I'm better.
|
||||
"""
|
||||
|
||||
Scenario: Text Formatter from config file
|
||||
Given we use the config "format_text.yaml"
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should be
|
||||
"""
|
||||
[2013-06-10 15:40] Life is good.
|
||||
But I'm better.
|
||||
"""
|
||||
|
||||
Scenario Outline: Exporting entries with Cyrillic characters to directory should not fail
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
And we create a cache directory
|
||||
When we run "jrnl 2020-11-21: Первая"
|
||||
When we run "jrnl --format md --file {cache_dir} -on 2020-11-21"
|
||||
Then the cache should contain the files
|
||||
"""
|
||||
2020-11-21_первая.md
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Export date counts
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl 2020-08-31 01:01: Hi."
|
||||
And we run "jrnl --format dates"
|
||||
Then the output should be
|
||||
"""
|
||||
2020-08-29, 1
|
||||
2020-08-31, 2
|
||||
2020-09-24, 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
|
@ -1,93 +0,0 @@
|
|||
Feature: Importing data
|
||||
|
||||
Scenario Outline: --import allows new entry from stdin
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --import" and pipe "[2020-07-05 15:00] Observe and import."
|
||||
Then we flush the output
|
||||
When we run "jrnl -c import"
|
||||
Then the output should contain "Observe and import"
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: --import allows new large entry from stdin
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --import" and pipe
|
||||
"""
|
||||
[2020-07-05 15:00] Observe and import.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada quis
|
||||
est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque augue
|
||||
et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu consequat.
|
||||
Aenean ante ex, elementum ut interdum et, mattis eget lacus. In commodo nulla nec
|
||||
tellus placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at
|
||||
dolor dui end of entry.
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -on 2020-07-05"
|
||||
Then the output should contain "2020-07-05 15:00 Observe and import."
|
||||
And the output should contain "Lorem ipsum"
|
||||
And the output should contain "end of entry."
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario Outline: --import allows multiple new entries from stdin
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl --import" and pipe
|
||||
"""
|
||||
[2020-07-05 15:00] Observe and import.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
|
||||
[2020-07-05 15:01] Twice as nice.
|
||||
Sed dignissim sed nisl eu consequat.
|
||||
"""
|
||||
Then we flush the output
|
||||
When we run "jrnl -on 2020-07-05"
|
||||
Then the output should contain "2020-07-05 15:00 Observe and import."
|
||||
And the output should contain "Lorem ipsum"
|
||||
And the output should contain "2020-07-05 15:01 Twice as nice."
|
||||
And the output should contain "Sed dignissim"
|
||||
|
||||
Examples: Configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
# | basic_folder | @todo
|
||||
# | basic_dayone | @todo
|
||||
|
||||
Scenario: --import allows import new entries from file
|
||||
Given we use the config "simple.yaml"
|
||||
Then the journal should contain "My first entry."
|
||||
And the journal should contain "Life is good."
|
||||
But the journal should not contain "I have an @idea"
|
||||
And the journal should not contain "I met with"
|
||||
When we run "jrnl --import --file features/journals/tags.journal"
|
||||
Then the journal should contain "My first entry."
|
||||
And the journal should contain "Life is good."
|
||||
And the journal should contain "PROFIT!"
|
||||
|
||||
Scenario: --import prioritizes --file over pipe data if both are given
|
||||
Given we use the config "simple.yaml"
|
||||
Then the journal should contain "My first entry."
|
||||
And the journal should contain "Life is good."
|
||||
But the journal should not contain "I have an @idea"
|
||||
And the journal should not contain "I met with"
|
||||
When we run "jrnl --import --file features/journals/tags.journal" and pipe
|
||||
"""
|
||||
[2020-07-05 15:00] I should not exist!
|
||||
"""
|
||||
Then the journal should contain "My first entry."
|
||||
And the journal should contain "PROFIT!"
|
||||
But the journal should not contain "I should not exist!"
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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: Write to specified journal without a timestamp but with colon
|
||||
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
|
||||
And journal "work" should contain "a long day in the office"
|
||||
|
||||
Scenario: Write to specified journal without a timestamp but with colon
|
||||
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
|
||||
And journal "work" should contain "a long day in the office"
|
||||
|
||||
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"
|
||||
|
||||
Scenario: Don't crash if no file exists for a configured encrypted journal
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl new_encrypted Adding first entry" and enter
|
||||
"""
|
||||
these three eyes
|
||||
these three eyes
|
||||
n
|
||||
"""
|
||||
Then we should see the message "Encrypted journal 'new_encrypted' created"
|
|
@ -1,318 +0,0 @@
|
|||
Feature: Searching in a journal
|
||||
|
||||
Scenario Outline: Displaying entries using -on today should display entries created today
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl today: Adding an entry right now."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -on today"
|
||||
Then the output should contain "Adding an entry right now."
|
||||
But the output should not contain "Everything is alright"
|
||||
And the output should not contain "Life is good"
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
|
||||
Scenario Outline: Displaying entries using -from day should display correct entries
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl yesterday: This thing happened yesterday"
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl today at 11:59pm: Adding an entry right now."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl tomorrow: A future entry."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -from today"
|
||||
Then the output should contain "Adding an entry right now."
|
||||
And the output should contain "A future entry."
|
||||
And the output should not contain "This thing happened yesterday"
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
|
||||
Scenario Outline: Displaying entries using -from and -to day should display correct entries
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl yesterday: This thing happened yesterday"
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl today at 11:59pm: Adding an entry right now."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl tomorrow: A future entry."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -from yesterday -to today"
|
||||
Then the output should contain "This thing happened yesterday"
|
||||
And the output should contain "Adding an entry right now."
|
||||
And the output should not contain "A future entry."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
|
||||
Scenario Outline: Searching for a string
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -contains first --short"
|
||||
Then we should get no error
|
||||
And the output should be
|
||||
"""
|
||||
2020-08-29 11:11 Entry the first.
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching for a string within tag results
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl @tagone -contains maybe"
|
||||
Then we should get no error
|
||||
And the output should contain "maybe"
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching for a string within AND tag results
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -and @tagone @tagtwo -contains maybe"
|
||||
Then we should get no error
|
||||
And the output should contain "maybe"
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching for a string within NOT tag results
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -not @tagone -contains lonesome"
|
||||
Then we should get no error
|
||||
And the output should contain "lonesome"
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching for dates
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -on 2020-08-31 --short"
|
||||
Then the output should be "2020-08-31 14:32 A second entry in what I hope to be a long series."
|
||||
Then we flush the output
|
||||
When we run "jrnl -on 'august 31 2020' --short"
|
||||
Then the output should be "2020-08-31 14:32 A second entry in what I hope to be a long series."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario: Out of order entries to a Folder journal should be listed in date order
|
||||
Given we use the config "empty_folder.yaml"
|
||||
When we run "jrnl 3/7/2014 4:37pm: Second entry of journal."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl 23 July 2013: Testing folder journal."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -2"
|
||||
Then the output should be
|
||||
"""
|
||||
2013-07-23 09:00 Testing folder journal.
|
||||
|
||||
2014-03-07 16:37 Second entry of journal.
|
||||
"""
|
||||
|
||||
Scenario Outline: Searching for all tags should show counts of each tag
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
And the output should be
|
||||
"""
|
||||
@tagtwo : 2
|
||||
@tagone : 2
|
||||
@tagthree : 1
|
||||
@ipsum : 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Filtering journals should also filter tags
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl -from 'september 2020' --tags"
|
||||
Then we should get no error
|
||||
And the output should be
|
||||
"""
|
||||
@tagthree : 1
|
||||
@tagone : 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Excluding a tag should filter out all entries with that tag
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --tags -not @tagtwo"
|
||||
Then the output should be
|
||||
"""
|
||||
@tagthree : 1
|
||||
@tagone : 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Excluding multiple tags should filter out all entries with those tags
|
||||
Given we use the config "<config>.yaml"
|
||||
When we run "jrnl --tags -not @tagone -not @tagthree"
|
||||
Then the output should be
|
||||
"""
|
||||
@tagtwo : 1
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario: DayOne tag searching should work with tags containing a mixture of upper and lower case.
|
||||
# https://github.com/jrnl-org/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!"
|
||||
|
||||
Scenario: Loading a sample journal
|
||||
Given we use the config "simple.yaml"
|
||||
When we run "jrnl -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 Outline: Searching by month
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -month 9 --short"
|
||||
Then the output should be "2020-09-24 09:14 The third entry finally after weeks without writing."
|
||||
And we flush the output
|
||||
When we run "jrnl -month Sept --short"
|
||||
Then the output should be "2020-09-24 09:14 The third entry finally after weeks without writing."
|
||||
And we flush the output
|
||||
When we run "jrnl -month September --short"
|
||||
Then the output should be "2020-09-24 09:14 The third entry finally after weeks without writing."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching by day
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -day 31 --short"
|
||||
Then the output should be "2020-08-31 14:32 A second entry in what I hope to be a long series."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching by year
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl 2019-01-01 01:01: I like this year."
|
||||
And we run "jrnl -year 2019 --short"
|
||||
Then the output should be "2019-01-01 01:01 I like this year."
|
||||
And we flush the output
|
||||
When we run "jrnl -year 19 --short"
|
||||
Then the output should be "2019-01-01 01:01 I like this year."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Combining month, day, and year search terms
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl -month 08 -day 29 --short"
|
||||
Then the output should be "2020-08-29 11:11 Entry the first."
|
||||
And we flush the output
|
||||
When we run "jrnl -day 29 -year 2020 --short"
|
||||
Then the output should be "2020-08-29 11:11 Entry the first."
|
||||
And we flush the output
|
||||
When we run "jrnl -month 09 -year 2020 --short"
|
||||
Then the output should be "2020-09-24 09:14 The third entry finally after weeks without writing."
|
||||
And we flush the output
|
||||
When we run "jrnl -month 08 -day 29 -year 2020 --short"
|
||||
Then the output should be "2020-08-29 11:11 Entry the first."
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Searching today in history
|
||||
Given we use the config "<config>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
And we set current date and time to "2020-08-31 14:32"
|
||||
When we run "jrnl 2019-08-31 01:01: Hi, from last year."
|
||||
And we run "jrnl -today-in-history --short"
|
||||
Then the output should be
|
||||
"""
|
||||
2019-08-31 01:01 Hi, from last year.
|
||||
2020-08-31 14:32 A second entry in what I hope to be a long series.
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
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!
|
||||
"""
|
|
@ -1,648 +0,0 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given
|
||||
from behave import then
|
||||
from behave import when
|
||||
import keyring
|
||||
import toml
|
||||
import yaml
|
||||
|
||||
import jrnl.time
|
||||
from jrnl import Journal
|
||||
from jrnl import __version__
|
||||
from jrnl import plugins
|
||||
from jrnl.cli import cli
|
||||
from jrnl.config import load_config
|
||||
from jrnl.os_compat import split_args
|
||||
|
||||
try:
|
||||
import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError:
|
||||
import parsedatetime as pdt
|
||||
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring that just stores its values 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):
|
||||
self.keys[servicename][username] = None
|
||||
|
||||
|
||||
class NoKeyring(keyring.backend.KeyringBackend):
|
||||
"""A keyring that simulated an environment with no keyring backend."""
|
||||
|
||||
priority = 2
|
||||
keys = defaultdict(dict)
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
raise keyring.errors.NoKeyringError
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
raise keyring.errors.NoKeyringError
|
||||
|
||||
def delete_password(self, servicename, username):
|
||||
raise keyring.errors.NoKeyringError
|
||||
|
||||
|
||||
class FailedKeyring(keyring.backend.KeyringBackend):
|
||||
"""
|
||||
A keyring that cannot be retrieved.
|
||||
"""
|
||||
|
||||
priority = 2
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
raise keyring.errors.KeyringError
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
raise keyring.errors.KeyringError
|
||||
|
||||
def delete_password(self, servicename, username):
|
||||
raise keyring.errors.KeyringError
|
||||
|
||||
|
||||
# set a default keyring
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
|
||||
def read_journal(context, journal_name="default"):
|
||||
configuration = load_config(context.config_path)
|
||||
with open(configuration["journals"][journal_name]) as journal_file:
|
||||
journal = journal_file.read()
|
||||
return journal
|
||||
|
||||
|
||||
def open_journal(context, journal_name="default"):
|
||||
configuration = load_config(context.config_path)
|
||||
journal_conf = configuration["journals"][journal_name]
|
||||
|
||||
# We can override the default config on a by-journal basis
|
||||
if type(journal_conf) is dict:
|
||||
configuration.update(journal_conf)
|
||||
# But also just give them a string to point to the journal file
|
||||
else:
|
||||
configuration["journal"] = journal_conf
|
||||
|
||||
return Journal.open_journal(journal_name, configuration)
|
||||
|
||||
|
||||
def read_value_from_string(string):
|
||||
if string[0] == "{":
|
||||
# Handle value being a dictionary
|
||||
return ast.literal_eval(string)
|
||||
|
||||
# Takes strings like "bool:true" or "int:32" and coerces them into proper type
|
||||
t, value = string.split(":")
|
||||
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
|
||||
return value
|
||||
|
||||
|
||||
@given('we use the config "{config_file}"')
|
||||
def set_config(context, config_file):
|
||||
full_path = os.path.join("features/configs", config_file)
|
||||
|
||||
context.config_path = os.path.abspath(full_path)
|
||||
|
||||
if config_file.endswith("yaml") and os.path.exists(full_path):
|
||||
# Add jrnl version to file for 2.x journals
|
||||
with open(context.config_path, "a") as cf:
|
||||
cf.write("version: {}".format(__version__))
|
||||
|
||||
|
||||
@given('we use the password "{password}" if prompted')
|
||||
def use_password_forever(context, password):
|
||||
context.password = password
|
||||
|
||||
|
||||
@given('we use the password "{password}" {num:d} times if prompted')
|
||||
def use_password(context, password, num=1):
|
||||
context.password = iter([password] * num)
|
||||
|
||||
|
||||
@given("we have a keyring")
|
||||
@given("we have a {type} keyring")
|
||||
def set_keyring(context, type=""):
|
||||
if type == "failed":
|
||||
keyring.set_keyring(FailedKeyring())
|
||||
else:
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
|
||||
@given("we do not have a keyring")
|
||||
def disable_keyring(context):
|
||||
keyring.core.set_keyring(NoKeyring())
|
||||
|
||||
|
||||
@given('we set current date and time to "{dt}"')
|
||||
def set_datetime(context, dt):
|
||||
context.now = dt
|
||||
|
||||
|
||||
@when('we change directory to "{path}"')
|
||||
def move_up_dir(context, path):
|
||||
os.chdir(path)
|
||||
|
||||
|
||||
@when("we open the editor and {method}")
|
||||
@when('we open the editor and {method} "{text}"')
|
||||
@when("we open the editor and {method} nothing")
|
||||
@when("we open the editor and {method} nothing")
|
||||
def open_editor_and_enter(context, method, text=""):
|
||||
text = text or context.text or ""
|
||||
|
||||
if method == "enter":
|
||||
file_method = "w+"
|
||||
elif method == "append":
|
||||
file_method = "a"
|
||||
else:
|
||||
file_method = "r+"
|
||||
|
||||
def _mock_editor(command):
|
||||
context.editor_command = command
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, file_method) as f:
|
||||
f.write(text)
|
||||
|
||||
return tmpfile
|
||||
|
||||
if "password" in context:
|
||||
password = context.password
|
||||
else:
|
||||
password = ""
|
||||
|
||||
# fmt: off
|
||||
# see: https://github.com/psf/black/issues/664
|
||||
with \
|
||||
patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
|
||||
patch("sys.stdin.isatty", return_value=True), \
|
||||
patch("jrnl.time.parse", side_effect=_mock_time_parse(context)), \
|
||||
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
|
||||
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
|
||||
:
|
||||
context.editor = mock_editor
|
||||
context.getpass = mock_getpass
|
||||
try:
|
||||
cli(["--edit"])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
@then("the editor should have been called")
|
||||
@then("the editor should have been called with {num} arguments")
|
||||
def count_editor_args(context, num=None):
|
||||
assert context.editor.called
|
||||
|
||||
if isinstance(num, int):
|
||||
assert len(context.editor_command) == int(num)
|
||||
|
||||
|
||||
@then("the editor should not have been called")
|
||||
def no_editor_called(context, num=None):
|
||||
assert "editor" not in context or not context.editor.called
|
||||
|
||||
|
||||
@then('one editor argument should be "{arg}"')
|
||||
def contains_editor_arg(context, arg):
|
||||
args = context.editor_command
|
||||
assert (
|
||||
arg in args and args.count(arg) == 1
|
||||
), f"\narg not in args exactly 1 time:\n{arg}\n{str(args)}"
|
||||
|
||||
|
||||
@then('one editor argument should match "{regex}"')
|
||||
def matches_editor_arg(context, regex):
|
||||
args = context.editor_command
|
||||
matches = list(filter(lambda x: re.search(regex, x), args))
|
||||
assert (
|
||||
len(matches) == 1
|
||||
), f"\nRegex didn't match exactly 1 time:\n{regex}\n{str(args)}"
|
||||
|
||||
|
||||
@then("the editor file content should {method}")
|
||||
@then("the editor file content should {method} empty")
|
||||
@then('the editor file content should {method} "{text}"')
|
||||
def contains_editor_file(context, method, text=""):
|
||||
text = text or context.text or ""
|
||||
content = context.editor_file.get("content")
|
||||
format = f'\n"""\n{content}\n"""\n'
|
||||
if method == "be":
|
||||
assert content == text, format
|
||||
elif method == "contain":
|
||||
assert text in content, format
|
||||
else:
|
||||
assert False, f"Method '{method}' not supported"
|
||||
|
||||
|
||||
@then('the temporary filename suffix should be "{suffix}"')
|
||||
def extension_editor_file(context, suffix):
|
||||
filename = Path(context.editor_file["name"]).name
|
||||
delimiter = "-" if "-" in filename else "."
|
||||
filename_suffix = delimiter + filename.split(delimiter)[-1]
|
||||
assert filename_suffix == suffix
|
||||
|
||||
|
||||
def _mock_getpass(inputs):
|
||||
def prompt_return(prompt=""):
|
||||
if type(inputs) == str:
|
||||
return inputs
|
||||
try:
|
||||
return next(inputs)
|
||||
except StopIteration:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
def _mock_input(inputs):
|
||||
def prompt_return(prompt=""):
|
||||
try:
|
||||
val = next(inputs)
|
||||
print(prompt, val)
|
||||
return val
|
||||
except StopIteration:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
def _mock_time_parse(context):
|
||||
original_parse = jrnl.time.parse
|
||||
if "now" not in context:
|
||||
return original_parse
|
||||
|
||||
def wrapper(input, *args, **kwargs):
|
||||
input = context.now if input == "now" else input
|
||||
return original_parse(input, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@when('we run "{command}" and enter')
|
||||
@when('we run "{command}" and enter nothing')
|
||||
@when('we run "{command}" and enter "{inputs}"')
|
||||
def run_with_input(context, command, inputs=""):
|
||||
# create an iterator through all inputs. These inputs will be fed one by one
|
||||
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
|
||||
if context.text:
|
||||
text = iter(context.text.split("\n"))
|
||||
else:
|
||||
text = iter([inputs])
|
||||
|
||||
args = split_args(command)[1:]
|
||||
|
||||
def _mock_editor(command):
|
||||
context.editor_command = command
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, "r") as editor_file:
|
||||
file_content = editor_file.read()
|
||||
context.editor_file = {"name": tmpfile, "content": file_content}
|
||||
Path(tmpfile).touch()
|
||||
|
||||
if "password" in context:
|
||||
password = context.password
|
||||
else:
|
||||
password = text
|
||||
|
||||
# fmt: off
|
||||
# see: https://github.com/psf/black/issues/664
|
||||
with \
|
||||
patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
|
||||
patch("sys.stdin.read", side_effect=text) as mock_read, \
|
||||
patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
|
||||
patch("jrnl.time.parse", side_effect=_mock_time_parse(context)), \
|
||||
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
|
||||
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
|
||||
:
|
||||
try:
|
||||
cli(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# put mocks into context so they can be checked later in "then" statements
|
||||
context.editor = mock_editor
|
||||
context.input = mock_input
|
||||
context.getpass = mock_getpass
|
||||
context.read = mock_read
|
||||
context.iter_text = text
|
||||
|
||||
context.execute_steps('''
|
||||
Then all input was used
|
||||
And at least one input method was called
|
||||
''')
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
@then("at least one input method was called")
|
||||
def inputs_were_called(context):
|
||||
assert (
|
||||
context.input.called
|
||||
or context.getpass.called
|
||||
or context.read.called
|
||||
or context.editor.called
|
||||
)
|
||||
|
||||
|
||||
@then("we should be prompted for a password")
|
||||
def password_was_called(context):
|
||||
assert context.getpass.called
|
||||
|
||||
|
||||
@then("we should not be prompted for a password")
|
||||
def password_was_not_called(context):
|
||||
assert not context.getpass.called
|
||||
|
||||
|
||||
@then("all input was used")
|
||||
def all_input_was_used(context):
|
||||
# all inputs were used (ignore if empty string)
|
||||
for temp in context.iter_text:
|
||||
assert "" == temp, "Not all inputs were consumed"
|
||||
|
||||
|
||||
@when('we run "{command}"')
|
||||
@when('we run "{command}" and pipe')
|
||||
@when('we run "{command}" and pipe "{text}"')
|
||||
def run(context, command, text=""):
|
||||
text = text or context.text or ""
|
||||
|
||||
if "cache_dir" in context and context.cache_dir is not None:
|
||||
cache_dir = os.path.join("features", "cache", context.cache_dir)
|
||||
command = command.format(cache_dir=cache_dir)
|
||||
|
||||
args = split_args(command)
|
||||
|
||||
def _mock_editor(command):
|
||||
context.editor_command = command
|
||||
tmpfile = command[-1]
|
||||
with open(tmpfile, "r") as editor_file:
|
||||
file_content = editor_file.read()
|
||||
context.editor_file = {"name": tmpfile, "content": file_content}
|
||||
Path(tmpfile).touch()
|
||||
|
||||
if "password" in context:
|
||||
password = context.password
|
||||
else:
|
||||
password = iter(text)
|
||||
|
||||
try:
|
||||
# fmt: off
|
||||
# see: https://github.com/psf/black/issues/664
|
||||
with \
|
||||
patch("sys.argv", args), \
|
||||
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
|
||||
patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
|
||||
patch("sys.stdin.read", side_effect=lambda: text), \
|
||||
patch("jrnl.time.parse", side_effect=_mock_time_parse(context)), \
|
||||
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
|
||||
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
|
||||
:
|
||||
context.editor = mock_editor
|
||||
context.getpass = mock_getpass
|
||||
cli(args[1:])
|
||||
context.exit_status = 0
|
||||
# fmt: on
|
||||
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 keyring password of "{journal}" to "{password}"')
|
||||
def set_keyring_password(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 == 0, context.exit_status
|
||||
|
||||
|
||||
@then("we flush the output")
|
||||
def flush_stdout(context):
|
||||
context.stdout_capture.truncate(0)
|
||||
context.stdout_capture.seek(0)
|
||||
|
||||
|
||||
@then("we flush the error output")
|
||||
def flush_stderr(context):
|
||||
context.stderr_capture.truncate(0)
|
||||
context.stderr_capture.seek(0)
|
||||
|
||||
|
||||
@then("we flush all the output")
|
||||
def flush_all_output(context):
|
||||
context.execute_steps(
|
||||
"""
|
||||
Then we flush the output
|
||||
Then we flush the error output
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@then("the output should be")
|
||||
@then("the output should be empty")
|
||||
@then('the output should be "{text}"')
|
||||
def check_output(context, text=None):
|
||||
text = (text or context.text or "").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()
|
||||
date, flag = CALENDAR.parse(text)
|
||||
output_date = time.strftime("%Y-%m-%d %H:%M", date)
|
||||
assert output_date in out, output_date
|
||||
|
||||
|
||||
@then("the output should contain pyproject.toml version")
|
||||
def check_output_version_inline(context):
|
||||
out = context.stdout_capture.getvalue()
|
||||
pyproject = (Path(__file__) / ".." / ".." / ".." / "pyproject.toml").resolve()
|
||||
pyproject_contents = toml.load(pyproject)
|
||||
pyproject_version = pyproject_contents["tool"]["poetry"]["version"]
|
||||
assert pyproject_version in out, pyproject_version
|
||||
|
||||
|
||||
@then("the output should contain")
|
||||
@then('the output should contain "{text}"')
|
||||
@then('the output should contain "{text}" or "{text2}"')
|
||||
def check_output_inline(context, text=None, text2=None):
|
||||
text = text or context.text
|
||||
out = context.stdout_capture.getvalue()
|
||||
assert (text and text in out) or (text2 and text2 in out)
|
||||
|
||||
|
||||
@then("the error output should contain")
|
||||
@then('the error output should contain "{text}"')
|
||||
@then('the error output should contain "{text}" or "{text2}"')
|
||||
def check_error_output_inline(context, text=None, text2=None):
|
||||
text = text or context.text
|
||||
out = context.stderr_capture.getvalue()
|
||||
assert (text and text in out) or (text2 and text2 in out)
|
||||
|
||||
|
||||
@then('the output should match "{regex}"')
|
||||
@then('the output should match "{regex}" {num} times')
|
||||
def matches_std_output(context, regex, num=1):
|
||||
out = context.stdout_capture.getvalue()
|
||||
matches = re.findall(regex, out)
|
||||
assert (
|
||||
matches and len(matches) == num
|
||||
), f"\nRegex didn't match exactly {num} time(s):\n{regex}\n{str(out)}\n{str(matches)}"
|
||||
|
||||
|
||||
@then('the error output should match "{regex}"')
|
||||
@then('the error output should match "{regex}" {num} times')
|
||||
def matches_err_ouput(context, regex, num=1):
|
||||
out = context.stderr_capture.getvalue()
|
||||
matches = re.findall(regex, out)
|
||||
assert (
|
||||
matches and len(matches) == num
|
||||
), f"\nRegex didn't match exactly {num} time(s):\n{regex}\n{str(out)}\n{str(matches)}"
|
||||
|
||||
|
||||
@then('the output should not contain "{text}"')
|
||||
def check_output_not_inline(context, text):
|
||||
out = context.stdout_capture.getvalue()
|
||||
assert text not in out
|
||||
|
||||
|
||||
@then('we should see the message "{text}"')
|
||||
@then('the error output should be "{text}"')
|
||||
def check_message(context, text):
|
||||
out = context.stderr_capture.getvalue()
|
||||
assert text in out, [text, out]
|
||||
|
||||
|
||||
@then('we should not see the message "{text}"')
|
||||
def check_not_message(context, text):
|
||||
out = context.stderr_capture.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(context, journal_name)
|
||||
assert text in journal, journal
|
||||
|
||||
|
||||
@then('the journal should not contain "{text}"')
|
||||
@then('journal "{journal_name}" should not contain "{text}"')
|
||||
def check_not_journal_content(context, text, journal_name="default"):
|
||||
journal = read_journal(context, journal_name)
|
||||
assert text not in journal, journal
|
||||
|
||||
|
||||
@then("the journal should not exist")
|
||||
@then('journal "{journal_name}" should not exist')
|
||||
def journal_doesnt_exist(context, journal_name="default"):
|
||||
configuration = load_config(context.config_path)
|
||||
|
||||
journal_path = configuration["journals"][journal_name]
|
||||
assert not os.path.exists(journal_path)
|
||||
|
||||
|
||||
@then("the journal should exist")
|
||||
@then('journal "{journal_name}" should exist')
|
||||
def journal_exists(context, journal_name="default"):
|
||||
configuration = load_config(context.config_path)
|
||||
|
||||
journal_path = configuration["journals"][journal_name]
|
||||
assert os.path.exists(journal_path)
|
||||
|
||||
|
||||
@then('the config should have "{key}" set to')
|
||||
@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):
|
||||
value = read_value_from_string(value or context.text or "")
|
||||
configuration = load_config(context.config_path)
|
||||
|
||||
if journal:
|
||||
configuration = configuration["journals"][journal]
|
||||
|
||||
assert key in configuration
|
||||
assert configuration[key] == value
|
||||
|
||||
|
||||
@then('the config for journal "{journal}" should not have "{key}" set')
|
||||
def config_no_var(context, key, value="", journal=None):
|
||||
configuration = load_config(context.config_path)
|
||||
|
||||
if journal:
|
||||
configuration = configuration["journals"][journal]
|
||||
|
||||
assert key not in configuration
|
||||
|
||||
|
||||
@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(context, journal_name)
|
||||
assert len(journal.entries) == number
|
||||
|
||||
|
||||
@when("the journal directory is listed")
|
||||
def list_journal_directory(context, journal="default"):
|
||||
with open(context.config_path) as config_file:
|
||||
configuration = yaml.load(config_file, Loader=yaml.FullLoader)
|
||||
journal_path = configuration["journals"][journal]
|
||||
for root, dirnames, f in os.walk(journal_path):
|
||||
for file in f:
|
||||
print(os.path.join(root, file))
|
||||
|
||||
|
||||
@then("fail")
|
||||
def debug_fail(context):
|
||||
assert False
|
|
@ -1,192 +0,0 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import string
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from behave import given
|
||||
from behave import then
|
||||
|
||||
|
||||
@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()
|
||||
struct = json.loads(out)
|
||||
|
||||
for node in field.split("."):
|
||||
try:
|
||||
struct = struct[int(node)]
|
||||
except ValueError:
|
||||
assert node in struct
|
||||
struct = struct[node]
|
||||
|
||||
assert key in struct
|
||||
|
||||
|
||||
@then("the json output should contain {path}")
|
||||
@then('the json output should contain {path} = "{value}"')
|
||||
def check_json_output_path(context, path, value=None):
|
||||
"""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]
|
||||
|
||||
if value is not None:
|
||||
assert struct == value, struct
|
||||
else:
|
||||
assert struct is not None
|
||||
|
||||
|
||||
@then(
|
||||
'entry {entry_number:d} should have an array "{name}" with {items_number:d} elements'
|
||||
)
|
||||
def entry_array_count(context, entry_number, name, items_number):
|
||||
# note that entry_number is 1-indexed.
|
||||
out = context.stdout_capture.getvalue()
|
||||
out_json = json.loads(out)
|
||||
assert len(out_json["entries"][entry_number - 1][name]) == items_number
|
||||
|
||||
|
||||
@then("the output should be a valid XML string")
|
||||
def assert_valid_xml_string(context):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
assert xml_tree, output
|
||||
|
||||
|
||||
@then('"{item}" node in the xml output should have {number:d} elements')
|
||||
def assert_xml_output_entries_count(context, item, number):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
|
||||
xml_tags = (node.tag for node in xml_tree)
|
||||
assert item in xml_tags, str(list(xml_tags))
|
||||
|
||||
actual_entry_count = len(xml_tree.find(item))
|
||||
assert actual_entry_count == number, actual_entry_count
|
||||
|
||||
|
||||
@then('there should be {number:d} "{item}" elements')
|
||||
def count_elements(context, number, item):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
assert len(xml_tree.findall(".//" + item)) == number
|
||||
|
||||
|
||||
@then('"tags" in the xml output should contain {expected_tags_json_list}')
|
||||
def assert_xml_output_tags(context, expected_tags_json_list):
|
||||
output = context.stdout_capture.getvalue()
|
||||
xml_tree = ElementTree.fromstring(output)
|
||||
|
||||
xml_tags = (node.tag for node in xml_tree)
|
||||
assert "tags" in xml_tags, str(list(xml_tags))
|
||||
|
||||
expected_tags = json.loads(expected_tags_json_list)
|
||||
actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags"))
|
||||
assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)]
|
||||
|
||||
|
||||
@given('we create cache directory "{dir_name}"')
|
||||
@given("we create a cache directory")
|
||||
def create_directory(context, dir_name=None):
|
||||
if not dir_name:
|
||||
dir_name = "cache_" + "".join(
|
||||
random.choices(string.ascii_uppercase + string.digits, k=20)
|
||||
)
|
||||
|
||||
working_dir = os.path.join("features", "cache", dir_name)
|
||||
if os.path.exists(working_dir):
|
||||
shutil.rmtree(working_dir)
|
||||
os.makedirs(working_dir)
|
||||
context.cache_dir = dir_name
|
||||
|
||||
|
||||
@then('cache "{dir_name}" should contain the files')
|
||||
@then('cache "{dir_name}" should contain the files {expected_files_json_list}')
|
||||
@then("the cache should contain the files")
|
||||
def assert_dir_contains_files(context, dir_name=None, expected_files_json_list=""):
|
||||
if not dir_name:
|
||||
dir_name = context.cache_dir
|
||||
|
||||
working_dir = os.path.join("features", "cache", dir_name)
|
||||
actual_files = os.listdir(working_dir)
|
||||
|
||||
expected_files = context.text or expected_files_json_list
|
||||
expected_files = expected_files.split("\n")
|
||||
|
||||
# sort to deal with inconsistent default file ordering on different OS's
|
||||
actual_files.sort()
|
||||
expected_files.sort()
|
||||
|
||||
assert actual_files == expected_files, [actual_files, expected_files]
|
||||
|
||||
|
||||
@then('the content of file "{file_path}" in cache directory "{cache_dir}" should be')
|
||||
@then('the content of file "{file_path}" in the cache should be')
|
||||
def assert_exported_yaml_file_content(context, file_path, cache_dir=None):
|
||||
if not cache_dir:
|
||||
cache_dir = context.cache_dir
|
||||
|
||||
expected_content = context.text.strip().splitlines()
|
||||
full_file_path = os.path.join("features", "cache", cache_dir, file_path)
|
||||
|
||||
with open(full_file_path, "r") as f:
|
||||
actual_content = f.read().strip().splitlines()
|
||||
|
||||
for actual_line, expected_line in zip(actual_content, expected_content):
|
||||
if actual_line.startswith("tags: ") and expected_line.startswith("tags: "):
|
||||
assert_equal_tags_ignoring_order(
|
||||
actual_line, expected_line, actual_content, expected_content
|
||||
)
|
||||
else:
|
||||
assert actual_line.strip() == expected_line.strip(), [
|
||||
[actual_line.strip(), expected_line.strip()],
|
||||
[actual_content, expected_content],
|
||||
]
|
||||
|
||||
|
||||
def assert_equal_tags_ignoring_order(
|
||||
actual_line, expected_line, actual_content, expected_content
|
||||
):
|
||||
actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
|
||||
expected_tags = set(
|
||||
tag.strip() for tag in expected_line[len("tags: ") :].split(",")
|
||||
)
|
||||
assert actual_tags == expected_tags, [
|
||||
[actual_tags, expected_tags],
|
||||
[expected_content, actual_content],
|
||||
]
|
|
@ -1,71 +0,0 @@
|
|||
Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||
|
||||
Scenario: Upgrade and parse journals with square brackets
|
||||
Given we use the config "upgrade_from_195.json"
|
||||
When we run "jrnl -9" and enter "Y"
|
||||
Then the journal should have 2 entries
|
||||
And the output should contain
|
||||
"""
|
||||
2010-06-10 15:00 A life without chocolate is like a bad analogy.
|
||||
"""
|
||||
And the output should contain
|
||||
"""
|
||||
2013-06-10 15:40 He said "[this] is the best time to be alive".
|
||||
"""
|
||||
|
||||
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 be prompted for a password
|
||||
And the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Upgrading a config without colors to colors
|
||||
Given we use the config "no_colors.yaml"
|
||||
When we run "jrnl -n 1"
|
||||
Then the config should have "colors" set to
|
||||
"""
|
||||
{
|
||||
'date':'none',
|
||||
'title':'none',
|
||||
'body':'none',
|
||||
'tags':'none'
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Upgrade and parse journals with little endian date format
|
||||
Given we use the config "upgrade_from_195_little_endian_dates.json"
|
||||
When we run "jrnl -9" and enter "Y"
|
||||
Then the journal should have 2 entries
|
||||
And the output should contain
|
||||
"""
|
||||
10.06.2010 15:00 A life without chocolate is like a bad analogy.
|
||||
"""
|
||||
And the output should contain
|
||||
"""
|
||||
10.06.2013 15:40 He said "[this] is the best time to be alive".
|
||||
"""
|
||||
|
||||
Scenario: Upgrade with missing journal
|
||||
Given we use the config "upgrade_from_195_with_missing_journal.json"
|
||||
When we run "jrnl -ls" and enter
|
||||
""""
|
||||
Y
|
||||
"""
|
||||
Then the output should contain "Error: features/journals/missing.journal does not exist."
|
||||
And we should get no error
|
||||
|
||||
Scenario: Upgrade with missing encrypted journal
|
||||
Given we use the config "upgrade_from_195_with_missing_encrypted_journal.json"
|
||||
When we run "jrnl -ls" and enter
|
||||
"""
|
||||
Y
|
||||
bad doggie no biscuit
|
||||
"""
|
||||
Then the output should contain "Error: features/journals/missing.journal does not exist."
|
||||
And the error output should contain "We're all done"
|
||||
And we should get no error
|
|
@ -1,216 +0,0 @@
|
|||
Feature: Writing new entries.
|
||||
|
||||
Scenario Outline: Multiline entry with punctuation should keep title punctuation
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
When we run "jrnl This is. the title\\n This is the second line"
|
||||
And we run "jrnl -n 1"
|
||||
Then the output should contain "This is. the title"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
| encrypted |
|
||||
|
||||
Scenario Outline: Single line entry with period should be split at period
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl This is. the title"
|
||||
And we run "jrnl -1"
|
||||
Then the output should contain "| the title"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: CJK entry should be split at fullwidth period without following space.
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl 七転び。八起き"
|
||||
And we run "jrnl -1"
|
||||
Then the output should contain "| 八起き"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_folder |
|
||||
| basic_dayone |
|
||||
|
||||
Scenario Outline: Writing an entry from command line should store the entry
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
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."
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
| encrypted |
|
||||
|
||||
Scenario Outline: Writing a partial entry from command line with edit flag should go to the editor
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we run "jrnl this is a partial --edit"
|
||||
Then we should see the message "Entry added"
|
||||
Then the editor should have been called
|
||||
And the editor file content should be
|
||||
"""
|
||||
this is a partial
|
||||
"""
|
||||
When we run "jrnl -n 1"
|
||||
Then the output should contain "this is a partial"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| basic_onefile |
|
||||
| basic_encrypted |
|
||||
| basic_dayone |
|
||||
| basic_folder |
|
||||
|
||||
Scenario Outline: Writing an empty entry from the editor should yield "Nothing saved to file" message
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "test" if prompted
|
||||
When we open the editor and enter nothing
|
||||
Then the error output should contain "[Nothing saved to file]"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| editor |
|
||||
| editor_empty_folder |
|
||||
| dayone |
|
||||
| basic_encrypted |
|
||||
| basic_onefile |
|
||||
|
||||
@skip
|
||||
Scenario Outline: Writing an empty entry from the command line with no editor should yield nothing
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
When we run "jrnl" and enter nothing
|
||||
Then the output should be empty
|
||||
And the error output should contain "Writing Entry; on a blank line"
|
||||
And the editor should not have been called
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| encrypted |
|
||||
# | dayone | @todo
|
||||
|
||||
Scenario Outline: Writing an entry does not print the entire journal
|
||||
# https://github.com/jrnl-org/jrnl/issues/87
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
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"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| editor |
|
||||
| editor_empty_folder |
|
||||
| dayone |
|
||||
| encrypted |
|
||||
|
||||
Scenario Outline: Embedded period stays in title
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
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.
|
||||
"""
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
| encrypted |
|
||||
|
||||
Scenario Outline: Write and read emoji support
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
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 "🐘"
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| dayone |
|
||||
| encrypted |
|
||||
|
||||
Scenario Outline: Writing an entry at the prompt (no editor) should store the entry
|
||||
Given we use the config "<config_file>.yaml"
|
||||
And we use the password "bad doggie no biscuit" if prompted
|
||||
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
||||
Then we should get no error
|
||||
When we run "jrnl -on '2013-07-25'"
|
||||
Then the output should contain "2013-07-25 09:00 I saw Elvis."
|
||||
And the output should contain "| He's alive."
|
||||
|
||||
Examples: configs
|
||||
| config_file |
|
||||
| simple |
|
||||
| empty_folder |
|
||||
| encrypted |
|
||||
|
||||
@todo
|
||||
Scenario: Writing an entry at the prompt (no editor) in DayOne journal
|
||||
# Need to test DayOne w/out an editor
|
||||
|
||||
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: Writing into Dayone adds extended metadata
|
||||
Given we use the config "dayone.yaml"
|
||||
When we run "jrnl 01 may 1979: Being born hurts."
|
||||
And we run "jrnl --export json"
|
||||
Then "entries" in the json output should have 5 elements
|
||||
And the json output should contain entries.0.creator.software_agent
|
||||
And the json output should contain entries.0.creator.os_agent
|
||||
And the json output should contain entries.0.creator.host_name
|
||||
And the json output should contain entries.0.creator.generation_date
|
||||
And the json output should contain entries.0.creator.device_agent
|
||||
And "entries.0.creator.software_agent" in the json output should contain "jrnl"
|
||||
|
||||
# fails when system time is UTC (as on Travis-CI)
|
||||
@skip
|
||||
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: Opening an folder that's not a DayOne folder should treat as folder journal
|
||||
Given we use the config "empty_folder.yaml"
|
||||
When we run "jrnl 23 july 2013: Testing folder journal."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should be "2013-07-23 09:00 Testing folder journal."
|
|
@ -1,203 +0,0 @@
|
|||
import base64
|
||||
import getpass
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.fernet import InvalidToken
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from .Journal import Journal
|
||||
from .Journal import LegacyJournal
|
||||
from .prompt import create_password
|
||||
|
||||
|
||||
def make_key(password):
|
||||
password = password.encode("utf-8")
|
||||
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=100_000,
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = kdf.derive(password)
|
||||
return base64.urlsafe_b64encode(key)
|
||||
|
||||
|
||||
def decrypt_content(
|
||||
decrypt_func: Callable[[str], Optional[str]],
|
||||
keychain: str = None,
|
||||
max_attempts: int = 3,
|
||||
) -> str:
|
||||
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||
password = pwd_from_keychain or getpass.getpass()
|
||||
result = decrypt_func(password)
|
||||
# Password is bad:
|
||||
if result is None and pwd_from_keychain:
|
||||
set_keychain(keychain, None)
|
||||
attempt = 1
|
||||
while result is None and attempt < max_attempts:
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = getpass.getpass()
|
||||
result = decrypt_func(password)
|
||||
attempt += 1
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
print("Extremely wrong password.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class EncryptedJournal(Journal):
|
||||
def __init__(self, name="default", **kwargs):
|
||||
super().__init__(name, **kwargs)
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
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"]
|
||||
dirname = os.path.dirname(filename)
|
||||
if not os.path.exists(filename):
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
print(f"[Directory {dirname} created]", file=sys.stderr)
|
||||
self.create_file(filename)
|
||||
self.password = create_password(self.name)
|
||||
|
||||
print(
|
||||
f"Encrypted journal '{self.name}' created at {filename}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
logging.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def _load(self, filename):
|
||||
"""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 decrypt_journal(password):
|
||||
key = make_key(password)
|
||||
try:
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8")
|
||||
self.password = password
|
||||
return plain
|
||||
except (InvalidToken, IndexError):
|
||||
return None
|
||||
|
||||
if self.password:
|
||||
return decrypt_journal(self.password)
|
||||
|
||||
return decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
||||
def _store(self, filename, text):
|
||||
key = make_key(self.password)
|
||||
journal = Fernet(key).encrypt(text.encode("utf-8"))
|
||||
with open(filename, "wb") as f:
|
||||
f.write(journal)
|
||||
|
||||
@classmethod
|
||||
def from_journal(cls, other: Journal):
|
||||
new_journal = super().from_journal(other)
|
||||
try:
|
||||
new_journal.password = (
|
||||
other.password
|
||||
if hasattr(other, "password")
|
||||
else create_password(other.name)
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("[Interrupted while creating new journal]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return new_journal
|
||||
|
||||
|
||||
class LegacyEncryptedJournal(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().__init__(name, **kwargs)
|
||||
self.config["encrypt"] = True
|
||||
self.password = None
|
||||
|
||||
def _load(self, filename):
|
||||
with open(filename, "rb") as f:
|
||||
journal_encrypted = f.read()
|
||||
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
||||
|
||||
def decrypt_journal(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.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 self.password:
|
||||
return decrypt_journal(self.password)
|
||||
return decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||
|
||||
|
||||
def get_keychain(journal_name):
|
||||
import keyring
|
||||
|
||||
try:
|
||||
return keyring.get_password("jrnl", journal_name)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if not isinstance(e, keyring.errors.NoKeyringError):
|
||||
print("Failed to retrieve keyring", file=sys.stderr)
|
||||
return ""
|
||||
|
||||
|
||||
def set_keychain(journal_name, password):
|
||||
import keyring
|
||||
|
||||
if password is None:
|
||||
try:
|
||||
keyring.delete_password("jrnl", journal_name)
|
||||
except keyring.errors.KeyringError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
keyring.set_password("jrnl", journal_name, password)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if isinstance(e, keyring.errors.NoKeyringError):
|
||||
print(
|
||||
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print("Failed to retrieve keyring", file=sys.stderr)
|
|
@ -1,95 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
import codecs
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
from . import Journal
|
||||
|
||||
|
||||
def get_files(journal_config):
|
||||
"""Searches through sub directories starting with journal_config and find all text files"""
|
||||
filenames = []
|
||||
for root, dirnames, f in os.walk(journal_config):
|
||||
for filename in fnmatch.filter(f, "*.txt"):
|
||||
filenames.append(os.path.join(root, filename))
|
||||
return filenames
|
||||
|
||||
|
||||
class Folder(Journal.Journal):
|
||||
"""A Journal handling multiple files in a folder"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
self._diff_entry_dates = []
|
||||
super(Folder, self).__init__(**kwargs)
|
||||
|
||||
def open(self):
|
||||
filenames = []
|
||||
self.entries = []
|
||||
filenames = get_files(self.config["journal"])
|
||||
for filename in filenames:
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
journal = f.read()
|
||||
self.entries.extend(self._parse(journal))
|
||||
self.sort()
|
||||
return self
|
||||
|
||||
def write(self):
|
||||
"""Writes only the entries that have been modified into proper files."""
|
||||
# Create a list of dates of modified entries. Start with diff_entry_dates
|
||||
modified_dates = self._diff_entry_dates
|
||||
seen_dates = set(self._diff_entry_dates)
|
||||
for e in self.entries:
|
||||
if e.modified:
|
||||
if e.date not in seen_dates:
|
||||
modified_dates.append(e.date)
|
||||
seen_dates.add(e.date)
|
||||
|
||||
# For every date that had a modified entry, write to a file
|
||||
for d in modified_dates:
|
||||
write_entries = []
|
||||
filename = os.path.join(
|
||||
self.config["journal"],
|
||||
d.strftime("%Y"),
|
||||
d.strftime("%m"),
|
||||
d.strftime("%d") + ".txt",
|
||||
)
|
||||
dirname = os.path.dirname(filename)
|
||||
# create directory if it doesn't exist
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
for e in self.entries:
|
||||
if (
|
||||
e.date.year == d.year
|
||||
and e.date.month == d.month
|
||||
and e.date.day == d.day
|
||||
):
|
||||
write_entries.append(e)
|
||||
journal = "\n".join([e.__str__() for e in write_entries])
|
||||
with codecs.open(filename, "w", "utf-8") as journal_file:
|
||||
journal_file.write(journal)
|
||||
# look for and delete empty files
|
||||
filenames = []
|
||||
filenames = get_files(self.config["journal"])
|
||||
for filename in filenames:
|
||||
if os.stat(filename).st_size <= 0:
|
||||
# print("empty file: {}".format(filename))
|
||||
os.remove(filename)
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates it's entries."""
|
||||
mod_entries = self._parse(edited)
|
||||
diff_entries = set(self.entries) - set(mod_entries)
|
||||
for e in diff_entries:
|
||||
self._diff_entry_dates.append(e.date)
|
||||
# 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
|
|
@ -1,9 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
try:
|
||||
from .__version__ import __version__
|
||||
from jrnl.__version__ import __version__
|
||||
except ImportError:
|
||||
__version__ = "source"
|
||||
__title__ = "jrnl"
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import sys
|
||||
|
||||
from .cli import cli
|
||||
from jrnl.main import run
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(cli())
|
||||
sys.exit(run())
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "v2.7.1-beta"
|
||||
__version__ = "v4.2.1"
|
||||
|
|
187
jrnl/args.py
187
jrnl/args.py
|
@ -1,26 +1,26 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from .commands import postconfig_decrypt
|
||||
from .commands import postconfig_encrypt
|
||||
from .commands import postconfig_import
|
||||
from .commands import postconfig_list
|
||||
from .commands import preconfig_diagnostic
|
||||
from .commands import preconfig_version
|
||||
from .output import deprecated_cmd
|
||||
from .plugins import EXPORT_FORMATS
|
||||
from .plugins import IMPORT_FORMATS
|
||||
from .plugins import util
|
||||
from jrnl.commands import postconfig_decrypt
|
||||
from jrnl.commands import postconfig_encrypt
|
||||
from jrnl.commands import postconfig_import
|
||||
from jrnl.commands import postconfig_list
|
||||
from jrnl.commands import preconfig_diagnostic
|
||||
from jrnl.commands import preconfig_version
|
||||
from jrnl.output import deprecated_cmd
|
||||
from jrnl.plugins import EXPORT_FORMATS
|
||||
from jrnl.plugins import IMPORT_FORMATS
|
||||
from jrnl.plugins import util
|
||||
|
||||
|
||||
class WrappingFormatter(argparse.RawTextHelpFormatter):
|
||||
"""Used in help screen"""
|
||||
|
||||
def _split_lines(self, text, width):
|
||||
def _split_lines(self, text: str, width: int) -> list[str]:
|
||||
text = text.split("\n\n")
|
||||
text = map(lambda t: self._whitespace_matcher.sub(" ", t).strip(), text)
|
||||
text = map(lambda t: textwrap.wrap(t, width=56), text)
|
||||
|
@ -28,7 +28,44 @@ class WrappingFormatter(argparse.RawTextHelpFormatter):
|
|||
return text
|
||||
|
||||
|
||||
def parse_args(args=[]):
|
||||
class IgnoreNoneAppendAction(argparse._AppendAction):
|
||||
"""
|
||||
Pass -not without a following string and avoid appending
|
||||
a None value to the excluded list
|
||||
"""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if values is not None:
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
|
||||
def parse_not_arg(
|
||||
args: list[str], parsed_args: argparse.Namespace, parser: argparse.ArgumentParser
|
||||
) -> argparse.Namespace:
|
||||
"""
|
||||
It's possible to use -not as a precursor to -starred and -tagged
|
||||
to reverse their behaviour, however this requires some extra logic
|
||||
to parse, and to ensure we still do not allow passing an empty -not
|
||||
"""
|
||||
|
||||
parsed_args.exclude_starred = False
|
||||
parsed_args.exclude_tagged = False
|
||||
|
||||
if "-not-starred" in "".join(args):
|
||||
parsed_args.starred = False
|
||||
parsed_args.exclude_starred = True
|
||||
if "-not-tagged" in "".join(args):
|
||||
parsed_args.tagged = False
|
||||
parsed_args.exclude_tagged = True
|
||||
if "-not" in args and not any(
|
||||
[parsed_args.exclude_starred, parsed_args.exclude_tagged, parsed_args.excluded]
|
||||
):
|
||||
parser.error("argument -not: expected 1 argument")
|
||||
|
||||
return parsed_args
|
||||
|
||||
|
||||
def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||
"""
|
||||
Argument parsing that is doable before the config is available.
|
||||
Everything else goes into "text" for later parsing.
|
||||
|
@ -39,9 +76,9 @@ def parse_args(args=[]):
|
|||
description="Collect your thoughts and notes without leaving the command line",
|
||||
epilog=textwrap.dedent(
|
||||
"""
|
||||
Thank you to all of our contributors! Come see the whole list of code and
|
||||
financial contributors at https://github.com/jrnl-org/jrnl. And special
|
||||
thanks to Bad Lip Reading for the Yoda joke in the Writing section above."""
|
||||
We gratefully thank all contributors!
|
||||
Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl
|
||||
And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)""" # noqa: E501
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -85,7 +122,13 @@ def parse_args(args=[]):
|
|||
action="store_const",
|
||||
const=postconfig_list,
|
||||
dest="postconfig_cmd",
|
||||
help="List all configured journals",
|
||||
help="""
|
||||
List all configured journals.
|
||||
|
||||
Optional parameters:
|
||||
|
||||
--format [json or yaml]
|
||||
""",
|
||||
)
|
||||
standalone.add_argument(
|
||||
"--ls",
|
||||
|
@ -168,6 +211,12 @@ def parse_args(args=[]):
|
|||
"Writing", textwrap.dedent(compose_msg).strip()
|
||||
)
|
||||
composing.add_argument("text", metavar="", nargs="*")
|
||||
composing.add_argument(
|
||||
"--template",
|
||||
dest="template",
|
||||
help="Path to template file. Can be a local path, absolute path, or a path "
|
||||
"relative to $XDG_DATA_HOME/jrnl/templates/",
|
||||
)
|
||||
|
||||
read_msg = (
|
||||
"To find entries from your journal, use any combination of the below filters."
|
||||
|
@ -216,14 +265,17 @@ def parse_args(args=[]):
|
|||
reading.add_argument(
|
||||
"-contains",
|
||||
dest="contains",
|
||||
action="append",
|
||||
metavar="TEXT",
|
||||
help="Show entries containing specific text (put quotes around text with spaces)",
|
||||
help="Show entries containing specific text (put quotes around text with "
|
||||
"spaces)",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-and",
|
||||
dest="strict",
|
||||
action="store_true",
|
||||
help='Show only entries that match all conditions, like saying "x AND y" (default: OR)',
|
||||
help='Show only entries that match all conditions, like saying "x AND y" '
|
||||
"(default: OR)",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-starred",
|
||||
|
@ -231,12 +283,19 @@ def parse_args(args=[]):
|
|||
action="store_true",
|
||||
help="Show only starred entries (marked with *)",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-tagged",
|
||||
dest="tagged",
|
||||
action="store_true",
|
||||
help="Show only entries that have at least one tag",
|
||||
)
|
||||
reading.add_argument(
|
||||
"-n",
|
||||
dest="limit",
|
||||
default=None,
|
||||
metavar="NUMBER",
|
||||
help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect)",
|
||||
help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same "
|
||||
"effect)",
|
||||
nargs="?",
|
||||
type=int,
|
||||
)
|
||||
|
@ -245,13 +304,21 @@ def parse_args(args=[]):
|
|||
dest="excluded",
|
||||
nargs="?",
|
||||
default=[],
|
||||
metavar="TAG",
|
||||
action="append",
|
||||
help="Exclude entries with this tag",
|
||||
metavar="TAG/FLAG",
|
||||
action=IgnoreNoneAppendAction,
|
||||
help=(
|
||||
"If passed a string, will exclude entries with that tag. "
|
||||
"Can be also used before -starred or -tagged flags, to exclude "
|
||||
"starred or tagged entries respectively."
|
||||
),
|
||||
)
|
||||
|
||||
search_options_msg = """ These help you do various tasks with the selected entries from your search.
|
||||
search_options_msg = (
|
||||
" " # Preserves indentation
|
||||
"""
|
||||
These help you do various tasks with the selected entries from your search.
|
||||
If used on their own (with no search), they will act on your entire journal"""
|
||||
)
|
||||
exporting = parser.add_argument_group(
|
||||
"Searching Options", textwrap.dedent(search_options_msg)
|
||||
)
|
||||
|
@ -267,6 +334,14 @@ def parse_args(args=[]):
|
|||
action="store_true",
|
||||
help="Interactively deletes selected entries",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--change-time",
|
||||
dest="change_time",
|
||||
nargs="?",
|
||||
metavar="DATE",
|
||||
const="now",
|
||||
help="Change timestamp for selected entries (default: now)",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--format",
|
||||
metavar="TYPE",
|
||||
|
@ -294,7 +369,8 @@ def parse_args(args=[]):
|
|||
"--tags",
|
||||
dest="tags",
|
||||
action="store_true",
|
||||
help="Alias for '--format tags'. Returns a list of all tags and number of occurences",
|
||||
help="Alias for '--format tags'. Returns a list of all tags and number of "
|
||||
"occurrences",
|
||||
)
|
||||
exporting.add_argument(
|
||||
"--short",
|
||||
|
@ -314,8 +390,67 @@ def parse_args(args=[]):
|
|||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
config_overrides = parser.add_argument_group(
|
||||
"Config file override",
|
||||
textwrap.dedent("Apply a one-off override of the config file option"),
|
||||
)
|
||||
config_overrides.add_argument(
|
||||
"--config-override",
|
||||
dest="config_override",
|
||||
action="append",
|
||||
type=str,
|
||||
nargs=2,
|
||||
default=[],
|
||||
metavar="CONFIG_KV_PAIR",
|
||||
help="""
|
||||
Override configured key-value pair with CONFIG_KV_PAIR for this command invocation only.
|
||||
|
||||
Examples: \n
|
||||
\t - Use a different editor for this jrnl entry, call: \n
|
||||
\t jrnl --config-override editor "nano" \n
|
||||
\t - Override color selections\n
|
||||
\t jrnl --config-override colors.body blue --config-override colors.title green
|
||||
""", # noqa: E501
|
||||
)
|
||||
config_overrides.add_argument(
|
||||
"--co",
|
||||
dest="config_override",
|
||||
action="append",
|
||||
type=str,
|
||||
nargs=2,
|
||||
default=[],
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
alternate_config = parser.add_argument_group(
|
||||
"Specifies alternate config to be used",
|
||||
textwrap.dedent("Applies alternate config for current session"),
|
||||
)
|
||||
|
||||
alternate_config.add_argument(
|
||||
"--config-file",
|
||||
dest="config_file_path",
|
||||
type=str,
|
||||
default="",
|
||||
help="""
|
||||
Overrides default (created when first installed) config file for this command only.
|
||||
|
||||
Examples: \n
|
||||
\t - Use a work config file for this jrnl entry, call: \n
|
||||
\t jrnl --config-file /home/user1/work_config.yaml
|
||||
\t - Use a personal config file stored on a thumb drive: \n
|
||||
\t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
|
||||
""", # noqa: E501
|
||||
)
|
||||
|
||||
alternate_config.add_argument(
|
||||
"--cf", dest="config_file_path", type=str, default="", help=argparse.SUPPRESS
|
||||
)
|
||||
|
||||
# Handle '-123' as a shortcut for '-n 123'
|
||||
num = re.compile(r"^-(\d+)$")
|
||||
args = [num.sub(r"-n \1", arg) for arg in args]
|
||||
parsed_args = parser.parse_intermixed_args(args)
|
||||
parsed_args = parse_not_arg(args, parsed_args, parser)
|
||||
|
||||
return parser.parse_intermixed_args(args)
|
||||
return parsed_args
|
||||
|
|
42
jrnl/cli.py
42
jrnl/cli.py
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from .jrnl import run
|
||||
from .args import parse_args
|
||||
from .exception import JrnlError
|
||||
|
||||
|
||||
def configure_logger(debug=False):
|
||||
if not debug:
|
||||
logging.disable()
|
||||
return
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(levelname)-8s %(name)-12s %(message)s",
|
||||
)
|
||||
logging.getLogger("parsedatetime").setLevel(logging.INFO)
|
||||
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def cli(manual_args=None):
|
||||
try:
|
||||
if manual_args is None:
|
||||
manual_args = sys.argv[1:]
|
||||
|
||||
args = parse_args(manual_args)
|
||||
configure_logger(args.debug)
|
||||
logging.debug("Parsed args: %s", args)
|
||||
|
||||
return run(args)
|
||||
|
||||
except JrnlError as e:
|
||||
print(e.message, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
return 1
|
|
@ -1,21 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import re
|
||||
from string import punctuation
|
||||
from string import whitespace
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import colorama
|
||||
|
||||
from .os_compat import on_windows
|
||||
from jrnl.os_compat import on_windows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.journals import Entry
|
||||
|
||||
if on_windows():
|
||||
colorama.init()
|
||||
|
||||
WARNING_COLOR = colorama.Fore.YELLOW
|
||||
ERROR_COLOR = colorama.Fore.RED
|
||||
RESET_COLOR = colorama.Fore.RESET
|
||||
|
||||
|
||||
def colorize(string, color, bold=False):
|
||||
def colorize(string: str, color: str, bold: bool = False) -> str:
|
||||
"""Returns the string colored with colorama.Fore.color. If the color set by
|
||||
the user is "NONE" or the color doesn't exist in the colorama.Fore attributes,
|
||||
it returns the string without any modification."""
|
||||
|
@ -28,7 +30,9 @@ def colorize(string, color, bold=False):
|
|||
return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL
|
||||
|
||||
|
||||
def highlight_tags_with_background_color(entry, text, color, is_title=False):
|
||||
def highlight_tags_with_background_color(
|
||||
entry: "Entry", text: str, color: str, is_title: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Takes a string and colorizes the tags in it based upon the config value for
|
||||
color.tags, while colorizing the rest of the text based on `color`.
|
||||
|
@ -47,9 +51,9 @@ def highlight_tags_with_background_color(entry, text, color, is_title=False):
|
|||
:returns [(colorized_str, original_str)]"""
|
||||
for part in fragments:
|
||||
if part and part[0] not in config["tagsymbols"]:
|
||||
yield (colorize(part, color, bold=is_title), part)
|
||||
yield colorize(part, color, bold=is_title), part
|
||||
elif part:
|
||||
yield (colorize(part, config["colors"]["tags"], bold=True), part)
|
||||
yield colorize(part, config["colors"]["tags"], bold=True), part
|
||||
|
||||
config = entry.journal.config
|
||||
if config["highlight"]: # highlight tags
|
||||
|
|
145
jrnl/commands.py
145
jrnl/commands.py
|
@ -1,3 +1,6 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
"""
|
||||
Functions in this file are standalone commands. All standalone commands are split into
|
||||
two categories depending on whether they require the config to be loaded to be able to
|
||||
|
@ -11,71 +14,128 @@ run.
|
|||
Also, please note that all (non-builtin) imports should be scoped to each function to
|
||||
avoid any possible overhead for these standalone commands.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from jrnl.config import cmd_requires_valid_journal_name
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
|
||||
def preconfig_diagnostic(_):
|
||||
|
||||
def preconfig_diagnostic(_) -> None:
|
||||
from jrnl import __title__
|
||||
from jrnl import __version__
|
||||
|
||||
print(
|
||||
f"jrnl: {__version__}\n"
|
||||
f"{__title__}: {__version__}\n"
|
||||
f"Python: {sys.version}\n"
|
||||
f"OS: {platform.system()} {platform.release()}"
|
||||
)
|
||||
|
||||
|
||||
def preconfig_version(_):
|
||||
def preconfig_version(_) -> None:
|
||||
import textwrap
|
||||
|
||||
from jrnl import __title__
|
||||
from jrnl import __version__
|
||||
|
||||
version_str = f"""{__title__} version {__version__}
|
||||
output = f"""
|
||||
{__title__} {__version__}
|
||||
|
||||
Copyright (C) 2012-2021 jrnl contributors
|
||||
Copyright © 2012-2023 jrnl contributors
|
||||
|
||||
This is free software, and you are welcome to redistribute it under certain
|
||||
conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html"""
|
||||
This is free software, and you are welcome to redistribute it under certain
|
||||
conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
"""
|
||||
|
||||
print(version_str)
|
||||
output = textwrap.dedent(output).strip()
|
||||
|
||||
print(output)
|
||||
|
||||
|
||||
def postconfig_list(config, **kwargs):
|
||||
from .output import list_journals
|
||||
def postconfig_list(args: argparse.Namespace, config: dict, **_) -> int:
|
||||
from jrnl.output import list_journals
|
||||
|
||||
print(list_journals(config))
|
||||
print(list_journals(config, args.export))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def postconfig_import(args, config, **kwargs):
|
||||
from .Journal import open_journal
|
||||
from .plugins import get_importer
|
||||
@cmd_requires_valid_journal_name
|
||||
def postconfig_import(args: argparse.Namespace, config: dict, **_) -> int:
|
||||
from jrnl.journals import open_journal
|
||||
from jrnl.plugins import get_importer
|
||||
|
||||
# Requires opening the journal
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
||||
format = args.export if args.export else "jrnl"
|
||||
get_importer(format).import_(journal, args.filename)
|
||||
|
||||
if (importer := get_importer(format)) is None:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.ImporterNotFound,
|
||||
MsgStyle.ERROR,
|
||||
{"format": format},
|
||||
)
|
||||
)
|
||||
|
||||
importer.import_(journal, args.filename)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def postconfig_encrypt(args, config, original_config, **kwargs):
|
||||
@cmd_requires_valid_journal_name
|
||||
def postconfig_encrypt(
|
||||
args: argparse.Namespace, config: dict, original_config: dict
|
||||
) -> int:
|
||||
"""
|
||||
Encrypt a journal in place, or optionally to a new file
|
||||
"""
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .Journal import open_journal
|
||||
from .config import update_config
|
||||
from .install import save_config
|
||||
from jrnl.config import update_config
|
||||
from jrnl.install import save_config
|
||||
from jrnl.journals import open_journal
|
||||
|
||||
# Open the journal
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
||||
if hasattr(journal, "can_be_encrypted") and not journal.can_be_encrypted:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CannotEncryptJournalType,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"journal_name": args.journal_name,
|
||||
"journal_type": journal.__class__.__name__,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# If journal is encrypted, create new password
|
||||
logging.debug("Clearing encryption method...")
|
||||
|
||||
if journal.config["encrypt"] is True:
|
||||
logging.debug("Journal already encrypted. Re-encrypting...")
|
||||
print(f"Journal {journal.name} is already encrypted. Create a new password.")
|
||||
journal.encryption_method.clear()
|
||||
else:
|
||||
journal.config["encrypt"] = True
|
||||
journal.encryption_method = None
|
||||
|
||||
new_journal = EncryptedJournal.from_journal(journal)
|
||||
new_journal.write(args.filename)
|
||||
journal.write(args.filename)
|
||||
|
||||
print(
|
||||
f"Journal encrypted to {args.filename or new_journal.config['journal']}.",
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalEncryptedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{"path": args.filename or journal.config["journal"]},
|
||||
)
|
||||
)
|
||||
|
||||
# Update the config, if we encrypted in place
|
||||
|
@ -85,22 +145,31 @@ def postconfig_encrypt(args, config, original_config, **kwargs):
|
|||
)
|
||||
save_config(original_config)
|
||||
|
||||
return 0
|
||||
|
||||
def postconfig_decrypt(args, config, original_config, **kwargs):
|
||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
from .Journal import PlainJournal
|
||||
from .Journal import open_journal
|
||||
from .config import update_config
|
||||
from .install import save_config
|
||||
|
||||
@cmd_requires_valid_journal_name
|
||||
def postconfig_decrypt(
|
||||
args: argparse.Namespace, config: dict, original_config: dict
|
||||
) -> int:
|
||||
"""Decrypts to file. If filename is not set, we encrypt the journal file itself."""
|
||||
from jrnl.config import update_config
|
||||
from jrnl.install import save_config
|
||||
from jrnl.journals import open_journal
|
||||
|
||||
journal = open_journal(args.journal_name, config)
|
||||
journal.config["encrypt"] = False
|
||||
|
||||
new_journal = PlainJournal.from_journal(journal)
|
||||
new_journal.write(args.filename)
|
||||
print(
|
||||
f"Journal decrypted to {args.filename or new_journal.config['journal']}.",
|
||||
file=sys.stderr,
|
||||
logging.debug("Clearing encryption method...")
|
||||
journal.config["encrypt"] = False
|
||||
journal.encryption_method = None
|
||||
|
||||
journal.write(args.filename)
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalDecryptedTo,
|
||||
MsgStyle.NORMAL,
|
||||
{"path": args.filename or journal.config["journal"]},
|
||||
)
|
||||
)
|
||||
|
||||
# Update the config, if we decrypted in place
|
||||
|
@ -109,3 +178,5 @@ def postconfig_decrypt(args, config, original_config, **kwargs):
|
|||
original_config, {"encrypt": False}, args.journal_name, force_local=True
|
||||
)
|
||||
save_config(original_config)
|
||||
|
||||
return 0
|
||||
|
|
199
jrnl/config.py
199
jrnl/config.py
|
@ -1,96 +1,127 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
|
||||
import colorama
|
||||
import yaml
|
||||
import xdg.BaseDirectory
|
||||
from rich.pretty import pretty_repr
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml import constructor
|
||||
|
||||
from . import __version__
|
||||
from .exception import JrnlError
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .output import list_journals
|
||||
from jrnl import __version__
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import list_journals
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.path import get_config_path
|
||||
from jrnl.path import get_default_journal_path
|
||||
|
||||
# Constants
|
||||
DEFAULT_CONFIG_NAME = "jrnl.yaml"
|
||||
XDG_RESOURCE = "jrnl"
|
||||
|
||||
DEFAULT_JOURNAL_NAME = "journal.txt"
|
||||
DEFAULT_JOURNAL_KEY = "default"
|
||||
|
||||
YAML_SEPARATOR = ": "
|
||||
YAML_FILE_ENCODING = "utf-8"
|
||||
|
||||
def save_config(config):
|
||||
|
||||
def make_yaml_valid_dict(input: list) -> dict:
|
||||
"""
|
||||
|
||||
Convert a two-element list of configuration key-value pair into a flat dict.
|
||||
|
||||
The dict is created through the yaml loader, with the assumption that
|
||||
"input[0]: input[1]" is valid yaml.
|
||||
|
||||
:param input: list of configuration keys in dot-notation and their respective values
|
||||
:type input: list
|
||||
:return: A single level dict of the configuration keys in dot-notation and their
|
||||
respective desired values
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
assert len(input) == 2
|
||||
|
||||
# yaml compatible strings are of the form Key:Value
|
||||
yamlstr = YAML_SEPARATOR.join(input)
|
||||
|
||||
runtime_modifications = YAML(typ="safe").load(yamlstr)
|
||||
|
||||
return runtime_modifications
|
||||
|
||||
|
||||
def save_config(config: dict, alt_config_path: str | None = None) -> None:
|
||||
"""Supply alt_config_path if using an alternate config through --config-file."""
|
||||
config["version"] = __version__
|
||||
with open(get_config_path(), "w") as f:
|
||||
yaml.safe_dump(
|
||||
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
|
||||
)
|
||||
|
||||
yaml = YAML(typ="safe")
|
||||
yaml.default_flow_style = False # prevents collapsing of tree structure
|
||||
|
||||
with open(
|
||||
alt_config_path if alt_config_path else get_config_path(),
|
||||
"w",
|
||||
encoding=YAML_FILE_ENCODING,
|
||||
) as f:
|
||||
yaml.dump(config, f)
|
||||
|
||||
|
||||
def get_config_path():
|
||||
try:
|
||||
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
|
||||
except FileExistsError:
|
||||
raise JrnlError(
|
||||
"ConfigDirectoryIsFile",
|
||||
config_directory_path=os.path.join(
|
||||
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
|
||||
),
|
||||
)
|
||||
return os.path.join(
|
||||
config_directory_path or os.path.expanduser("~"), DEFAULT_CONFIG_NAME
|
||||
)
|
||||
|
||||
|
||||
def get_default_config():
|
||||
def get_default_config() -> dict[str, Any]:
|
||||
return {
|
||||
"version": __version__,
|
||||
"journals": {"default": get_default_journal_path()},
|
||||
"journals": {"default": {"journal": get_default_journal_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": "@",
|
||||
"timeformat": "%F %r",
|
||||
"tagsymbols": "#@",
|
||||
"highlight": True,
|
||||
"linewrap": 79,
|
||||
"indent_character": "|",
|
||||
"colors": {
|
||||
"date": "none",
|
||||
"title": "none",
|
||||
"body": "none",
|
||||
"date": "none",
|
||||
"tags": "none",
|
||||
"title": "none",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_default_journal_path():
|
||||
journal_data_path = xdg.BaseDirectory.save_data_path(
|
||||
XDG_RESOURCE
|
||||
) or os.path.expanduser("~")
|
||||
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
|
||||
def get_default_colors() -> dict[str, Any]:
|
||||
return {
|
||||
"body": "none",
|
||||
"date": "black",
|
||||
"tags": "yellow",
|
||||
"title": "cyan",
|
||||
}
|
||||
|
||||
|
||||
def scope_config(config, journal_name):
|
||||
def scope_config(config: dict, journal_name: str) -> dict:
|
||||
if journal_name not in config["journals"]:
|
||||
return config
|
||||
config = config.copy()
|
||||
journal_conf = config["journals"].get(journal_name)
|
||||
if type(journal_conf) is dict:
|
||||
if isinstance(journal_conf, dict):
|
||||
# We can override the default config on a by-journal basis
|
||||
logging.debug(
|
||||
"Updating configuration with specific journal overrides %s", journal_conf
|
||||
"Updating configuration with specific journal overrides:\n%s",
|
||||
pretty_repr(journal_conf),
|
||||
)
|
||||
config.update(journal_conf)
|
||||
else:
|
||||
# But also just give them a string to point to the journal file
|
||||
config["journal"] = journal_conf
|
||||
|
||||
logging.debug("Scoped config:\n%s", pretty_repr(config))
|
||||
return config
|
||||
|
||||
|
||||
def verify_config_colors(config):
|
||||
def verify_config_colors(config: dict) -> bool:
|
||||
"""
|
||||
Ensures the keys set for colors are valid colorama.Fore attributes, or "None"
|
||||
:return: True if all keys are set correctly, False otherwise
|
||||
|
@ -101,33 +132,56 @@ def verify_config_colors(config):
|
|||
if upper_color == "NONE":
|
||||
continue
|
||||
if not getattr(colorama.Fore, upper_color, None):
|
||||
print(
|
||||
"[{2}ERROR{3}: {0} set to invalid color: {1}]".format(
|
||||
key, color, ERROR_COLOR, RESET_COLOR
|
||||
),
|
||||
file=sys.stderr,
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.InvalidColor,
|
||||
MsgStyle.NORMAL,
|
||||
{
|
||||
"key": key,
|
||||
"color": color,
|
||||
},
|
||||
)
|
||||
)
|
||||
all_valid_colors = False
|
||||
return all_valid_colors
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
def load_config(config_path: str) -> dict:
|
||||
"""Tries to load a config file from YAML."""
|
||||
with open(config_path) as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
try:
|
||||
with open(config_path, encoding=YAML_FILE_ENCODING) as f:
|
||||
yaml = YAML(typ="safe")
|
||||
yaml.allow_duplicate_keys = False
|
||||
return yaml.load(f)
|
||||
except constructor.DuplicateKeyError as e:
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.ConfigDoubleKeys,
|
||||
MsgStyle.WARNING,
|
||||
{
|
||||
"error_message": e,
|
||||
},
|
||||
)
|
||||
)
|
||||
with open(config_path, encoding=YAML_FILE_ENCODING) as f:
|
||||
yaml = YAML(typ="safe")
|
||||
yaml.allow_duplicate_keys = True
|
||||
return yaml.load(f)
|
||||
|
||||
|
||||
def is_config_json(config_path):
|
||||
def is_config_json(config_path: str) -> bool:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_file = f.read()
|
||||
return config_file.strip().startswith("{")
|
||||
|
||||
|
||||
def update_config(config, new_config, scope, force_local=False):
|
||||
def update_config(
|
||||
config: dict, new_config: dict, scope: str | None, force_local: bool = False
|
||||
) -> None:
|
||||
"""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
|
||||
if scope and isinstance(config["journals"][scope], dict):
|
||||
config["journals"][scope].update(new_config)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||
|
@ -136,7 +190,7 @@ def update_config(config, new_config, scope, force_local=False):
|
|||
config.update(new_config)
|
||||
|
||||
|
||||
def get_journal_name(args, config):
|
||||
def get_journal_name(args: argparse.Namespace, config: dict) -> argparse.Namespace:
|
||||
args.journal_name = DEFAULT_JOURNAL_KEY
|
||||
|
||||
# The first arg might be a journal name
|
||||
|
@ -149,10 +203,27 @@ def get_journal_name(args, config):
|
|||
args.journal_name = potential_journal_name
|
||||
args.text = args.text[1:]
|
||||
|
||||
if args.journal_name not in config["journals"]:
|
||||
print("No default journal configured.", file=sys.stderr)
|
||||
print(list_journals(config), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
logging.debug("Using journal name: %s", args.journal_name)
|
||||
return args
|
||||
|
||||
|
||||
def cmd_requires_valid_journal_name(func: Callable) -> Callable:
|
||||
def wrapper(args: argparse.Namespace, config: dict, original_config: dict):
|
||||
validate_journal_name(args.journal_name, config)
|
||||
func(args=args, config=config, original_config=original_config)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def validate_journal_name(journal_name: str, config: dict) -> None:
|
||||
if journal_name not in config["journals"]:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.NoNamedJournal,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"journal_name": journal_name,
|
||||
"journals": list_journals(config),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
455
jrnl/controller.py
Normal file
455
jrnl/controller.py
Normal file
|
@ -0,0 +1,455 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jrnl import install
|
||||
from jrnl import plugins
|
||||
from jrnl import time
|
||||
from jrnl.config import DEFAULT_JOURNAL_KEY
|
||||
from jrnl.config import get_config_path
|
||||
from jrnl.config import get_journal_name
|
||||
from jrnl.config import scope_config
|
||||
from jrnl.editor import get_text_from_editor
|
||||
from jrnl.editor import get_text_from_stdin
|
||||
from jrnl.editor import read_template_file
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.journals import open_journal
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.output import print_msgs
|
||||
from jrnl.override import apply_overrides
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
from jrnl.journals import Entry
|
||||
from jrnl.journals import Journal
|
||||
|
||||
|
||||
def run(args: "Namespace"):
|
||||
"""
|
||||
Flow:
|
||||
1. Run standalone command if it doesn't need config (help, version, etc), then exit
|
||||
2. Load config
|
||||
3. Run standalone command if it does need config (encrypt, decrypt, etc), then exit
|
||||
4. Load specified journal
|
||||
5. Start append mode, or search mode
|
||||
6. Perform actions with results from search mode (if needed)
|
||||
7. Profit
|
||||
"""
|
||||
|
||||
# Run command if possible before config is available
|
||||
if callable(args.preconfig_cmd):
|
||||
return args.preconfig_cmd(args)
|
||||
|
||||
# Load the config, and extract journal name
|
||||
config = install.load_or_install_jrnl(args.config_file_path)
|
||||
original_config = config.copy()
|
||||
|
||||
# Apply config overrides
|
||||
config = apply_overrides(args, config)
|
||||
|
||||
args = get_journal_name(args, config)
|
||||
config = scope_config(config, args.journal_name)
|
||||
|
||||
# Run post-config command now that config is ready
|
||||
if callable(args.postconfig_cmd):
|
||||
return args.postconfig_cmd(
|
||||
args=args, config=config, original_config=original_config
|
||||
)
|
||||
|
||||
# --- All the standalone commands are now done --- #
|
||||
|
||||
# Get the journal we're going to be working with
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
||||
kwargs = {
|
||||
"args": args,
|
||||
"config": config,
|
||||
"journal": journal,
|
||||
"old_entries": journal.entries,
|
||||
}
|
||||
|
||||
if _is_append_mode(**kwargs):
|
||||
append_mode(**kwargs)
|
||||
return
|
||||
|
||||
# If not append mode, then we're in search mode (only 2 modes exist)
|
||||
search_mode(**kwargs)
|
||||
entries_found_count = len(journal)
|
||||
_print_entries_found_count(entries_found_count, args)
|
||||
|
||||
# Actions
|
||||
_perform_actions_on_search_results(**kwargs)
|
||||
|
||||
if entries_found_count != 0 and _has_action_args(args):
|
||||
_print_changed_counts(journal)
|
||||
else:
|
||||
# display only occurs if no other action occurs
|
||||
_display_search_results(**kwargs)
|
||||
|
||||
|
||||
def _perform_actions_on_search_results(**kwargs):
|
||||
args = kwargs["args"]
|
||||
|
||||
# Perform actions (if needed)
|
||||
if args.change_time:
|
||||
_change_time_search_results(**kwargs)
|
||||
|
||||
if args.delete:
|
||||
_delete_search_results(**kwargs)
|
||||
|
||||
# open results in editor (if `--edit` was used)
|
||||
if args.edit:
|
||||
_edit_search_results(**kwargs)
|
||||
|
||||
|
||||
def _is_append_mode(args: "Namespace", config: dict, **kwargs) -> bool:
|
||||
"""Determines if we are in append mode (as opposed to search mode)"""
|
||||
# Are any search filters present? If so, then search mode.
|
||||
append_mode = (
|
||||
not _has_search_args(args)
|
||||
and not _has_action_args(args)
|
||||
and not _has_display_args(args)
|
||||
)
|
||||
|
||||
# Might be writing and want to move to editor part of the way through
|
||||
if args.edit and args.text:
|
||||
append_mode = True
|
||||
|
||||
# If the text is entirely tags, then we are also searching (not writing)
|
||||
if append_mode and args.text and _has_only_tags(config["tagsymbols"], args.text):
|
||||
append_mode = False
|
||||
|
||||
return append_mode
|
||||
|
||||
|
||||
def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -> None:
|
||||
"""
|
||||
Gets input from the user to write to the journal
|
||||
0. Check for a template passed as an argument, or in the global config
|
||||
1. Check for input from cli
|
||||
2. Check input being piped in
|
||||
3. Open editor if configured (prepopulated with template if available)
|
||||
4. Use stdin.read as last resort
|
||||
6. Write any found text to journal, or exit
|
||||
"""
|
||||
logging.debug("Append mode: starting")
|
||||
|
||||
template_text = _get_template(args, config)
|
||||
|
||||
if args.text:
|
||||
logging.debug(f"Append mode: cli text detected: {args.text}")
|
||||
raw = " ".join(args.text).strip()
|
||||
if args.edit:
|
||||
raw = _write_in_editor(config, raw)
|
||||
elif not sys.stdin.isatty():
|
||||
logging.debug("Append mode: receiving piped text")
|
||||
raw = sys.stdin.read()
|
||||
else:
|
||||
raw = _write_in_editor(config, template_text)
|
||||
|
||||
if template_text is not None and raw == template_text:
|
||||
logging.error("Append mode: raw text was the same as the template")
|
||||
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
|
||||
|
||||
if not raw or raw.isspace():
|
||||
logging.error("Append mode: couldn't get raw text or entry was empty")
|
||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL))
|
||||
|
||||
logging.debug(
|
||||
f"Append mode: appending raw text to journal '{args.journal_name}': {raw}"
|
||||
)
|
||||
journal.new_entry(raw)
|
||||
if args.journal_name != DEFAULT_JOURNAL_KEY:
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalEntryAdded,
|
||||
MsgStyle.NORMAL,
|
||||
{"journal_name": args.journal_name},
|
||||
)
|
||||
)
|
||||
journal.write()
|
||||
logging.debug("Append mode: completed journal.write()")
|
||||
|
||||
|
||||
def _get_template(args, config) -> str:
|
||||
# Read template file and pass as raw text into the composer
|
||||
logging.debug(
|
||||
"Get template:\n"
|
||||
f"--template: {args.template}\n"
|
||||
f"from config: {config.get('template')}"
|
||||
)
|
||||
template_path = args.template or config.get("template")
|
||||
|
||||
template_text = None
|
||||
|
||||
if template_path:
|
||||
template_text = read_template_file(template_path)
|
||||
|
||||
return template_text
|
||||
|
||||
|
||||
def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None:
|
||||
"""
|
||||
Search for entries in a journal, and return the
|
||||
results. If no search args, then return all results
|
||||
"""
|
||||
logging.debug("Search mode: starting")
|
||||
|
||||
# If no search args, then return all results (don't filter anything)
|
||||
if not _has_search_args(args) and not _has_display_args(args) and not args.text:
|
||||
logging.debug("Search mode: has no search args")
|
||||
return
|
||||
|
||||
logging.debug("Search mode: has search args")
|
||||
_filter_journal_entries(args, journal)
|
||||
|
||||
|
||||
def _write_in_editor(config: dict, prepopulated_text: str | None = None) -> str:
|
||||
if config["editor"]:
|
||||
logging.debug("Append mode: opening editor")
|
||||
raw = get_text_from_editor(config, prepopulated_text)
|
||||
else:
|
||||
raw = get_text_from_stdin()
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def _filter_journal_entries(args: "Namespace", journal: "Journal", **kwargs) -> None:
|
||||
"""Filter journal entries in-place based upon search args"""
|
||||
if args.on_date:
|
||||
args.start_date = args.end_date = args.on_date
|
||||
|
||||
if args.today_in_history:
|
||||
now = time.parse("now")
|
||||
args.day = now.day
|
||||
args.month = now.month
|
||||
|
||||
journal.filter(
|
||||
tags=args.text,
|
||||
month=args.month,
|
||||
day=args.day,
|
||||
year=args.year,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
starred=args.starred,
|
||||
tagged=args.tagged,
|
||||
exclude=args.excluded,
|
||||
exclude_starred=args.exclude_starred,
|
||||
exclude_tagged=args.exclude_tagged,
|
||||
contains=args.contains,
|
||||
)
|
||||
journal.limit(args.limit)
|
||||
|
||||
|
||||
def _print_entries_found_count(count: int, args: "Namespace") -> None:
|
||||
logging.debug(f"count: {count}")
|
||||
if count == 0:
|
||||
if args.edit or args.change_time:
|
||||
print_msg(Message(MsgText.NothingToModify, MsgStyle.WARNING))
|
||||
elif args.delete:
|
||||
print_msg(Message(MsgText.NothingToDelete, MsgStyle.WARNING))
|
||||
else:
|
||||
print_msg(Message(MsgText.NoEntriesFound, MsgStyle.NORMAL))
|
||||
return
|
||||
elif args.limit and args.limit == count:
|
||||
# Don't show count if the user expects a limited number of results
|
||||
logging.debug("args.limit is true-ish")
|
||||
return
|
||||
|
||||
logging.debug("Printing general summary")
|
||||
my_msg = (
|
||||
MsgText.EntryFoundCountSingular if count == 1 else MsgText.EntryFoundCountPlural
|
||||
)
|
||||
print_msg(Message(my_msg, MsgStyle.NORMAL, {"num": count}))
|
||||
|
||||
|
||||
def _other_entries(journal: "Journal", entries: list["Entry"]) -> list["Entry"]:
|
||||
"""Find entries that are not in journal"""
|
||||
return [e for e in entries if e not in journal.entries]
|
||||
|
||||
|
||||
def _edit_search_results(
|
||||
config: dict, journal: "Journal", old_entries: list["Entry"], **kwargs
|
||||
) -> None:
|
||||
"""
|
||||
1. Send the given journal entries to the user-configured editor
|
||||
2. Print out stats on any modifications to journal
|
||||
3. Write modifications to journal
|
||||
"""
|
||||
if not config["editor"]:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.EditorNotConfigured,
|
||||
MsgStyle.ERROR,
|
||||
{"config_file": get_config_path()},
|
||||
)
|
||||
)
|
||||
|
||||
# separate entries we are not editing
|
||||
other_entries = _other_entries(journal, old_entries)
|
||||
|
||||
# Send user to the editor
|
||||
try:
|
||||
edited = get_text_from_editor(config, journal.editable_str())
|
||||
except JrnlException as e:
|
||||
if e.has_message_text(MsgText.NoTextReceived):
|
||||
raise JrnlException(
|
||||
Message(MsgText.NoEditsReceivedJournalNotDeleted, MsgStyle.WARNING)
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
||||
journal.parse_editable_str(edited)
|
||||
|
||||
# Put back entries we separated earlier, sort, and write the journal
|
||||
journal.entries += other_entries
|
||||
journal.sort()
|
||||
journal.write()
|
||||
|
||||
|
||||
def _print_changed_counts(journal: "Journal", **kwargs) -> None:
|
||||
stats = journal.get_change_counts()
|
||||
msgs = []
|
||||
|
||||
if stats["added"] > 0:
|
||||
my_msg = (
|
||||
MsgText.JournalCountAddedSingular
|
||||
if stats["added"] == 1
|
||||
else MsgText.JournalCountAddedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["added"]}))
|
||||
|
||||
if stats["deleted"] > 0:
|
||||
my_msg = (
|
||||
MsgText.JournalCountDeletedSingular
|
||||
if stats["deleted"] == 1
|
||||
else MsgText.JournalCountDeletedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["deleted"]}))
|
||||
|
||||
if stats["modified"] > 0:
|
||||
my_msg = (
|
||||
MsgText.JournalCountModifiedSingular
|
||||
if stats["modified"] == 1
|
||||
else MsgText.JournalCountModifiedPlural
|
||||
)
|
||||
msgs.append(Message(my_msg, MsgStyle.NORMAL, {"num": stats["modified"]}))
|
||||
|
||||
if not msgs:
|
||||
msgs.append(Message(MsgText.NoEditsReceived, MsgStyle.NORMAL))
|
||||
|
||||
print_msgs(msgs)
|
||||
|
||||
|
||||
def _get_predit_stats(journal: "Journal") -> dict[str, int]:
|
||||
return {"count": len(journal)}
|
||||
|
||||
|
||||
def _delete_search_results(
|
||||
journal: "Journal", old_entries: list["Entry"], **kwargs
|
||||
) -> None:
|
||||
entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion)
|
||||
|
||||
journal.entries = old_entries
|
||||
|
||||
if entries_to_delete:
|
||||
journal.delete_entries(entries_to_delete)
|
||||
|
||||
journal.write()
|
||||
|
||||
|
||||
def _change_time_search_results(
|
||||
args: "Namespace",
|
||||
journal: "Journal",
|
||||
old_entries: list["Entry"],
|
||||
no_prompt: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
# separate entries we are not editing
|
||||
# @todo if there's only 1, don't prompt
|
||||
entries_to_change = journal.prompt_action_entries(MsgText.ChangeTimeEntryQuestion)
|
||||
|
||||
if entries_to_change:
|
||||
date = time.parse(args.change_time)
|
||||
journal.entries = old_entries
|
||||
journal.change_date_entries(date, entries_to_change)
|
||||
|
||||
journal.write()
|
||||
|
||||
|
||||
def _display_search_results(args: "Namespace", journal: "Journal", **kwargs) -> None:
|
||||
if len(journal) == 0:
|
||||
return
|
||||
|
||||
# Get export format from config file if not provided at the command line
|
||||
args.export = args.export or kwargs["config"].get("display_format")
|
||||
|
||||
if args.tags:
|
||||
print(plugins.get_exporter("tags").export(journal))
|
||||
|
||||
elif args.short or args.export == "short":
|
||||
print(journal.pprint(short=True))
|
||||
|
||||
elif args.export == "pretty":
|
||||
print(journal.pprint())
|
||||
|
||||
elif args.export:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
print(exporter.export(journal, args.filename))
|
||||
else:
|
||||
print(journal.pprint())
|
||||
|
||||
|
||||
def _has_search_args(args: "Namespace") -> bool:
|
||||
"""Looking for arguments that filter a journal"""
|
||||
return any(
|
||||
(
|
||||
args.contains,
|
||||
args.tagged,
|
||||
args.excluded,
|
||||
args.exclude_starred,
|
||||
args.exclude_tagged,
|
||||
args.end_date,
|
||||
args.today_in_history,
|
||||
args.month,
|
||||
args.day,
|
||||
args.year,
|
||||
args.limit,
|
||||
args.on_date,
|
||||
args.starred,
|
||||
args.start_date,
|
||||
args.strict, # -and
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _has_action_args(args: "Namespace") -> bool:
|
||||
return any(
|
||||
(
|
||||
args.change_time,
|
||||
args.delete,
|
||||
args.edit,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _has_display_args(args: "Namespace") -> bool:
|
||||
return any(
|
||||
(
|
||||
args.tags,
|
||||
args.short,
|
||||
args.export, # --format
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _has_only_tags(tag_symbols: str, args_text: str) -> bool:
|
||||
return all(word[0] in tag_symbols for word in " ".join(args_text).split())
|
107
jrnl/editor.py
107
jrnl/editor.py
|
@ -1,18 +1,25 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .os_compat import on_windows
|
||||
from .os_compat import split_args
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.os_compat import on_windows
|
||||
from jrnl.os_compat import split_args
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.path import absolute_path
|
||||
from jrnl.path import get_templates_path
|
||||
|
||||
|
||||
def get_text_from_editor(config, template=""):
|
||||
def get_text_from_editor(config: dict, template: str = "") -> str:
|
||||
suffix = ".jrnl"
|
||||
if config["template"]:
|
||||
template_filename = Path(config["template"]).name
|
||||
|
@ -26,37 +33,89 @@ def get_text_from_editor(config, template=""):
|
|||
|
||||
try:
|
||||
subprocess.call(split_args(config["editor"]) + [tmpfile])
|
||||
except Exception as e:
|
||||
error_msg = f"""
|
||||
{ERROR_COLOR}{str(e)}{RESET_COLOR}
|
||||
|
||||
Please check the 'editor' key in your config file for errors:
|
||||
{repr(config['editor'])}
|
||||
"""
|
||||
print(textwrap.dedent(error_msg).strip(), file=sys.stderr)
|
||||
exit(1)
|
||||
except FileNotFoundError:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.EditorMisconfigured,
|
||||
MsgStyle.ERROR,
|
||||
{"editor_key": config["editor"]},
|
||||
)
|
||||
)
|
||||
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if not raw:
|
||||
print("[Nothing saved to file]", file=sys.stderr)
|
||||
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL))
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def get_text_from_stdin():
|
||||
_how_to_quit = "Ctrl+z and then Enter" if on_windows() else "Ctrl+d"
|
||||
print(
|
||||
f"[Writing Entry; on a blank line, press {_how_to_quit} to finish writing]\n",
|
||||
file=sys.stderr,
|
||||
def get_text_from_stdin() -> str:
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.WritingEntryStart,
|
||||
MsgStyle.TITLE,
|
||||
{
|
||||
"how_to_quit": (
|
||||
MsgText.HowToQuitWindows if on_windows() else MsgText.HowToQuitLinux
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
raw = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
logging.error("Write mode: keyboard interrupt")
|
||||
print("[Entry NOT saved to journal]", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
logging.error("Append mode: keyboard interrupt")
|
||||
raise JrnlException(
|
||||
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
|
||||
Message(MsgText.JournalNotSaved, MsgStyle.WARNING),
|
||||
)
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def get_template_path(template_path: str, jrnl_template_dir: str) -> str:
|
||||
actual_template_path = os.path.join(jrnl_template_dir, template_path)
|
||||
if not os.path.exists(actual_template_path):
|
||||
logging.debug(
|
||||
f"Couldn't open {actual_template_path}. "
|
||||
"Treating template path like a local / abs path."
|
||||
)
|
||||
actual_template_path = absolute_path(template_path)
|
||||
|
||||
return actual_template_path
|
||||
|
||||
|
||||
def read_template_file(template_path: str) -> str:
|
||||
"""
|
||||
Reads the template file given a template path in this order:
|
||||
|
||||
* Check $XDG_DATA_HOME/jrnl/templates/template_path.
|
||||
* Check template_arg as an absolute / relative path.
|
||||
|
||||
If a file is found, its contents are returned as a string.
|
||||
If not, a JrnlException is raised.
|
||||
"""
|
||||
|
||||
jrnl_template_dir = get_templates_path()
|
||||
actual_template_path = get_template_path(template_path, jrnl_template_dir)
|
||||
|
||||
try:
|
||||
with open(actual_template_path, encoding="utf-8") as f:
|
||||
template_data = f.read()
|
||||
return template_data
|
||||
except FileNotFoundError:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CantReadTemplate,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"template_path": template_path,
|
||||
"actual_template_path": actual_template_path,
|
||||
"jrnl_template_dir": str(jrnl_template_dir) + os.sep,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
53
jrnl/encryption/BaseEncryption.py
Normal file
53
jrnl/encryption/BaseEncryption.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
|
||||
|
||||
class BaseEncryption(ABC):
|
||||
def __init__(self, journal_name: str, config: dict):
|
||||
logging.debug("start")
|
||||
self._encoding: str = "utf-8"
|
||||
self._journal_name: str = journal_name
|
||||
self._config: dict = config
|
||||
|
||||
def clear(self) -> None:
|
||||
pass
|
||||
|
||||
def encrypt(self, text: str) -> bytes:
|
||||
logging.debug("encrypting")
|
||||
return self._encrypt(text)
|
||||
|
||||
def decrypt(self, text: bytes) -> str:
|
||||
logging.debug("decrypting")
|
||||
if (result := self._decrypt(text)) is None:
|
||||
raise JrnlException(
|
||||
Message(MsgText.DecryptionFailedGeneric, MsgStyle.ERROR)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@abstractmethod
|
||||
def _encrypt(self, text: str) -> bytes:
|
||||
"""
|
||||
This is needed because self.decrypt might need
|
||||
to perform actions (e.g. prompt for password)
|
||||
before actually encrypting.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _decrypt(self, text: bytes) -> str | None:
|
||||
"""
|
||||
This is needed because self.decrypt might need
|
||||
to perform actions (e.g. prompt for password)
|
||||
before actually decrypting.
|
||||
"""
|
||||
pass
|
8
jrnl/encryption/BaseKeyEncryption.py
Normal file
8
jrnl/encryption/BaseKeyEncryption.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from .BaseEncryption import BaseEncryption
|
||||
|
||||
|
||||
class BaseKeyEncryption(BaseEncryption):
|
||||
pass
|
82
jrnl/encryption/BasePasswordEncryption.py
Normal file
82
jrnl/encryption/BasePasswordEncryption.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
|
||||
from jrnl.encryption.BaseEncryption import BaseEncryption
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.keyring import get_keyring_password
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.prompt import create_password
|
||||
from jrnl.prompt import prompt_password
|
||||
|
||||
|
||||
class BasePasswordEncryption(BaseEncryption):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
logging.debug("start")
|
||||
self._attempts: int = 0
|
||||
self._max_attempts: int = 3
|
||||
self._password: str = ""
|
||||
self._check_keyring: bool = True
|
||||
|
||||
@property
|
||||
def check_keyring(self) -> bool:
|
||||
return self._check_keyring
|
||||
|
||||
@check_keyring.setter
|
||||
def check_keyring(self, value: bool) -> None:
|
||||
self._check_keyring = value
|
||||
|
||||
@property
|
||||
def password(self) -> str | None:
|
||||
return self._password
|
||||
|
||||
@password.setter
|
||||
def password(self, value: str) -> None:
|
||||
self._password = value
|
||||
|
||||
def clear(self):
|
||||
self.password = None
|
||||
self.check_keyring = False
|
||||
|
||||
def encrypt(self, text: str) -> bytes:
|
||||
logging.debug("encrypting")
|
||||
if not self.password:
|
||||
if self.check_keyring and (
|
||||
keyring_pw := get_keyring_password(self._journal_name)
|
||||
):
|
||||
self.password = keyring_pw
|
||||
|
||||
if not self.password:
|
||||
self.password = create_password(self._journal_name)
|
||||
|
||||
return self._encrypt(text)
|
||||
|
||||
def decrypt(self, text: bytes) -> str:
|
||||
logging.debug("decrypting")
|
||||
if not self.password:
|
||||
if self.check_keyring and (
|
||||
keyring_pw := get_keyring_password(self._journal_name)
|
||||
):
|
||||
self.password = keyring_pw
|
||||
|
||||
if not self.password:
|
||||
self._prompt_password()
|
||||
|
||||
while (result := self._decrypt(text)) is None:
|
||||
self._prompt_password()
|
||||
|
||||
return result
|
||||
|
||||
def _prompt_password(self) -> None:
|
||||
if self._attempts >= self._max_attempts:
|
||||
raise JrnlException(
|
||||
Message(MsgText.PasswordMaxTriesExceeded, MsgStyle.ERROR)
|
||||
)
|
||||
|
||||
first_try = self._attempts == 0
|
||||
self.password = prompt_password(first_try=first_try)
|
||||
self._attempts += 1
|
42
jrnl/encryption/Jrnlv1Encryption.py
Normal file
42
jrnl/encryption/Jrnlv1Encryption.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers import modes
|
||||
|
||||
from jrnl.encryption.BasePasswordEncryption import BasePasswordEncryption
|
||||
|
||||
|
||||
class Jrnlv1Encryption(BasePasswordEncryption):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
logging.debug("start")
|
||||
|
||||
def _encrypt(self, _: str) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
def _decrypt(self, text: bytes) -> str | None:
|
||||
logging.debug("decrypting")
|
||||
iv, cipher = text[:16], text[16:]
|
||||
password = self._password or ""
|
||||
decryption_key = hashlib.sha256(password.encode(self._encoding)).digest()
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(decryption_key), modes.CBC(iv), default_backend()
|
||||
).decryptor()
|
||||
try:
|
||||
plain_padded = decryptor.update(cipher) + decryptor.finalize()
|
||||
if plain_padded[-1] in (" ", 32):
|
||||
# Ancient versions of jrnl. Do not judge me.
|
||||
return plain_padded.decode(self._encoding).rstrip(" ")
|
||||
else:
|
||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||
plain = unpadder.update(plain_padded) + unpadder.finalize()
|
||||
return plain.decode(self._encoding)
|
||||
except ValueError:
|
||||
return None
|
59
jrnl/encryption/Jrnlv2Encryption.py
Normal file
59
jrnl/encryption/Jrnlv2Encryption.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.fernet import InvalidToken
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
from .BasePasswordEncryption import BasePasswordEncryption
|
||||
|
||||
|
||||
class Jrnlv2Encryption(BasePasswordEncryption):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
# Salt is hard-coded
|
||||
self._salt: bytes = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8"
|
||||
self._key: bytes = b""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
logging.debug("start")
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
@password.setter
|
||||
def password(self, value: str | None):
|
||||
self._password = value
|
||||
self._make_key()
|
||||
|
||||
def _make_key(self) -> None:
|
||||
if self._password is None:
|
||||
# Password was removed after being set
|
||||
self._key = None
|
||||
return
|
||||
password = self.password.encode(self._encoding)
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=self._salt,
|
||||
iterations=100_000,
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = kdf.derive(password)
|
||||
self._key = base64.urlsafe_b64encode(key)
|
||||
|
||||
def _encrypt(self, text: str) -> bytes:
|
||||
logging.debug("encrypting")
|
||||
return Fernet(self._key).encrypt(text.encode(self._encoding))
|
||||
|
||||
def _decrypt(self, text: bytes) -> str | None:
|
||||
logging.debug("decrypting")
|
||||
try:
|
||||
return Fernet(self._key).decrypt(text).decode(self._encoding)
|
||||
except (InvalidToken, IndexError):
|
||||
return None
|
20
jrnl/encryption/NoEncryption.py
Normal file
20
jrnl/encryption/NoEncryption.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
|
||||
from jrnl.encryption.BaseEncryption import BaseEncryption
|
||||
|
||||
|
||||
class NoEncryption(BaseEncryption):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
logging.debug("start")
|
||||
|
||||
def _encrypt(self, text: str) -> bytes:
|
||||
logging.debug("encrypting")
|
||||
return text.encode(self._encoding)
|
||||
|
||||
def _decrypt(self, text: bytes) -> str:
|
||||
logging.debug("decrypting")
|
||||
return text.decode(self._encoding)
|
36
jrnl/encryption/__init__.py
Normal file
36
jrnl/encryption/__init__.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from enum import Enum
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Type
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BaseEncryption import BaseEncryption
|
||||
|
||||
|
||||
class EncryptionMethods(str, Enum):
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
NONE = "NoEncryption"
|
||||
JRNLV1 = "Jrnlv1Encryption"
|
||||
JRNLV2 = "Jrnlv2Encryption"
|
||||
|
||||
|
||||
def determine_encryption_method(config: str | bool) -> Type["BaseEncryption"]:
|
||||
ENCRYPTION_METHODS = {
|
||||
True: EncryptionMethods.JRNLV2, # the default
|
||||
False: EncryptionMethods.NONE,
|
||||
"jrnlv1": EncryptionMethods.JRNLV1,
|
||||
"jrnlv2": EncryptionMethods.JRNLV2,
|
||||
}
|
||||
|
||||
key = config
|
||||
if isinstance(config, str):
|
||||
key = config.lower()
|
||||
|
||||
my_class = ENCRYPTION_METHODS[key]
|
||||
|
||||
return getattr(import_module(f"jrnl.encryption.{my_class}"), my_class)
|
|
@ -1,38 +1,24 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
import textwrap
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jrnl.output import print_msg
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgText
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
pass
|
||||
class JrnlException(Exception):
|
||||
"""Common exceptions raised by jrnl."""
|
||||
|
||||
def __init__(self, *messages: "Message"):
|
||||
self.messages = messages
|
||||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
def print(self) -> None:
|
||||
for msg in self.messages:
|
||||
print_msg(msg)
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JrnlError(Exception):
|
||||
"""Common exceptions raised by jrnl. """
|
||||
|
||||
def __init__(self, error_type, **kwargs):
|
||||
self.error_type = error_type
|
||||
self.message = self._get_error_message(**kwargs)
|
||||
|
||||
def _get_error_message(self, **kwargs):
|
||||
error_messages = {
|
||||
"ConfigDirectoryIsFile": textwrap.dedent(
|
||||
"""
|
||||
The path to your jrnl configuration directory is a file, not a directory:
|
||||
|
||||
{config_directory_path}
|
||||
|
||||
Removing this file will allow jrnl to save its configuration.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
return error_messages[self.error_type].format(**kwargs)
|
||||
|
||||
pass
|
||||
def has_message_text(self, message_text: "MsgText"):
|
||||
return any([m.text == message_text for m in self.messages])
|
||||
|
|
185
jrnl/install.py
185
jrnl/install.py
|
@ -1,122 +1,173 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import DEFAULT_JOURNAL_KEY
|
||||
from .config import get_config_path
|
||||
from .config import get_default_config
|
||||
from .config import get_default_journal_path
|
||||
from .config import load_config
|
||||
from .config import save_config
|
||||
from .config import verify_config_colors
|
||||
from .exception import UserAbort
|
||||
from .prompt import yesno
|
||||
from .upgrade import is_old_version
|
||||
from rich.pretty import pretty_repr
|
||||
|
||||
from jrnl import __version__
|
||||
from jrnl.config import DEFAULT_JOURNAL_KEY
|
||||
from jrnl.config import get_config_path
|
||||
from jrnl.config import get_default_colors
|
||||
from jrnl.config import get_default_config
|
||||
from jrnl.config import get_default_journal_path
|
||||
from jrnl.config import load_config
|
||||
from jrnl.config import save_config
|
||||
from jrnl.config import verify_config_colors
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.path import absolute_path
|
||||
from jrnl.path import expand_path
|
||||
from jrnl.path import home_dir
|
||||
from jrnl.prompt import yesno
|
||||
from jrnl.upgrade import is_old_version
|
||||
|
||||
|
||||
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."""
|
||||
def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> None:
|
||||
"""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. Also checks for
|
||||
existence of and difference in version number between config dict
|
||||
and current jrnl version, and if so, update the config file accordingly.
|
||||
Supply alt_config_path if using an alternate config through --config-file."""
|
||||
default_config = get_default_config()
|
||||
missing_keys = set(default_config).difference(config)
|
||||
missing_keys = set(default_config).difference(config_data)
|
||||
if missing_keys:
|
||||
for key in missing_keys:
|
||||
config[key] = default_config[key]
|
||||
save_config(config)
|
||||
print(
|
||||
f"[Configuration updated to newest version at {get_config_path()}]",
|
||||
file=sys.stderr,
|
||||
config_data[key] = default_config[key]
|
||||
|
||||
different_version = config_data["version"] != __version__
|
||||
if different_version:
|
||||
config_data["version"] = __version__
|
||||
|
||||
if missing_keys or different_version:
|
||||
save_config(config_data, alt_config_path)
|
||||
config_path = alt_config_path if alt_config_path else get_config_path()
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.ConfigUpdated, MsgStyle.NORMAL, {"config_path": config_path}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def load_or_install_jrnl():
|
||||
"""
|
||||
If jrnl is already installed, loads and returns a config object.
|
||||
Else, perform various prompts to install jrnl.
|
||||
"""
|
||||
def find_default_config() -> str:
|
||||
config_path = (
|
||||
get_config_path()
|
||||
if os.path.exists(get_config_path())
|
||||
else os.path.join(os.path.expanduser("~"), ".jrnl_config")
|
||||
else os.path.join(home_dir(), ".jrnl_config")
|
||||
)
|
||||
return config_path
|
||||
|
||||
|
||||
def find_alt_config(alt_config: str) -> str:
|
||||
if not os.path.exists(alt_config):
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.AltConfigNotFound, MsgStyle.ERROR, {"config_file": alt_config}
|
||||
)
|
||||
)
|
||||
|
||||
return alt_config
|
||||
|
||||
|
||||
def load_or_install_jrnl(alt_config_path: str) -> dict:
|
||||
"""
|
||||
If jrnl is already installed, loads and returns a default config object.
|
||||
If alternate config is specified via --config-file flag, it will be used.
|
||||
Else, perform various prompts to install jrnl.
|
||||
"""
|
||||
config_path = (
|
||||
find_alt_config(alt_config_path) if alt_config_path else find_default_config()
|
||||
)
|
||||
|
||||
if os.path.exists(config_path):
|
||||
logging.debug("Reading configuration from file %s", config_path)
|
||||
config = load_config(config_path)
|
||||
|
||||
if config is None:
|
||||
raise JrnlException(
|
||||
Message(
|
||||
MsgText.CantParseConfigFile,
|
||||
MsgStyle.ERROR,
|
||||
{
|
||||
"config_path": config_path,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if is_old_version(config_path):
|
||||
from . import upgrade
|
||||
from jrnl import upgrade
|
||||
|
||||
try:
|
||||
upgrade.upgrade_jrnl(config_path)
|
||||
except upgrade.UpgradeValidationException:
|
||||
print("Aborting upgrade.", file=sys.stderr)
|
||||
print(
|
||||
"Please tell us about this problem at the following URL:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Exiting.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
upgrade_config(config)
|
||||
upgrade_config(config, alt_config_path)
|
||||
verify_config_colors(config)
|
||||
|
||||
else:
|
||||
logging.debug("Configuration file not found, installing jrnl...")
|
||||
try:
|
||||
config = install()
|
||||
except KeyboardInterrupt:
|
||||
raise UserAbort("Installation aborted")
|
||||
|
||||
logging.debug('Using configuration "%s"', config)
|
||||
logging.debug('Using configuration:\n"%s"', pretty_repr(config))
|
||||
return config
|
||||
|
||||
|
||||
def install():
|
||||
def install() -> dict:
|
||||
_initialize_autocomplete()
|
||||
|
||||
# Where to create the journal?
|
||||
default_journal_path = get_default_journal_path()
|
||||
path_query = f"Path to your journal file (leave blank for {default_journal_path}): "
|
||||
journal_path = os.path.abspath(input(path_query).strip() or default_journal_path)
|
||||
default_config = get_default_config()
|
||||
default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser(
|
||||
os.path.expandvars(journal_path)
|
||||
user_given_path = print_msg(
|
||||
Message(
|
||||
MsgText.InstallJournalPathQuestion,
|
||||
MsgStyle.PROMPT,
|
||||
params={
|
||||
"default_journal_path": default_journal_path,
|
||||
},
|
||||
),
|
||||
get_input=True,
|
||||
)
|
||||
journal_path = absolute_path(user_given_path or default_journal_path)
|
||||
default_config = get_default_config()
|
||||
default_config["journals"][DEFAULT_JOURNAL_KEY]["journal"] = journal_path
|
||||
|
||||
# If the folder doesn't exist, create it
|
||||
path = os.path.split(default_config["journals"][DEFAULT_JOURNAL_KEY])[0]
|
||||
try:
|
||||
path = os.path.split(journal_path)[0]
|
||||
with contextlib.suppress(OSError):
|
||||
os.makedirs(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Encrypt it?
|
||||
encrypt = yesno(
|
||||
"Do you want to encrypt your journal? You can always change this later",
|
||||
default=False,
|
||||
)
|
||||
encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False)
|
||||
if encrypt:
|
||||
default_config["encrypt"] = True
|
||||
print("Journal will be encrypted.", file=sys.stderr)
|
||||
print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL))
|
||||
|
||||
# Use colors?
|
||||
use_colors = yesno(Message(MsgText.UseColorsQuestion), default=True)
|
||||
if use_colors:
|
||||
default_config["colors"] = get_default_colors()
|
||||
|
||||
save_config(default_config)
|
||||
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.InstallComplete,
|
||||
MsgStyle.NORMAL,
|
||||
params={"config_path": get_config_path()},
|
||||
)
|
||||
)
|
||||
|
||||
return default_config
|
||||
|
||||
|
||||
def _initialize_autocomplete():
|
||||
# readline is not included in Windows Active Python and perhaps some other distributions
|
||||
def _initialize_autocomplete() -> None:
|
||||
# readline is not included in Windows Active Python and perhaps some other distss
|
||||
if sys.modules.get("readline"):
|
||||
import readline
|
||||
|
||||
|
@ -125,8 +176,8 @@ def _initialize_autocomplete():
|
|||
readline.set_completer(_autocomplete_path)
|
||||
|
||||
|
||||
def _autocomplete_path(text, state):
|
||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + "*")
|
||||
def _autocomplete_path(text: str, state: int) -> list[str | None]:
|
||||
expansions = glob.glob(expand_path(text) + "*")
|
||||
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
|
||||
expansions.append(None)
|
||||
return expansions[state]
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from datetime import datetime
|
||||
import contextlib
|
||||
import datetime
|
||||
import fnmatch
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import plistlib
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import uuid
|
||||
import zoneinfo
|
||||
from pathlib import Path
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import pytz
|
||||
import tzlocal
|
||||
|
||||
from . import Entry
|
||||
from . import Journal
|
||||
from . import __title__
|
||||
from . import __version__
|
||||
from jrnl import __title__
|
||||
from jrnl import __version__
|
||||
|
||||
from .Entry import Entry
|
||||
from .Journal import Journal
|
||||
|
||||
|
||||
class DayOne(Journal.Journal):
|
||||
class DayOne(Journal):
|
||||
"""A special Journal handling DayOne files"""
|
||||
|
||||
# InvalidFileException was added to plistlib in Python3.4
|
||||
|
@ -34,13 +37,10 @@ class DayOne(Journal.Journal):
|
|||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
self._deleted_entries = []
|
||||
self.can_be_encrypted = False
|
||||
super().__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"))
|
||||
]
|
||||
def open(self) -> "DayOne":
|
||||
filenames = []
|
||||
for root, dirnames, f in os.walk(self.config["journal"]):
|
||||
for filename in fnmatch.filter(f, "*.doentry"):
|
||||
|
@ -54,16 +54,17 @@ class DayOne(Journal.Journal):
|
|||
pass
|
||||
else:
|
||||
try:
|
||||
timezone = pytz.timezone(dict_entry["Time Zone"])
|
||||
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
||||
timezone = tzlocal.get_localzone()
|
||||
timezone = zoneinfo.ZoneInfo(dict_entry["Time Zone"])
|
||||
except KeyError:
|
||||
timezone_name = str(tzlocal.get_localzone())
|
||||
timezone = zoneinfo.ZoneInfo(timezone_name)
|
||||
date = dict_entry["Creation Date"]
|
||||
# convert the date to UTC rather than keep messing with
|
||||
# timezones
|
||||
if timezone.zone != "UTC":
|
||||
date = date + timezone.utcoffset(date, is_dst=False)
|
||||
if timezone.key != "UTC":
|
||||
date = date.replace(fold=1) + timezone.utcoffset(date)
|
||||
|
||||
entry = Entry.Entry(
|
||||
entry = Entry(
|
||||
self,
|
||||
date,
|
||||
text=dict_entry["Entry Text"],
|
||||
|
@ -74,51 +75,36 @@ class DayOne(Journal.Journal):
|
|||
self.config["tagsymbols"][0] + tag.lower()
|
||||
for tag in dict_entry.get("Tags", [])
|
||||
]
|
||||
if entry._tags:
|
||||
entry._tags.sort()
|
||||
|
||||
"""Extended DayOne attributes"""
|
||||
try:
|
||||
# just ignore it if the keys don't exist
|
||||
with contextlib.suppress(KeyError):
|
||||
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
|
||||
|
||||
entry.creator_generation_date = dict_entry.get("Creator", {}).get(
|
||||
"Generation Date", date
|
||||
)
|
||||
|
||||
self.entries.append(entry)
|
||||
self.sort()
|
||||
return self
|
||||
|
||||
def write(self):
|
||||
def write(self) -> None:
|
||||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
if entry.modified:
|
||||
utc_time = datetime.utcfromtimestamp(
|
||||
utc_time = datetime.datetime.utcfromtimestamp(
|
||||
time.mktime(entry.date.timetuple())
|
||||
)
|
||||
|
||||
|
@ -178,20 +164,20 @@ class DayOne(Journal.Journal):
|
|||
)
|
||||
os.remove(filename)
|
||||
|
||||
def editable_str(self):
|
||||
def editable_str(self) -> str:
|
||||
"""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([f"{str(e)}\n# {e.uuid}\n" for e in self.entries])
|
||||
|
||||
def _update_old_entry(self, entry, new_entry):
|
||||
for attr in ("title", "body", "date"):
|
||||
def _update_old_entry(self, entry: Entry, new_entry: Entry) -> None:
|
||||
for attr in ("title", "body", "date", "tags"):
|
||||
old_attr = getattr(entry, attr)
|
||||
new_attr = getattr(new_entry, attr)
|
||||
if old_attr != new_attr:
|
||||
entry.modified = True
|
||||
setattr(entry, attr, new_attr)
|
||||
|
||||
def _get_and_remove_uuid_from_entry(self, entry):
|
||||
def _get_and_remove_uuid_from_entry(self, entry: Entry) -> Entry:
|
||||
uuid_regex = "^ *?# ([a-zA-Z0-9]+) *?$"
|
||||
m = re.search(uuid_regex, entry.body, re.MULTILINE)
|
||||
entry.uuid = m.group(1) if m else None
|
||||
|
@ -202,7 +188,7 @@ class DayOne(Journal.Journal):
|
|||
|
||||
return entry
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
def parse_editable_str(self, edited: str) -> None:
|
||||
"""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
|
||||
|
@ -212,6 +198,8 @@ class DayOne(Journal.Journal):
|
|||
|
||||
for entry in entries_from_editor:
|
||||
entry = self._get_and_remove_uuid_from_entry(entry)
|
||||
if entry._tags:
|
||||
entry._tags.sort()
|
||||
|
||||
# Remove deleted entries
|
||||
edited_uuids = [e.uuid for e in entries_from_editor]
|
||||
|
@ -221,5 +209,11 @@ class DayOne(Journal.Journal):
|
|||
for entry in entries_from_editor:
|
||||
for old_entry in self.entries:
|
||||
if entry.uuid == old_entry.uuid:
|
||||
if old_entry._tags:
|
||||
tags_not_in_body = [
|
||||
tag for tag in old_entry._tags if (tag not in entry._body)
|
||||
]
|
||||
if tags_not_in_body:
|
||||
entry._tags.extend(tags_not_in_body.sort())
|
||||
self._update_old_entry(old_entry, entry)
|
||||
break
|
107
jrnl/Entry.py → jrnl/journals/Entry.py
Executable file → Normal file
107
jrnl/Entry.py → jrnl/journals/Entry.py
Executable file → Normal file
|
@ -1,21 +1,30 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ansiwrap
|
||||
from jrnl.color import colorize
|
||||
from jrnl.color import highlight_tags_with_background_color
|
||||
from jrnl.output import wrap_with_ansi_colors
|
||||
|
||||
from .color import colorize
|
||||
from .color import highlight_tags_with_background_color
|
||||
if TYPE_CHECKING:
|
||||
from .Journal import Journal
|
||||
|
||||
|
||||
class Entry:
|
||||
def __init__(self, journal, date=None, text="", starred=False):
|
||||
def __init__(
|
||||
self,
|
||||
journal: "Journal",
|
||||
date: datetime.datetime | None = None,
|
||||
text: str = "",
|
||||
starred: bool = False,
|
||||
):
|
||||
self.journal = journal # Reference to journal mainly to access its config
|
||||
self.date = date or datetime.now()
|
||||
self.date = date or datetime.datetime.now()
|
||||
self.text = text
|
||||
self._title = None
|
||||
self._body = None
|
||||
|
@ -24,7 +33,7 @@ class Entry:
|
|||
self.modified = False
|
||||
|
||||
@property
|
||||
def fulltext(self):
|
||||
def fulltext(self) -> str:
|
||||
return self.title + " " + self.body
|
||||
|
||||
def _parse_text(self):
|
||||
|
@ -38,48 +47,48 @@ class Entry:
|
|||
self._tags = list(self._parse_tags())
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
if self._title is None:
|
||||
self._parse_text()
|
||||
return self._title
|
||||
|
||||
@title.setter
|
||||
def title(self, x):
|
||||
def title(self, x: str):
|
||||
self._title = x
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
def body(self) -> str:
|
||||
if self._body is None:
|
||||
self._parse_text()
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, x):
|
||||
def body(self, x: str):
|
||||
self._body = x
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
def tags(self) -> list[str]:
|
||||
if self._tags is None:
|
||||
self._parse_text()
|
||||
return self._tags
|
||||
|
||||
@tags.setter
|
||||
def tags(self, x):
|
||||
def tags(self, x: list[str]):
|
||||
self._tags = x
|
||||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = fr"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
|
||||
def tag_regex(tagsymbols: str) -> re.Pattern:
|
||||
pattern = rf"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
|
||||
return re.compile(pattern)
|
||||
|
||||
def _parse_tags(self):
|
||||
def _parse_tags(self) -> set[str]:
|
||||
tagsymbols = self.journal.config["tagsymbols"]
|
||||
return {
|
||||
tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Returns a string representation of the entry to be written into a journal file."""
|
||||
"""Returns string representation of the entry to be written to journal file."""
|
||||
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||
if self.starred:
|
||||
|
@ -90,7 +99,7 @@ class Entry:
|
|||
body=self.body.rstrip("\n "),
|
||||
)
|
||||
|
||||
def pprint(self, short=False):
|
||||
def pprint(self, short: bool = False) -> str:
|
||||
"""Returns a pretty-printed version of the entry.
|
||||
If short is true, only print the title."""
|
||||
# Handle indentation
|
||||
|
@ -106,8 +115,20 @@ class Entry:
|
|||
)
|
||||
|
||||
if not short and self.journal.config["linewrap"]:
|
||||
columns = self.journal.config["linewrap"]
|
||||
|
||||
if columns == "auto":
|
||||
try:
|
||||
columns = os.get_terminal_size().columns
|
||||
except OSError:
|
||||
logging.debug(
|
||||
"Can't determine terminal size automatically 'linewrap': '%s'",
|
||||
self.journal.config["linewrap"],
|
||||
)
|
||||
columns = 79
|
||||
|
||||
# Color date / title and bold title
|
||||
title = ansiwrap.fill(
|
||||
title = wrap_with_ansi_colors(
|
||||
date_str
|
||||
+ " "
|
||||
+ highlight_tags_with_background_color(
|
||||
|
@ -116,40 +137,22 @@ class Entry:
|
|||
self.journal.config["colors"]["title"],
|
||||
is_title=True,
|
||||
),
|
||||
self.journal.config["linewrap"],
|
||||
columns,
|
||||
)
|
||||
body = highlight_tags_with_background_color(
|
||||
self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"]
|
||||
)
|
||||
body_text = [
|
||||
colorize(
|
||||
ansiwrap.fill(
|
||||
line,
|
||||
self.journal.config["linewrap"],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True,
|
||||
),
|
||||
self.journal.config["colors"]["body"],
|
||||
)
|
||||
or indent
|
||||
for line in body.rstrip(" \n").splitlines()
|
||||
]
|
||||
|
||||
# ansiwrap doesn't handle lines with only the "\n" character and some
|
||||
# ANSI escapes properly, so we have this hack here to make sure the
|
||||
# beginning of each line has the indent character and it's colored
|
||||
# properly. textwrap doesn't have this issue, however, it doesn't wrap
|
||||
# the strings properly as it counts ANSI escapes as literal characters.
|
||||
# TL;DR: I'm sorry.
|
||||
body = wrap_with_ansi_colors(body, columns - len(indent))
|
||||
if indent:
|
||||
# Without explicitly colorizing the indent character, it will lose its
|
||||
# color after a tag appears.
|
||||
body = "\n".join(
|
||||
[
|
||||
colorize(indent, self.journal.config["colors"]["body"]) + line
|
||||
if not ansiwrap.strip_color(line).startswith(indent)
|
||||
else line
|
||||
for line in body_text
|
||||
]
|
||||
for line in body.splitlines()
|
||||
)
|
||||
|
||||
body = colorize(body, self.journal.config["colors"]["body"])
|
||||
else:
|
||||
title = (
|
||||
date_str
|
||||
|
@ -185,7 +188,7 @@ class Entry:
|
|||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: "Entry"):
|
||||
if (
|
||||
not isinstance(other, Entry)
|
||||
or self.title.strip() != other.title.strip()
|
||||
|
@ -196,7 +199,7 @@ class Entry:
|
|||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: "Entry"):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
|
@ -211,14 +214,14 @@ SENTENCE_SPLITTER = re.compile(
|
|||
\s+ # AND a sequence of required spaces.
|
||||
)
|
||||
|[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces.
|
||||
""",
|
||||
""", # noqa: E501
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n")
|
||||
|
||||
|
||||
def split_title(text):
|
||||
def split_title(text: str) -> tuple[str, str]:
|
||||
"""Splits the first sentence off from a text."""
|
||||
sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip())
|
||||
if not sep:
|
158
jrnl/journals/FolderJournal.py
Normal file
158
jrnl/journals/FolderJournal.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jrnl import time
|
||||
|
||||
from .Journal import Journal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.journals import Entry
|
||||
|
||||
# glob search patterns for folder/file structure
|
||||
DIGIT_PATTERN = "[0123456789]"
|
||||
YEAR_PATTERN = DIGIT_PATTERN * 4
|
||||
MONTH_PATTERN = "[01]" + DIGIT_PATTERN
|
||||
DAY_PATTERN = "[0123]" + DIGIT_PATTERN + ".txt"
|
||||
|
||||
|
||||
class Folder(Journal):
|
||||
"""A Journal handling multiple files in a folder"""
|
||||
|
||||
def __init__(self, name: str = "default", **kwargs):
|
||||
self.entries = []
|
||||
self._diff_entry_dates = []
|
||||
self.can_be_encrypted = False
|
||||
super().__init__(name, **kwargs)
|
||||
|
||||
def open(self) -> "Folder":
|
||||
filenames = []
|
||||
self.entries = []
|
||||
|
||||
if os.path.exists(self.config["journal"]):
|
||||
filenames = Folder._get_files(self.config["journal"])
|
||||
for filename in filenames:
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
journal = f.read()
|
||||
self.entries.extend(self._parse(journal))
|
||||
self.sort()
|
||||
|
||||
return self
|
||||
|
||||
def write(self) -> None:
|
||||
"""Writes only the entries that have been modified into proper files."""
|
||||
# Create a list of dates of modified entries. Start with diff_entry_dates
|
||||
modified_dates = self._diff_entry_dates
|
||||
seen_dates = set(self._diff_entry_dates)
|
||||
|
||||
for e in self.entries:
|
||||
if e.modified:
|
||||
if e.date not in modified_dates:
|
||||
modified_dates.append(e.date)
|
||||
if e.date not in seen_dates:
|
||||
seen_dates.add(e.date)
|
||||
|
||||
# For every date that had a modified entry, write to a file
|
||||
for d in modified_dates:
|
||||
write_entries = []
|
||||
filename = os.path.join(
|
||||
self.config["journal"],
|
||||
d.strftime("%Y"),
|
||||
d.strftime("%m"),
|
||||
d.strftime("%d") + ".txt",
|
||||
)
|
||||
dirname = os.path.dirname(filename)
|
||||
# create directory if it doesn't exist
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
for e in self.entries:
|
||||
if (
|
||||
e.date.year == d.year
|
||||
and e.date.month == d.month
|
||||
and e.date.day == d.day
|
||||
):
|
||||
write_entries.append(e)
|
||||
journal = "\n".join([e.__str__() for e in write_entries])
|
||||
with codecs.open(filename, "w", "utf-8") as journal_file:
|
||||
journal_file.write(journal)
|
||||
# look for and delete empty files
|
||||
filenames = []
|
||||
filenames = Folder._get_files(self.config["journal"])
|
||||
for filename in filenames:
|
||||
if os.stat(filename).st_size <= 0:
|
||||
os.remove(filename)
|
||||
|
||||
def delete_entries(self, entries_to_delete: list["Entry"]) -> None:
|
||||
"""Deletes specific entries from a journal."""
|
||||
for entry in entries_to_delete:
|
||||
self.entries.remove(entry)
|
||||
self._diff_entry_dates.append(entry.date)
|
||||
self.deleted_entry_count += 1
|
||||
|
||||
def change_date_entries(self, date: str, entries_to_change: list["Entry"]) -> None:
|
||||
"""Changes entry dates to given date."""
|
||||
|
||||
date = time.parse(date)
|
||||
|
||||
self._diff_entry_dates.append(date)
|
||||
|
||||
for entry in entries_to_change:
|
||||
self._diff_entry_dates.append(entry.date)
|
||||
entry.date = date
|
||||
entry.modified = True
|
||||
|
||||
def parse_editable_str(self, edited: str) -> None:
|
||||
"""Parses the output of self.editable_str and updates its entries."""
|
||||
mod_entries = self._parse(edited)
|
||||
diff_entries = set(self.entries) - set(mod_entries)
|
||||
for e in diff_entries:
|
||||
self._diff_entry_dates.append(e.date)
|
||||
# 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.increment_change_counts_by_edit(mod_entries)
|
||||
self.entries = mod_entries
|
||||
|
||||
@staticmethod
|
||||
def _get_files(journal_path: str) -> list[str]:
|
||||
"""Searches through sub directories starting with journal_path and find all text
|
||||
files that look like entries"""
|
||||
for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)):
|
||||
for month_folder in Folder._get_month_folders(year_folder):
|
||||
yield from Folder._get_day_files(month_folder)
|
||||
|
||||
@staticmethod
|
||||
def _get_year_folders(path: pathlib.Path) -> list[pathlib.Path]:
|
||||
for child in path.glob(YEAR_PATTERN):
|
||||
if child.is_dir():
|
||||
yield child
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _get_month_folders(path: pathlib.Path) -> list[pathlib.Path]:
|
||||
for child in path.glob(MONTH_PATTERN):
|
||||
if int(child.name) > 0 and int(child.name) <= 12 and path.is_dir():
|
||||
yield child
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def _get_day_files(path: pathlib.Path) -> list[str]:
|
||||
for child in path.glob(DAY_PATTERN):
|
||||
if (
|
||||
int(child.stem) > 0
|
||||
and int(child.stem) <= 31
|
||||
and time.is_valid_date(
|
||||
year=int(path.parent.name),
|
||||
month=int(path.name),
|
||||
day=int(child.stem),
|
||||
)
|
||||
and child.is_file()
|
||||
):
|
||||
yield str(child)
|
|
@ -1,17 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from . import Entry
|
||||
from . import time
|
||||
from .prompt import yesno
|
||||
from jrnl import time
|
||||
from jrnl.config import validate_journal_name
|
||||
from jrnl.encryption import determine_encryption_method
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
from jrnl.path import expand_path
|
||||
from jrnl.prompt import yesno
|
||||
|
||||
from .Entry import Entry
|
||||
|
||||
|
||||
class Tag:
|
||||
|
@ -44,6 +49,11 @@ class Journal:
|
|||
self.search_tags = None # Store tags we're highlighting
|
||||
self.name = name
|
||||
self.entries = []
|
||||
self.encryption_method = None
|
||||
|
||||
# Track changes to journal in session. Modified is tracked in Entry
|
||||
self.added_entry_count = 0
|
||||
self.deleted_entry_count = 0
|
||||
|
||||
def __len__(self):
|
||||
"""Returns the number of entries"""
|
||||
|
@ -54,7 +64,7 @@ class Journal:
|
|||
return (entry for entry in self.entries)
|
||||
|
||||
@classmethod
|
||||
def from_journal(cls, other):
|
||||
def from_journal(cls, other: "Journal") -> "Journal":
|
||||
"""Creates a new journal by copying configuration and entries from
|
||||
another journal object"""
|
||||
new_journal = cls(other.name, **other.config)
|
||||
|
@ -67,60 +77,94 @@ class Journal:
|
|||
)
|
||||
return new_journal
|
||||
|
||||
def import_(self, other_journal_txt):
|
||||
self.entries = list(
|
||||
frozenset(self.entries) | frozenset(self._parse(other_journal_txt))
|
||||
)
|
||||
def import_(self, other_journal_txt: str) -> None:
|
||||
imported_entries = self._parse(other_journal_txt)
|
||||
for entry in imported_entries:
|
||||
entry.modified = True
|
||||
|
||||
self.entries = list(frozenset(self.entries) | frozenset(imported_entries))
|
||||
self.sort()
|
||||
|
||||
def open(self, filename=None):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
def _get_encryption_method(self) -> None:
|
||||
encryption_method = determine_encryption_method(self.config["encrypt"])
|
||||
self.encryption_method = encryption_method(self.name, self.config)
|
||||
|
||||
def _decrypt(self, text: bytes) -> str:
|
||||
if self.encryption_method is None:
|
||||
self._get_encryption_method()
|
||||
|
||||
return self.encryption_method.decrypt(text)
|
||||
|
||||
def _encrypt(self, text: str) -> bytes:
|
||||
if self.encryption_method is None:
|
||||
self._get_encryption_method()
|
||||
|
||||
return self.encryption_method.encrypt(text)
|
||||
|
||||
def open(self, filename: str | None = None) -> "Journal":
|
||||
"""Opens the journal file and parses it into a list of Entries
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config["journal"]
|
||||
dirname = os.path.dirname(filename)
|
||||
if not os.path.exists(filename):
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
print(f"[Directory {dirname} created]", file=sys.stderr)
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.DirectoryCreated,
|
||||
MsgStyle.NORMAL,
|
||||
{"directory_name": dirname},
|
||||
)
|
||||
)
|
||||
self.create_file(filename)
|
||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.JournalCreated,
|
||||
MsgStyle.NORMAL,
|
||||
{
|
||||
"journal_name": self.name,
|
||||
"filename": filename,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.write()
|
||||
|
||||
text = self._load(filename)
|
||||
text = self._decrypt(text)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
logging.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def write(self, filename=None):
|
||||
def write(self, filename: str | None = None) -> None:
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
filename = filename or self.config["journal"]
|
||||
text = self._to_text()
|
||||
text = self._encrypt(text)
|
||||
self._store(filename, text)
|
||||
|
||||
def validate_parsing(self):
|
||||
"""Confirms that the jrnl is still parsed correctly after being dumped to text."""
|
||||
def validate_parsing(self) -> bool:
|
||||
"""Confirms that the jrnl is still parsed correctly after conversion to text."""
|
||||
new_entries = self._parse(self._to_text())
|
||||
for i, entry in enumerate(self.entries):
|
||||
if entry != new_entries[i]:
|
||||
return False
|
||||
return True
|
||||
return all(entry == new_entries[i] for i, entry in enumerate(self.entries))
|
||||
|
||||
@staticmethod
|
||||
def create_file(filename):
|
||||
def create_file(filename: str) -> None:
|
||||
with open(filename, "w"):
|
||||
pass
|
||||
|
||||
def _to_text(self):
|
||||
def _to_text(self) -> str:
|
||||
return "\n".join([str(e) for e in self.entries])
|
||||
|
||||
def _load(self, filename):
|
||||
raise NotImplementedError
|
||||
def _load(self, filename: str) -> bytes:
|
||||
with open(filename, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
@classmethod
|
||||
def _store(filename, text):
|
||||
raise NotImplementedError
|
||||
def _store(self, filename: str, text: bytes) -> None:
|
||||
with open(filename, "wb") as f:
|
||||
f.write(text)
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
def _parse(self, journal_txt: str) -> list[Entry]:
|
||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||
|
||||
# Return empty array if the journal is blank
|
||||
|
@ -135,7 +179,9 @@ class Journal:
|
|||
for match in date_blob_re.finditer(journal_txt):
|
||||
date_blob = match.groups()[0]
|
||||
try:
|
||||
new_date = datetime.strptime(date_blob, self.config["timeformat"])
|
||||
new_date = datetime.datetime.strptime(
|
||||
date_blob, self.config["timeformat"]
|
||||
)
|
||||
except ValueError:
|
||||
# Passing in a date that had brackets around it
|
||||
new_date = time.parse(date_blob, bracketed=True)
|
||||
|
@ -144,11 +190,11 @@ class Journal:
|
|||
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))
|
||||
entries.append(Entry(self, date=new_date))
|
||||
|
||||
# If no entries were found, treat all the existing text as an entry made now
|
||||
if not entries:
|
||||
entries.append(Entry.Entry(self, date=time.parse("now")))
|
||||
entries.append(Entry(self, date=time.parse("now")))
|
||||
|
||||
# Fill in the text of the last entry
|
||||
entries[-1].text = journal_txt[last_entry_pos:]
|
||||
|
@ -157,7 +203,7 @@ class Journal:
|
|||
entry._parse_text()
|
||||
return entries
|
||||
|
||||
def pprint(self, short=False):
|
||||
def pprint(self, short: bool = False) -> str:
|
||||
"""Prettyprints the journal's entries"""
|
||||
return "\n".join([e.pprint(short=short) for e in self.entries])
|
||||
|
||||
|
@ -167,20 +213,21 @@ class Journal:
|
|||
def __repr__(self):
|
||||
return f"<Journal with {len(self.entries)} entries>"
|
||||
|
||||
def sort(self):
|
||||
def sort(self) -> None:
|
||||
"""Sorts the Journal's entries by date"""
|
||||
self.entries = sorted(self.entries, key=lambda entry: entry.date)
|
||||
|
||||
def limit(self, n=None):
|
||||
def limit(self, n: int | None = None) -> None:
|
||||
"""Removes all but the last n entries"""
|
||||
if n:
|
||||
self.entries = self.entries[-n:]
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
def tags(self) -> list[Tag]:
|
||||
"""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.
|
||||
# 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 = {(tags.count(tag), tag) for tag in tags}
|
||||
|
@ -195,8 +242,11 @@ class Journal:
|
|||
start_date=None,
|
||||
end_date=None,
|
||||
starred=False,
|
||||
tagged=False,
|
||||
exclude_starred=False,
|
||||
exclude_tagged=False,
|
||||
strict=False,
|
||||
contains=None,
|
||||
contains=[],
|
||||
exclude=[],
|
||||
):
|
||||
"""Removes all entries from the journal that don't match the filter.
|
||||
|
@ -218,10 +268,15 @@ class Journal:
|
|||
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
|
||||
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
|
||||
has_tags = (
|
||||
self.search_tags.issubset if strict else self.search_tags.intersection
|
||||
)
|
||||
|
||||
def excluded(tags):
|
||||
return 0 < len([tag for tag in tags if tag in excluded_tags])
|
||||
|
||||
if contains:
|
||||
contains_lower = contains.casefold()
|
||||
contains_lower = [substring.casefold() for substring in contains]
|
||||
|
||||
# Create datetime object for comparison below
|
||||
# this approach allows various formats
|
||||
|
@ -231,8 +286,9 @@ class Journal:
|
|||
result = [
|
||||
entry
|
||||
for entry in self.entries
|
||||
if (not tags or tagged(entry.tags))
|
||||
and (not starred or entry.starred)
|
||||
if (not tags or has_tags(entry.tags))
|
||||
and (not (starred or exclude_starred) or entry.starred == starred)
|
||||
and (not (tagged or exclude_tagged) or bool(entry.tags) == tagged)
|
||||
and (not month or entry.date.month == compare_d.month)
|
||||
and (not day or entry.date.day == compare_d.day)
|
||||
and (not year or entry.date.year == compare_d.year)
|
||||
|
@ -242,40 +298,67 @@ class Journal:
|
|||
and (
|
||||
not contains
|
||||
or (
|
||||
contains_lower in entry.title.casefold()
|
||||
or contains_lower in entry.body.casefold()
|
||||
strict
|
||||
and all(
|
||||
substring in entry.title.casefold()
|
||||
or substring in entry.body.casefold()
|
||||
for substring in contains_lower
|
||||
)
|
||||
)
|
||||
or (
|
||||
not strict
|
||||
and any(
|
||||
substring in entry.title.casefold()
|
||||
or substring in entry.body.casefold()
|
||||
for substring in contains_lower
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
self.entries = result
|
||||
|
||||
def delete_entries(self, entries_to_delete):
|
||||
def delete_entries(self, entries_to_delete: list[Entry]) -> None:
|
||||
"""Deletes specific entries from a journal."""
|
||||
for entry in entries_to_delete:
|
||||
self.entries.remove(entry)
|
||||
self.deleted_entry_count += 1
|
||||
|
||||
def prompt_delete_entries(self):
|
||||
"""Prompts for deletion of each of the entries in a journal.
|
||||
Returns the entries the user wishes to delete."""
|
||||
def change_date_entries(
|
||||
self, date: datetime.datetime, entries_to_change: list[Entry]
|
||||
) -> None:
|
||||
"""Changes entry dates to given date."""
|
||||
date = time.parse(date)
|
||||
|
||||
to_delete = []
|
||||
for entry in entries_to_change:
|
||||
entry.date = date
|
||||
entry.modified = True
|
||||
|
||||
def ask_delete(entry):
|
||||
def prompt_action_entries(self, msg: MsgText) -> list[Entry]:
|
||||
"""Prompts for action for each entry in a journal, using given message.
|
||||
Returns the entries the user wishes to apply the action on."""
|
||||
to_act = []
|
||||
|
||||
def ask_action(entry):
|
||||
return yesno(
|
||||
f"Delete entry '{entry.pprint(short=True)}'?",
|
||||
Message(
|
||||
msg,
|
||||
params={"entry_title": entry.pprint(short=True)},
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
for entry in self.entries:
|
||||
if ask_delete(entry):
|
||||
to_delete.append(entry)
|
||||
if ask_action(entry):
|
||||
to_act.append(entry)
|
||||
|
||||
return to_delete
|
||||
return to_act
|
||||
|
||||
def new_entry(self, raw, date=None, sort=True):
|
||||
def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry:
|
||||
"""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 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")
|
||||
# Split raw text into title and body
|
||||
|
@ -302,19 +385,19 @@ class Journal:
|
|||
)
|
||||
if not date: # Still nothing? Meh, just live in the moment.
|
||||
date = time.parse("now")
|
||||
entry = Entry.Entry(self, date, raw, starred=starred)
|
||||
entry = Entry(self, date, raw, starred=starred)
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
if sort:
|
||||
self.sort()
|
||||
return entry
|
||||
|
||||
def editable_str(self):
|
||||
def editable_str(self) -> str:
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
manually and later be parsed with eslf.parse_editable_str."""
|
||||
manually and later be parsed with self.parse_editable_str."""
|
||||
return "\n".join([str(e) for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
def parse_editable_str(self, edited: str) -> None:
|
||||
"""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
|
||||
|
@ -322,17 +405,23 @@ class Journal:
|
|||
# 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.increment_change_counts_by_edit(mod_entries)
|
||||
|
||||
self.entries = mod_entries
|
||||
|
||||
def increment_change_counts_by_edit(self, mod_entries: Entry) -> None:
|
||||
if len(mod_entries) > len(self.entries):
|
||||
self.added_entry_count += len(mod_entries) - len(self.entries)
|
||||
else:
|
||||
self.deleted_entry_count += len(self.entries) - len(mod_entries)
|
||||
|
||||
class PlainJournal(Journal):
|
||||
def _load(self, filename):
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _store(self, filename, text):
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
def get_change_counts(self) -> dict:
|
||||
return {
|
||||
"added": self.added_entry_count,
|
||||
"deleted": self.deleted_entry_count,
|
||||
"modified": len([e for e in self.entries if e.modified]),
|
||||
}
|
||||
|
||||
|
||||
class LegacyJournal(Journal):
|
||||
|
@ -340,15 +429,11 @@ class LegacyJournal(Journal):
|
|||
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 open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
def _parse(self, journal_txt: str) -> list[Entry]:
|
||||
"""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"]))
|
||||
date_length = len(datetime.datetime.today().strftime(self.config["timeformat"]))
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
|
@ -358,7 +443,7 @@ class LegacyJournal(Journal):
|
|||
line = line.rstrip()
|
||||
try:
|
||||
# try to parse line as date => new entry begins
|
||||
new_date = datetime.strptime(
|
||||
new_date = datetime.datetime.strptime(
|
||||
line[:date_length], self.config["timeformat"]
|
||||
)
|
||||
|
||||
|
@ -372,7 +457,7 @@ class LegacyJournal(Journal):
|
|||
else:
|
||||
starred = False
|
||||
|
||||
current_entry = Entry.Entry(
|
||||
current_entry = Entry(
|
||||
self, date=new_date, text=line[date_length + 1 :], starred=starred
|
||||
)
|
||||
except ValueError:
|
||||
|
@ -391,41 +476,50 @@ class LegacyJournal(Journal):
|
|||
return entries
|
||||
|
||||
|
||||
def open_journal(journal_name, config, legacy=False):
|
||||
def open_journal(journal_name: str, config: dict, legacy: bool = False) -> Journal:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
logging.debug(f"open_journal '{journal_name}'")
|
||||
validate_journal_name(journal_name, config)
|
||||
config = config.copy()
|
||||
config["journal"] = os.path.expanduser(os.path.expandvars(config["journal"]))
|
||||
config["journal"] = expand_path(config["journal"])
|
||||
|
||||
if os.path.isdir(config["journal"]):
|
||||
if config["encrypt"]:
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.ConfigEncryptedForUnencryptableJournalType,
|
||||
MsgStyle.WARNING,
|
||||
{
|
||||
"journal_name": journal_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
||||
config["journal"]
|
||||
):
|
||||
from . import DayOneJournal
|
||||
from jrnl.journals import DayOne
|
||||
|
||||
return DayOneJournal.DayOne(**config).open()
|
||||
return DayOne(**config).open()
|
||||
else:
|
||||
from . import FolderJournal
|
||||
from jrnl.journals import Folder
|
||||
|
||||
return FolderJournal.Folder(**config).open()
|
||||
return Folder(journal_name, **config).open()
|
||||
|
||||
if not config["encrypt"]:
|
||||
if legacy:
|
||||
return LegacyJournal(journal_name, **config).open()
|
||||
return PlainJournal(journal_name, **config).open()
|
||||
if config["journal"].endswith(os.sep):
|
||||
from jrnl.journals import Folder
|
||||
|
||||
from . import EncryptedJournal
|
||||
return Folder(journal_name, **config).open()
|
||||
return Journal(journal_name, **config).open()
|
||||
|
||||
try:
|
||||
if legacy:
|
||||
return EncryptedJournal.LegacyEncryptedJournal(
|
||||
journal_name, **config
|
||||
).open()
|
||||
return EncryptedJournal.EncryptedJournal(journal_name, **config).open()
|
||||
except KeyboardInterrupt:
|
||||
# Since encrypted journals prompt for a password, it's easy for a user to ctrl+c out
|
||||
print("[Interrupted while opening journal]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
config["encrypt"] = "jrnlv1"
|
||||
return LegacyJournal(journal_name, **config).open()
|
||||
return Journal(journal_name, **config).open()
|
5
jrnl/journals/__init__.py
Normal file
5
jrnl/journals/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from .DayOneJournal import DayOne
|
||||
from .Entry import Entry
|
||||
from .FolderJournal import Folder
|
||||
from .Journal import Journal
|
||||
from .Journal import open_journal
|
332
jrnl/jrnl.py
332
jrnl/jrnl.py
|
@ -1,332 +0,0 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from . import install
|
||||
from . import plugins
|
||||
from .Journal import open_journal
|
||||
from .color import ERROR_COLOR
|
||||
from .color import RESET_COLOR
|
||||
from .config import get_journal_name
|
||||
from .config import scope_config
|
||||
from .config import get_config_path
|
||||
from .editor import get_text_from_editor
|
||||
from .editor import get_text_from_stdin
|
||||
from .exception import UserAbort
|
||||
from . import time
|
||||
|
||||
|
||||
def run(args):
|
||||
"""
|
||||
Flow:
|
||||
1. Run standalone command if it doesn't require config (help, version, etc), then exit
|
||||
2. Load config
|
||||
3. Run standalone command if it does require config (encrypt, decrypt, etc), then exit
|
||||
4. Load specified journal
|
||||
5. Start write mode, or search mode
|
||||
6. Profit
|
||||
"""
|
||||
|
||||
# Run command if possible before config is available
|
||||
if callable(args.preconfig_cmd):
|
||||
return args.preconfig_cmd(args)
|
||||
|
||||
# Load the config, and extract journal name
|
||||
try:
|
||||
config = install.load_or_install_jrnl()
|
||||
original_config = config.copy()
|
||||
args = get_journal_name(args, config)
|
||||
config = scope_config(config, args.journal_name)
|
||||
except UserAbort as err:
|
||||
print(f"\n{err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Run post-config command now that config is ready
|
||||
if callable(args.postconfig_cmd):
|
||||
return args.postconfig_cmd(
|
||||
args=args, config=config, original_config=original_config
|
||||
)
|
||||
|
||||
# --- All the standalone commands are now done --- #
|
||||
|
||||
# Get the journal we're going to be working with
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
||||
kwargs = {
|
||||
"args": args,
|
||||
"config": config,
|
||||
"journal": journal,
|
||||
}
|
||||
|
||||
if _is_write_mode(**kwargs):
|
||||
write_mode(**kwargs)
|
||||
else:
|
||||
search_mode(**kwargs)
|
||||
|
||||
|
||||
def _is_write_mode(args, config, **kwargs):
|
||||
"""Determines if we are in write mode (as opposed to search mode)"""
|
||||
write_mode = True
|
||||
|
||||
# Are any search filters present? If so, then search mode.
|
||||
write_mode = not any(
|
||||
(
|
||||
args.contains,
|
||||
args.delete,
|
||||
args.edit,
|
||||
args.export,
|
||||
args.end_date,
|
||||
args.today_in_history,
|
||||
args.month,
|
||||
args.day,
|
||||
args.year,
|
||||
args.limit,
|
||||
args.on_date,
|
||||
args.short,
|
||||
args.starred,
|
||||
args.start_date,
|
||||
args.strict,
|
||||
args.tags,
|
||||
)
|
||||
)
|
||||
|
||||
# Might be writing and want to move to editor part of the way through
|
||||
if args.edit and args.text:
|
||||
write_mode = True
|
||||
|
||||
# If the text is entirely tags, then we are also searching (not writing)
|
||||
if (
|
||||
write_mode
|
||||
and args.text
|
||||
and all(word[0] in config["tagsymbols"] for word in " ".join(args.text).split())
|
||||
):
|
||||
write_mode = False
|
||||
|
||||
return write_mode
|
||||
|
||||
|
||||
def write_mode(args, config, journal, **kwargs):
|
||||
"""
|
||||
Gets input from the user to write to the journal
|
||||
1. Check for input from cli
|
||||
2. Check input being piped in
|
||||
3. Open editor if configured (prepopulated with template if available)
|
||||
4. Use stdin.read as last resort
|
||||
6. Write any found text to journal, or exit
|
||||
"""
|
||||
logging.debug("Write mode: starting")
|
||||
|
||||
if args.text:
|
||||
logging.debug("Write mode: cli text detected: %s", args.text)
|
||||
raw = " ".join(args.text).strip()
|
||||
if args.edit:
|
||||
raw = _write_in_editor(config, raw)
|
||||
|
||||
elif not sys.stdin.isatty():
|
||||
logging.debug("Write mode: receiving piped text")
|
||||
raw = sys.stdin.read()
|
||||
|
||||
else:
|
||||
raw = _write_in_editor(config)
|
||||
|
||||
if not raw:
|
||||
logging.error("Write mode: couldn't get raw text")
|
||||
sys.exit()
|
||||
|
||||
logging.debug(
|
||||
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw
|
||||
)
|
||||
journal.new_entry(raw)
|
||||
print(f"[Entry added to {args.journal_name} journal]", file=sys.stderr)
|
||||
journal.write()
|
||||
logging.debug("Write mode: completed journal.write()", args.journal_name, raw)
|
||||
|
||||
|
||||
def search_mode(args, journal, **kwargs):
|
||||
"""
|
||||
Search for entries in a journal, then either:
|
||||
1. Send them to configured editor for user manipulation
|
||||
2. Delete them (with confirmation for each entry)
|
||||
3. Display them (with formatting options)
|
||||
"""
|
||||
kwargs = {
|
||||
**kwargs,
|
||||
"args": args,
|
||||
"journal": journal,
|
||||
"old_entries": journal.entries,
|
||||
}
|
||||
|
||||
# Filters the journal entries in place
|
||||
_search_journal(**kwargs)
|
||||
|
||||
# Where do the search results go?
|
||||
if args.edit:
|
||||
_edit_search_results(**kwargs)
|
||||
|
||||
elif args.delete:
|
||||
_delete_search_results(**kwargs)
|
||||
|
||||
else:
|
||||
_display_search_results(**kwargs)
|
||||
|
||||
|
||||
def _write_in_editor(config, template=None):
|
||||
if config["editor"]:
|
||||
logging.debug("Write mode: opening editor")
|
||||
if not template:
|
||||
template = _get_editor_template(config)
|
||||
raw = get_text_from_editor(config, template)
|
||||
|
||||
else:
|
||||
raw = get_text_from_stdin()
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def _get_editor_template(config, **kwargs):
|
||||
logging.debug("Write mode: loading template for entry")
|
||||
|
||||
if not config["template"]:
|
||||
logging.debug("Write mode: no template configured")
|
||||
return ""
|
||||
|
||||
try:
|
||||
template = open(config["template"]).read()
|
||||
logging.debug("Write mode: template loaded: %s", template)
|
||||
except OSError:
|
||||
logging.error("Write mode: template not loaded")
|
||||
print(
|
||||
f"[Could not read template at '{config['template']}']",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def _search_journal(args, journal, **kwargs):
|
||||
""" Search the journal with the given args"""
|
||||
if args.on_date:
|
||||
args.start_date = args.end_date = args.on_date
|
||||
|
||||
if args.today_in_history:
|
||||
now = time.parse("now")
|
||||
args.day = now.day
|
||||
args.month = now.month
|
||||
|
||||
journal.filter(
|
||||
tags=args.text,
|
||||
month=args.month,
|
||||
day=args.day,
|
||||
year=args.year,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded,
|
||||
contains=args.contains,
|
||||
)
|
||||
journal.limit(args.limit)
|
||||
|
||||
|
||||
def _edit_search_results(config, journal, old_entries, **kwargs):
|
||||
"""
|
||||
1. Send the given journal entries to the user-configured editor
|
||||
2. Print out stats on any modifications to journal
|
||||
3. Write modifications to journal
|
||||
"""
|
||||
if not config["editor"]:
|
||||
print(
|
||||
f"""
|
||||
[{ERROR_COLOR}ERROR{RESET_COLOR}: There is no editor configured.]
|
||||
|
||||
Please specify an editor in config file ({get_config_path()})
|
||||
to use the --edit option.
|
||||
""",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# separate entries we are not editing
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
|
||||
# Get stats now for summary later
|
||||
old_stats = _get_predit_stats(journal)
|
||||
|
||||
# Send user to the editor
|
||||
edited = get_text_from_editor(config, journal.editable_str())
|
||||
journal.parse_editable_str(edited)
|
||||
|
||||
# Print summary if available
|
||||
_print_edited_summary(journal, old_stats)
|
||||
|
||||
# Put back entries we separated earlier, sort, and write the journal
|
||||
journal.entries += other_entries
|
||||
journal.sort()
|
||||
journal.write()
|
||||
|
||||
|
||||
def _print_edited_summary(journal, old_stats, **kwargs):
|
||||
stats = {
|
||||
"deleted": old_stats["count"] - len(journal),
|
||||
"modified": len([e for e in journal.entries if e.modified]),
|
||||
}
|
||||
|
||||
prompts = []
|
||||
|
||||
if stats["deleted"]:
|
||||
prompts.append(
|
||||
f"{stats['deleted']} {_pluralize_entry(stats['deleted'])} deleted"
|
||||
)
|
||||
|
||||
if stats["modified"]:
|
||||
prompts.append(
|
||||
f"{stats['modified']} {_pluralize_entry(stats['modified'])} modified"
|
||||
)
|
||||
|
||||
if prompts:
|
||||
print(f"[{', '.join(prompts).capitalize()}]", file=sys.stderr)
|
||||
|
||||
|
||||
def _get_predit_stats(journal):
|
||||
return {"count": len(journal)}
|
||||
|
||||
|
||||
def _pluralize_entry(num):
|
||||
return "entry" if num == 1 else "entries"
|
||||
|
||||
|
||||
def _delete_search_results(journal, old_entries, **kwargs):
|
||||
if not journal.entries:
|
||||
print(
|
||||
"[No entries deleted, because the search returned no results.]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
entries_to_delete = journal.prompt_delete_entries()
|
||||
|
||||
if entries_to_delete:
|
||||
journal.entries = old_entries
|
||||
journal.delete_entries(entries_to_delete)
|
||||
|
||||
journal.write()
|
||||
|
||||
|
||||
def _display_search_results(args, journal, **kwargs):
|
||||
if args.short:
|
||||
print(journal.pprint(short=True))
|
||||
|
||||
elif args.tags:
|
||||
print(plugins.get_exporter("tags").export(journal))
|
||||
|
||||
elif args.export:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
print(exporter.export(journal, args.filename))
|
||||
elif kwargs["config"].get("display_format"):
|
||||
exporter = plugins.get_exporter(kwargs["config"]["display_format"])
|
||||
print(exporter.export(journal, args.filename))
|
||||
else:
|
||||
print(journal.pprint())
|
29
jrnl/keyring.py
Normal file
29
jrnl/keyring.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import keyring
|
||||
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
|
||||
|
||||
def get_keyring_password(journal_name: str = "default") -> str | None:
|
||||
try:
|
||||
return keyring.get_password("jrnl", journal_name)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if not isinstance(e, keyring.errors.NoKeyringError):
|
||||
print_msg(Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR))
|
||||
return None
|
||||
|
||||
|
||||
def set_keyring_password(password: str, journal_name: str = "default") -> None:
|
||||
try:
|
||||
return keyring.set_password("jrnl", journal_name, password)
|
||||
except keyring.errors.KeyringError as e:
|
||||
if isinstance(e, keyring.errors.NoKeyringError):
|
||||
msg = Message(MsgText.KeyringBackendNotFound, MsgStyle.WARNING)
|
||||
else:
|
||||
msg = Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR)
|
||||
print_msg(msg)
|
88
jrnl/main.py
Normal file
88
jrnl/main.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from jrnl import controller
|
||||
from jrnl.args import parse_args
|
||||
from jrnl.exception import JrnlException
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
from jrnl.output import print_msg
|
||||
|
||||
|
||||
def configure_logger(debug: bool = False) -> None:
|
||||
if not debug:
|
||||
logging.disable()
|
||||
return
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
datefmt="[%X]",
|
||||
format="%(message)s",
|
||||
handlers=[RichHandler()],
|
||||
)
|
||||
logging.getLogger("parsedatetime").setLevel(logging.INFO)
|
||||
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
||||
logging.debug("Logging start")
|
||||
|
||||
|
||||
def run(manual_args: list[str] | None = None) -> int:
|
||||
try:
|
||||
if manual_args is None:
|
||||
manual_args = sys.argv[1:]
|
||||
|
||||
args = parse_args(manual_args)
|
||||
configure_logger(args.debug)
|
||||
logging.debug("Parsed args:\n%s", args)
|
||||
|
||||
status_code = controller.run(args)
|
||||
|
||||
except JrnlException as e:
|
||||
status_code = 1
|
||||
e.print()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
status_code = 1
|
||||
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.KeyboardInterruptMsg,
|
||||
MsgStyle.ERROR_ON_NEW_LINE,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# uncaught exception
|
||||
status_code = 1
|
||||
debug = False
|
||||
try:
|
||||
if args.debug: # type: ignore
|
||||
debug = True
|
||||
except NameError:
|
||||
# This should only happen when the exception
|
||||
# happened before the args were parsed
|
||||
if "--debug" in sys.argv:
|
||||
debug = True
|
||||
|
||||
if debug:
|
||||
from rich.console import Console
|
||||
|
||||
traceback.print_tb(sys.exc_info()[2])
|
||||
Console(stderr=True).print_exception(extra_lines=1)
|
||||
|
||||
print_msg(
|
||||
Message(
|
||||
MsgText.UncaughtException,
|
||||
MsgStyle.ERROR,
|
||||
{"name": type(e).__name__, "exception": e},
|
||||
)
|
||||
)
|
||||
|
||||
# This should be the only exit point
|
||||
return status_code
|
17
jrnl/messages/Message.py
Normal file
17
jrnl/messages/Message.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Mapping
|
||||
from typing import NamedTuple
|
||||
|
||||
from jrnl.messages.MsgStyle import MsgStyle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from jrnl.messages.MsgText import MsgText
|
||||
|
||||
|
||||
class Message(NamedTuple):
|
||||
text: "MsgText"
|
||||
style: MsgStyle = MsgStyle.NORMAL
|
||||
params: Mapping = {}
|
93
jrnl/messages/MsgStyle.py
Normal file
93
jrnl/messages/MsgStyle.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from enum import Enum
|
||||
from typing import Callable
|
||||
from typing import NamedTuple
|
||||
|
||||
from rich import box
|
||||
from rich.panel import Panel
|
||||
|
||||
from jrnl.messages.MsgText import MsgText
|
||||
|
||||
|
||||
class MsgStyle(Enum):
|
||||
class _Color(NamedTuple):
|
||||
"""
|
||||
String representing a standard color to display
|
||||
see: https://rich.readthedocs.io/en/stable/appendix/colors.html
|
||||
"""
|
||||
|
||||
color: str
|
||||
|
||||
class _Decoration(Enum):
|
||||
NONE = {
|
||||
"callback": lambda x, **_: x,
|
||||
"args": {},
|
||||
}
|
||||
BOX = {
|
||||
"callback": Panel,
|
||||
"args": {
|
||||
"expand": False,
|
||||
"padding": (0, 2),
|
||||
"title_align": "left",
|
||||
"box": box.HEAVY,
|
||||
},
|
||||
}
|
||||
|
||||
@property
|
||||
def callback(self) -> Callable:
|
||||
return self.value["callback"]
|
||||
|
||||
@property
|
||||
def args(self) -> dict:
|
||||
return self.value["args"]
|
||||
|
||||
PROMPT = {
|
||||
"decoration": _Decoration.NONE,
|
||||
"color": _Color("white"),
|
||||
"append_space": True,
|
||||
}
|
||||
TITLE = {
|
||||
"decoration": _Decoration.BOX,
|
||||
"color": _Color("cyan"),
|
||||
}
|
||||
NORMAL = {
|
||||
"decoration": _Decoration.BOX,
|
||||
"color": _Color("white"),
|
||||
}
|
||||
WARNING = {
|
||||
"decoration": _Decoration.BOX,
|
||||
"color": _Color("yellow"),
|
||||
}
|
||||
ERROR = {
|
||||
"decoration": _Decoration.BOX,
|
||||
"color": _Color("red"),
|
||||
"box_title": str(MsgText.Error),
|
||||
}
|
||||
ERROR_ON_NEW_LINE = {
|
||||
"decoration": _Decoration.BOX,
|
||||
"color": _Color("red"),
|
||||
"prepend_newline": True,
|
||||
"box_title": str(MsgText.Error),
|
||||
}
|
||||
|
||||
@property
|
||||
def decoration(self) -> _Decoration:
|
||||
return self.value["decoration"]
|
||||
|
||||
@property
|
||||
def color(self) -> _Color:
|
||||
return self.value["color"].color
|
||||
|
||||
@property
|
||||
def prepend_newline(self) -> bool:
|
||||
return self.value.get("prepend_newline", False)
|
||||
|
||||
@property
|
||||
def append_space(self) -> bool:
|
||||
return self.value.get("append_space", False)
|
||||
|
||||
@property
|
||||
def box_title(self) -> MsgText:
|
||||
return self.value.get("box_title")
|
291
jrnl/messages/MsgText.py
Normal file
291
jrnl/messages/MsgText.py
Normal file
|
@ -0,0 +1,291 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MsgText(Enum):
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
# -- Welcome --- #
|
||||
WelcomeToJrnl = """
|
||||
Welcome to jrnl {version}!
|
||||
|
||||
It looks like you've been using an older version of jrnl until now. That's
|
||||
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
||||
you can enjoy all of the great new features that come with jrnl 2:
|
||||
|
||||
- Support for storing your journal in multiple files
|
||||
- Faster reading and writing for large journals
|
||||
- New encryption back-end that makes installing jrnl much easier
|
||||
- Tons of bug fixes
|
||||
|
||||
Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
|
||||
If you choose to proceed, you will not be able to use your journals with
|
||||
older versions of jrnl anymore.
|
||||
"""
|
||||
|
||||
AllDoneUpgrade = "We're all done here and you can start enjoying jrnl 2"
|
||||
|
||||
InstallComplete = """
|
||||
jrnl configuration created at {config_path}
|
||||
For advanced features, read the docs at https://jrnl.sh
|
||||
"""
|
||||
|
||||
# --- Prompts --- #
|
||||
InstallJournalPathQuestion = """
|
||||
Path to your journal file (leave blank for {default_journal_path}):
|
||||
"""
|
||||
DeleteEntryQuestion = "Delete entry '{entry_title}'?"
|
||||
ChangeTimeEntryQuestion = "Change time for '{entry_title}'?"
|
||||
EncryptJournalQuestion = """
|
||||
Do you want to encrypt your journal? (You can always change this later)
|
||||
"""
|
||||
UseColorsQuestion = """
|
||||
Do you want jrnl to use colors to display entries? (You can always change this later)
|
||||
""" # noqa: E501 - the line is still under 88 when dedented
|
||||
YesOrNoPromptDefaultYes = "[Y/n]"
|
||||
YesOrNoPromptDefaultNo = "[y/N]"
|
||||
ContinueUpgrade = "Continue upgrading jrnl?"
|
||||
|
||||
# these should be lowercase, if possible in language
|
||||
# "lowercase" means whatever `.lower()` returns
|
||||
OneCharacterYes = "y"
|
||||
OneCharacterNo = "n"
|
||||
|
||||
# --- Exceptions ---#
|
||||
Error = "Error"
|
||||
UncaughtException = """
|
||||
{name}
|
||||
{exception}
|
||||
|
||||
This is probably a bug. Please file an issue at:
|
||||
https://github.com/jrnl-org/jrnl/issues/new/choose
|
||||
"""
|
||||
|
||||
ConfigDirectoryIsFile = """
|
||||
Problem with config file!
|
||||
The path to your jrnl configuration directory is a file, not a directory:
|
||||
|
||||
{config_directory_path}
|
||||
|
||||
Removing this file will allow jrnl to save its configuration.
|
||||
"""
|
||||
|
||||
CantParseConfigFile = """
|
||||
Unable to parse config file at:
|
||||
{config_path}
|
||||
"""
|
||||
|
||||
LineWrapTooSmallForDateFormat = """
|
||||
The provided linewrap value of {config_linewrap} is too small by
|
||||
{columns} columns to display the timestamps in the configured time
|
||||
format for journal {journal}.
|
||||
|
||||
You can avoid this error by specifying a linewrap value that is larger
|
||||
by at least {columns} in the configuration file or by using
|
||||
--config-override at the command line
|
||||
"""
|
||||
|
||||
CannotEncryptJournalType = """
|
||||
The journal {journal_name} can't be encrypted because it is a
|
||||
{journal_type} journal.
|
||||
|
||||
To encrypt it, create a new journal referencing a file, export
|
||||
this journal to the new journal, then encrypt the new journal.
|
||||
"""
|
||||
|
||||
ConfigEncryptedForUnencryptableJournalType = """
|
||||
The config for journal "{journal_name}" has 'encrypt' set to true, but this type
|
||||
of journal can't be encrypted. Please fix your config file.
|
||||
"""
|
||||
|
||||
DecryptionFailedGeneric = "The decryption of journal data failed."
|
||||
|
||||
KeyboardInterruptMsg = "Aborted by user"
|
||||
|
||||
CantReadTemplate = """
|
||||
Unable to find a template file {template_path}.
|
||||
|
||||
The following paths were checked:
|
||||
* {jrnl_template_dir}{template_path}
|
||||
* {actual_template_path}
|
||||
"""
|
||||
|
||||
NoNamedJournal = "No '{journal_name}' journal configured\n{journals}"
|
||||
|
||||
DoesNotExist = "{name} does not exist"
|
||||
|
||||
# --- Journal status ---#
|
||||
JournalNotSaved = "Entry NOT saved to journal"
|
||||
JournalEntryAdded = "Entry added to {journal_name} journal"
|
||||
|
||||
JournalCountAddedSingular = "{num} entry added"
|
||||
JournalCountModifiedSingular = "{num} entry modified"
|
||||
JournalCountDeletedSingular = "{num} entry deleted"
|
||||
|
||||
JournalCountAddedPlural = "{num} entries added"
|
||||
JournalCountModifiedPlural = "{num} entries modified"
|
||||
JournalCountDeletedPlural = "{num} entries deleted"
|
||||
|
||||
JournalCreated = "Journal '{journal_name}' created at {filename}"
|
||||
DirectoryCreated = "Directory {directory_name} created"
|
||||
JournalEncrypted = "Journal will be encrypted"
|
||||
JournalEncryptedTo = "Journal encrypted to {path}"
|
||||
JournalDecryptedTo = "Journal decrypted to {path}"
|
||||
BackupCreated = "Created a backup at {filename}"
|
||||
|
||||
# --- Editor ---#
|
||||
WritingEntryStart = """
|
||||
Writing Entry
|
||||
To finish writing, press {how_to_quit} on a blank line.
|
||||
"""
|
||||
HowToQuitWindows = "Ctrl+z and then Enter"
|
||||
HowToQuitLinux = "Ctrl+d"
|
||||
|
||||
EditorMisconfigured = """
|
||||
No such file or directory: '{editor_key}'
|
||||
|
||||
Please check the 'editor' key in your config file for errors:
|
||||
editor: '{editor_key}'
|
||||
"""
|
||||
|
||||
EditorNotConfigured = """
|
||||
There is no editor configured
|
||||
|
||||
To use the --edit option, please specify an editor your config file:
|
||||
{config_file}
|
||||
|
||||
For examples of how to configure an external editor, see:
|
||||
https://jrnl.sh/en/stable/external-editors/
|
||||
"""
|
||||
|
||||
NoEditsReceivedJournalNotDeleted = """
|
||||
No text received from editor. Were you trying to delete all the entries?
|
||||
|
||||
This seems a bit drastic, so the operation was cancelled.
|
||||
|
||||
To delete all entries, use the --delete option.
|
||||
"""
|
||||
|
||||
NoEditsReceived = "No edits to save, because nothing was changed"
|
||||
|
||||
NoTextReceived = """
|
||||
No entry to save, because no text was received
|
||||
"""
|
||||
NoChangesToTemplate = """
|
||||
No entry to save, because the template was not changed
|
||||
"""
|
||||
# --- Upgrade --- #
|
||||
JournalFailedUpgrade = """
|
||||
The following journal{s} failed to upgrade:
|
||||
{failed_journals}
|
||||
|
||||
Please tell us about this problem at the following URL:
|
||||
https://github.com/jrnl-org/jrnl/issues/new?title=JournalFailedUpgrade
|
||||
"""
|
||||
|
||||
UpgradeAborted = "jrnl was NOT upgraded"
|
||||
|
||||
AbortingUpgrade = "Aborting upgrade..."
|
||||
|
||||
ImportAborted = "Entries were NOT imported"
|
||||
|
||||
JournalsToUpgrade = """
|
||||
The following journals will be upgraded to jrnl {version}:
|
||||
|
||||
"""
|
||||
|
||||
JournalsToIgnore = """
|
||||
The following journals will not be touched:
|
||||
|
||||
"""
|
||||
|
||||
UpgradingJournal = """
|
||||
Upgrading '{journal_name}' journal stored in {path}...
|
||||
"""
|
||||
|
||||
UpgradingConfig = "Upgrading config..."
|
||||
|
||||
PaddedJournalName = "{journal_name:{pad}} -> {path}"
|
||||
|
||||
# -- Config --- #
|
||||
AltConfigNotFound = """
|
||||
Alternate configuration file not found at the given path:
|
||||
{config_file}
|
||||
"""
|
||||
|
||||
ConfigUpdated = """
|
||||
Configuration updated to newest version at {config_path}
|
||||
"""
|
||||
|
||||
ConfigDoubleKeys = """
|
||||
There is at least one duplicate key in your configuration file.
|
||||
|
||||
Details:
|
||||
{error_message}
|
||||
"""
|
||||
|
||||
# --- Password --- #
|
||||
Password = "Password:"
|
||||
PasswordFirstEntry = "Enter password for journal '{journal_name}': "
|
||||
PasswordConfirmEntry = "Enter password again: "
|
||||
PasswordMaxTriesExceeded = "Too many attempts with wrong password"
|
||||
PasswordCanNotBeEmpty = "Password can't be empty!"
|
||||
PasswordDidNotMatch = "Passwords did not match, please try again"
|
||||
WrongPasswordTryAgain = "Wrong password, try again"
|
||||
PasswordStoreInKeychain = "Do you want to store the password in your keychain?"
|
||||
|
||||
# --- Search --- #
|
||||
NothingToDelete = """
|
||||
No entries to delete, because the search returned no results
|
||||
"""
|
||||
|
||||
NothingToModify = """
|
||||
No entries to modify, because the search returned no results
|
||||
"""
|
||||
|
||||
NoEntriesFound = "no entries found"
|
||||
EntryFoundCountSingular = "{num} entry found"
|
||||
EntryFoundCountPlural = "{num} entries found"
|
||||
|
||||
# --- Formats --- #
|
||||
HeadingsPastH6 = """
|
||||
Headings increased past H6 on export - {date} {title}
|
||||
"""
|
||||
|
||||
YamlMustBeDirectory = """
|
||||
YAML export must be to a directory, not a single file
|
||||
"""
|
||||
|
||||
JournalExportedTo = "Journal exported to {path}"
|
||||
|
||||
# --- Import --- #
|
||||
ImportSummary = """
|
||||
{count} imported to {journal_name} journal
|
||||
"""
|
||||
|
||||
ImporterNotFound = """
|
||||
No importer found for file type '{format}'.
|
||||
'{format}' is likely to be an export-only format.
|
||||
"""
|
||||
|
||||
# --- Color --- #
|
||||
InvalidColor = "{key} set to invalid color: {color}"
|
||||
|
||||
# --- Keyring --- #
|
||||
KeyringBackendNotFound = """
|
||||
Keyring backend not found.
|
||||
|
||||
Please install one of the supported backends by visiting:
|
||||
https://pypi.org/project/keyring/
|
||||
"""
|
||||
|
||||
KeyringRetrievalFailure = "Failed to retrieve keyring"
|
||||
|
||||
# --- Deprecation --- #
|
||||
DeprecatedCommand = """
|
||||
The command {old_cmd} is deprecated and will be removed from jrnl soon.
|
||||
Please use {new_cmd} instead.
|
||||
"""
|
10
jrnl/messages/__init__.py
Normal file
10
jrnl/messages/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.messages import Message
|
||||
from jrnl.messages import MsgStyle
|
||||
from jrnl.messages import MsgText
|
||||
|
||||
Message = Message.Message
|
||||
MsgStyle = MsgStyle.MsgStyle
|
||||
MsgText = MsgText.MsgText
|
|
@ -1,18 +1,18 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# Copyright © 2012-2023 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import shlex
|
||||
from sys import platform
|
||||
|
||||
|
||||
def on_windows():
|
||||
def on_windows() -> bool:
|
||||
return "win32" in platform
|
||||
|
||||
|
||||
def on_posix():
|
||||
def on_posix() -> bool:
|
||||
return not on_windows()
|
||||
|
||||
|
||||
def split_args(args):
|
||||
def split_args(args: str) -> list[str]:
|
||||
"""Split arguments and add escape characters as appropriate for the OS"""
|
||||
return shlex.split(args, posix=on_posix())
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue