Diff

from v0.3.0 to v0.3.1

Diffstat

 .gitignore | 1 
 .pylintrc | 3 
 .travis.yml | 6 
 .vulture_whitelist.py | 31 
 Makefile | 6 
 Pipfile.lock | 151 +++---
 nncli/__init__.py | 2 
 nncli/__main__.py | 1 
 nncli/clipboard.py | 28 
 nncli/config.py | 802 ++++++++++++++++++++++++---------
 nncli/gui.py | 913 ++++++++++++++++++++++++++++++++++++++
 nncli/log.py | 40 +
 nncli/nextcloud_note.py | 87 +-
 nncli/nncli.py | 1029 ++++--------------------------------------
 nncli/notes_db.py | 511 ++++++++++++--------
 nncli/temp.py | 48 +
 nncli/user_input.py | 7 
 nncli/utils.py | 131 ++++
 nncli/view_help.py | 172 ++++--
 nncli/view_log.py | 30 
 nncli/view_note.py | 93 ++-
 nncli/view_titles.py | 173 ++++--
 pyproject.toml | 1 
 tests/test_config.py | 101 ++-
 tests/test_gui.py | 129 +++++
 tests/test_nncli.py | 425 ++++++++--------
 tox.ini | 7 

.gitignore

6 6 .pytest_cache/
7 7 docs/build/
8 8 .tox
9 +htmlcov/

.pylintrc

140 140 xreadlines-attribute,
141 141 deprecated-sys-function,
142 142 exception-escape,
143 - comprehension-escape
143 + comprehension-escape,
144 + duplicate-code
144 145
145 146 # Enable the message, report, category or checker with the given id(s). You can
146 147 # either give multiple identifier separated by comma (,) or put this option

.travis.yml

35 35 script: make test
36 36 <<: *xenial-mixin
37 37 python: 3.7
38 - # - stage: lint
39 - # script: make lint
40 - # python: 3.6
38 + - stage: lint
39 + script: make lint
40 + python: 3.6
41 41 - stage: coverage
42 42 script: make coverage
43 43 after_success: coveralls

.vulture_whitelist.py

1 -loop # unused variable (nncli/nncli.py:185)
2 -loop # unused variable (nncli/nncli.py:735)
3 -_.widget # unused attribute (nncli/nncli.py:746)
4 -_.widget # unused attribute (nncli/nncli.py:750)
5 -all_notes_cnt # unused variable (nncli/nncli.py:885)
6 -match_regex # unused variable (nncli/nncli.py:885)
7 -all_notes_cnt # unused variable (nncli/nncli.py:922)
8 -match_regex # unused variable (nncli/nncli.py:922)
9 -all_notes_cnt # unused variable (nncli/nncli.py:976)
10 -match_regex # unused variable (nncli/nncli.py:976)
11 -frame # unused variable (nncli/nncli.py:1057)
12 -signum # unused variable (nncli/nncli.py:1057)
13 -note_index # unused variable (nncli/notes_db.py:410)
14 -note_index # unused variable (nncli/notes_db.py:510)
15 -_.all_notes_cnt # unused attribute (nncli/view_titles.py:13)
16 -_.match_regex # unused attribute (nncli/view_titles.py:13)
17 -_.all_notes_cnt # unused attribute (nncli/view_titles.py:20)
18 -_.match_regex # unused attribute (nncli/view_titles.py:20)
1 +convert # unused function (nncli/cli.py:12)
2 +param # unused variable (nncli/cli.py:12)
3 +loop # unused variable (nncli/gui.py:826)
4 +_.widget # unused attribute (nncli/gui.py:839)
5 +_.widget # unused attribute (nncli/gui.py:844)
6 +arg # unused variable (nncli/gui.py:888)
7 +loop # unused variable (nncli/gui.py:888)
8 +frame # unused variable (nncli/nncli.py:254)
9 +signum # unused variable (nncli/nncli.py:254)
10 +_.all_notes_cnt # unused attribute (nncli/view_titles.py:21)
11 +_.match_regex # unused attribute (nncli/view_titles.py:21)
12 +_.all_notes_cnt # unused attribute (nncli/view_titles.py:33)
13 +_.match_regex # unused attribute (nncli/view_titles.py:33)

Makefile

27 27 BROWSER := python -c "$$BROWSER_PYSCRIPT"
28 28 PIPENV := pipenv
29 29 PIPRUN := $(PIPENV) run
30 -PIPINST := $(PIPENV) --bare install --dev
30 +PIPINST := $(PIPENV) --bare install --dev --skip-lock
31 31
32 32 help:
33 33 @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
. . .
57 57
58 58 lint: ## check style with pylint
59 59 $(PIPRUN) pylint nncli tests --disable=parse-error
60 - $(PIPRUN) vulture nncli .vulture_whitelist
60 + $(PIPRUN) vulture nncli .vulture_whitelist.py
61 61
62 62 test: ## run tests quickly with the default Python
63 63 $(PIPRUN) python -m pytest
. . .
76 76 $(BROWSER) htmlcov/index.html
77 77
78 78 release: dist ## package and upload a release
79 - $(PIPRUN) flit publish
79 + twine upload -s dist/*
80 80
81 81 dist: ## builds source and wheel package
82 82 $(PIPRUN) flit build

Pipfile.lock

24 24 },
25 25 "certifi": {
26 26 "hashes": [
27 - "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
28 - "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
27 + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
28 + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
29 29 ],
30 - "version": "==2018.8.24"
30 + "version": "==2018.10.15"
31 31 },
32 32 "chardet": {
33 33 "hashes": [
. . .
38 38 },
39 39 "click": {
40 40 "hashes": [
41 - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
42 - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
41 + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
42 + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
43 43 ],
44 44 "index": "pypi",
45 - "version": "==6.7"
45 + "version": "==7.0"
46 46 },
47 47 "idna": {
48 48 "hashes": [
. . .
53 53 },
54 54 "requests": {
55 55 "hashes": [
56 - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
57 - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
56 + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
57 + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
58 58 ],
59 59 "index": "pypi",
60 - "version": "==2.19.1"
60 + "version": "==2.20.0"
61 61 },
62 62 "urllib3": {
63 63 "hashes": [
64 - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
65 - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
64 + "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
65 + "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
66 66 ],
67 - "markers": "python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.2.*' and python_version < '4'",
68 - "version": "==1.23"
67 + "version": "==1.24"
69 68 },
70 69 "urwid": {
71 70 "hashes": [
. . .
78 77 "develop": {
79 78 "alabaster": {
80 79 "hashes": [
81 - "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
82 - "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
80 + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
81 + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
83 82 ],
84 - "version": "==0.7.11"
83 + "version": "==0.7.12"
85 84 },
86 85 "astroid": {
87 86 "hashes": [
. . .
95 94 "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
96 95 "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
97 96 ],
98 - "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'",
99 97 "version": "==1.2.1"
100 98 },
101 99 "attrs": {
. . .
114 112 },
115 113 "certifi": {
116 114 "hashes": [
117 - "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
118 - "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
115 + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
116 + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
119 117 ],
120 - "version": "==2018.8.24"
118 + "version": "==2018.10.15"
121 119 },
122 120 "chardet": {
123 121 "hashes": [
. . .
130 128 "hashes": [
131 129 "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
132 130 "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
131 + "sha256:0bf8cbbd71adfff0ef1f3a1531e6402d13b7b01ac50a79c97ca15f030dba6306",
133 132 "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
134 133 "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
135 134 "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
. . .
158 157 "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
159 158 "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
160 159 "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
160 + "sha256:f05a636b4564104120111800021a92e43397bc12a5c72fed7036be8556e0029e",
161 161 "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80"
162 162 ],
163 - "markers": "python_version < '4' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'",
164 163 "version": "==4.5.1"
165 164 },
166 165 "docutils": {
. . .
170 169 "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
171 170 ],
172 171 "version": "==0.14"
172 + },
173 + "filelock": {
174 + "hashes": [
175 + "sha256:86fe6af56ae08ebc9c66d54ba3398c35b98916d0862d782b276a65816ff39392",
176 + "sha256:97694f181bdf58f213cca0a7cb556dc7bf90e2f8eb9aa3151260adac56701afb"
177 + ],
178 + "version": "==3.0.9"
173 179 },
174 180 "flit": {
175 181 "hashes": [
176 - "sha256:178e6865185b1802aa3b1944f4957d2c83fc56294dc8047d2c4722131f696e61",
177 - "sha256:da823d4acae9bda42dcc0c7ab1d9be475a8a47aae5fd6dde63841d9f430ccb2f"
182 + "sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1",
183 + "sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f"
178 184 ],
179 185 "index": "pypi",
180 - "version": "==1.1"
186 + "version": "==1.2.1"
181 187 },
182 188 "idna": {
183 189 "hashes": [
. . .
191 197 "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
192 198 "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
193 199 ],
194 - "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
195 200 "version": "==1.1.0"
196 201 },
197 202 "isort": {
. . .
200 205 "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
201 206 "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
202 207 ],
203 - "markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'",
204 208 "version": "==4.3.4"
205 209 },
206 210 "jinja2": {
. . .
275 279 },
276 280 "packaging": {
277 281 "hashes": [
278 - "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
279 - "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
282 + "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
283 + "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
280 284 ],
281 - "version": "==17.1"
285 + "version": "==18.0"
282 286 },
283 287 "pathlib2": {
284 288 "hashes": [
. . .
291 295 },
292 296 "pbr": {
293 297 "hashes": [
294 - "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
295 - "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
298 + "sha256:8fc938b1123902f5610b06756a31b1e6febf0d105ae393695b0c9d4244ed2910",
299 + "sha256:f20ec0abbf132471b68963bb34d9c78e603a5cf9e24473f14358e66551d47475"
296 300 ],
297 - "version": "==4.2.0"
301 + "version": "==5.1.0"
298 302 },
299 303 "pluggy": {
300 304 "hashes": [
301 - "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
302 - "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
305 + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
306 + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
303 307 ],
304 - "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.7'",
305 - "version": "==0.7.1"
308 + "version": "==0.8.0"
306 309 },
307 310 "pudb": {
308 311 "hashes": [
. . .
313 316 },
314 317 "py": {
315 318 "hashes": [
316 - "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
317 - "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
319 + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
320 + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
318 321 ],
319 - "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.1.*' and python_version >= '2.7'",
320 - "version": "==1.6.0"
322 + "version": "==1.7.0"
321 323 },
322 324 "pygments": {
323 325 "hashes": [
. . .
336 338 },
337 339 "pyparsing": {
338 340 "hashes": [
339 - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
340 - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
341 + "sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
342 + "sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
341 343 ],
342 - "version": "==2.2.0"
344 + "version": "==2.2.2"
343 345 },
344 346 "pytest": {
345 347 "hashes": [
346 - "sha256:2d7c49e931316cc7d1638a3e5f54f5d7b4e5225972b3c9838f3584788d27f349",
347 - "sha256:ad0c7db7b5d4081631e0155f5c61b80ad76ce148551aaafe3a718d65a7508b18"
348 + "sha256:a9e5e8d7ab9d5b0747f37740276eb362e6a76275d76cebbb52c6049d93b475db",
349 + "sha256:bf47e8ed20d03764f963f0070ff1c8fda6e2671fc5dd562a4d3b7148ad60f5ca"
348 350 ],
349 351 "index": "pypi",
350 - "version": "==3.7.4"
352 + "version": "==3.9.3"
351 353 },
352 354 "pytest-cov": {
353 355 "hashes": [
. . .
367 369 },
368 370 "pytoml": {
369 371 "hashes": [
370 - "sha256:dae3c4e31d09eb06a6076d671f2281ee5d2c43cbeae16599c3af20881bb818ac"
372 + "sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013"
371 373 ],
372 - "version": "==0.1.18"
374 + "version": "==0.1.20"
373 375 },
374 376 "pytz": {
375 377 "hashes": [
376 - "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
377 - "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
378 + "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
379 + "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
378 380 ],
379 - "version": "==2018.5"
381 + "version": "==2018.7"
380 382 },
381 383 "requests": {
382 384 "hashes": [
383 - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
384 - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
385 + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
386 + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
385 387 ],
386 388 "index": "pypi",
387 - "version": "==2.19.1"
388 - },
389 - "requests-download": {
390 - "hashes": [
391 - "sha256:92d895a6ca51ea51aa42bab864bddaee31b5601c7e7e1ade4c27b0eb6695d846",
392 - "sha256:994d9d332befae6616f562769bab163f08d6404dc7e28fb7bfed4a0a43a754ad"
393 - ],
394 - "version": "==0.1.2"
389 + "version": "==2.20.0"
395 390 },
396 391 "scandir": {
397 392 "hashes": [
. . .
427 422 },
428 423 "sphinx": {
429 424 "hashes": [
430 - "sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4",
431 - "sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86"
425 + "sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17",
426 + "sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464"
432 427 ],
433 428 "index": "pypi",
434 - "version": "==1.7.9"
429 + "version": "==1.8.1"
435 430 },
436 431 "sphinxcontrib-websupport": {
437 432 "hashes": [
438 433 "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
439 434 "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
440 435 ],
441 - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'",
442 436 "version": "==1.1.0"
443 437 },
438 + "toml": {
439 + "hashes": [
440 + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
441 + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
442 + ],
443 + "version": "==0.10.0"
444 + },
444 445 "tox": {
445 446 "hashes": [
446 - "sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7",
447 - "sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600"
447 + "sha256:513e32fdf2f9e2d583c2f248f47ba9886428c949f068ac54a0469cac55df5862",
448 + "sha256:75fa30e8329b41b664585f5fb837e23ce1d7e6fa1f7811f2be571c990f9d911b"
448 449 ],
449 450 "index": "pypi",
450 - "version": "==3.2.1"
451 + "version": "==3.5.3"
451 452 },
452 453 "urllib3": {
453 454 "hashes": [
454 - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
455 - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
455 + "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
456 + "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
456 457 ],
457 - "markers": "python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.2.*' and python_version < '4'",
458 - "version": "==1.23"
458 + "version": "==1.24"
459 459 },
460 460 "urwid": {
461 461 "hashes": [
. . .
469 469 "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669",
470 470 "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752"
471 471 ],
472 - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'",
473 472 "version": "==16.0.0"
474 473 },
475 474 "vulture": {
476 475 "hashes": [
477 - "sha256:79c89ef5e3f2365467bcf491f425f777ae8fd157584dcc550d4591920b00fe3f",
478 - "sha256:e794345a19c76f93f48f4519653038df90ad468ddea7912e14b07a07f6412e32"
476 + "sha256:4b5a8980c338e9c068d43e7164555a1e4c9c7d84961ce2bc6f3ed975f6e5bc9d",
477 + "sha256:524b6b9642d0bbe74ea21478bf260937d1ba9b3b86676ca0b17cd10b4b51ba01"
479 478 ],
480 479 "index": "pypi",
481 - "version": "==0.29"
480 + "version": "==1.0"
482 481 },
483 482 "wrapt": {
484 483 "hashes": [

nncli/__init__.py

1 1 # -*- coding: utf-8 -*-
2 2 """NextCloud Notes Command Line Interface"""
3 3
4 -__version__ = '0.3.0'
4 +__version__ = '0.3.1'

nncli/__main__.py

2 2 """nncli main module"""
3 3 import nncli.cli
4 4
5 +# pylint: disable=no-value-for-parameter
5 6 if __name__ == '__main__':
6 7 nncli.cli.main()

nncli/clipboard.py

1 1 # -*- coding: utf-8 -*-
2 -
2 +"""clipboard module"""
3 3 import os
4 -from distutils import spawn
4 +import subprocess
5 +from subprocess import CalledProcessError
5 6
6 -class Clipboard(object):
7 +class Clipboard:
8 + """Class implements copying note content to the clipboard"""
7 9 def __init__(self):
8 10 self.copy_command = self.get_copy_command()
9 11
10 - def get_copy_command(self):
11 - if (spawn.find_executable('xsel')):
12 + @staticmethod
13 + def get_copy_command():
14 + """Defines the copy command based on the contents of $PATH"""
15 +
16 + try:
17 + subprocess.check_output(['which', 'xsel'])
12 18 return 'echo "%s" | xsel -ib'
13 - if (spawn.find_executable('pbcopy')):
19 + except CalledProcessError:
20 + pass
21 +
22 + try:
23 + subprocess.check_output(['which', 'pbcopy'])
14 24 return 'echo "%s" | pbcopy'
25 + except CalledProcessError:
26 + pass
27 +
15 28 return None
16 29
17 30 def copy(self, text):
18 - if (self.copy_command):
31 + """Copies text to the system clipboard"""
32 + if self.copy_command:
19 33 os.system(self.copy_command % text)

nncli/config.py

1 1 # -*- coding: utf-8 -*-
2 -
3 -import os, sys, urwid, collections, configparser, subprocess
2 +"""config module"""
3 +import collections
4 +import configparser
5 +import os
6 +import subprocess
7 +import sys
4 8
5 9 from appdirs import user_cache_dir, user_config_dir
6 10
11 +# pylint: disable=too-few-public-methods
7 12 class Config:
13 + """A class to contain all configuration data for nncli"""
14 + class State:
15 + """A container class for state information"""
16 + def __init__(self, **kwargs):
17 + self.__dict__.update(kwargs)
8 18
9 19 def __init__(self, custom_file=None):
20 + self.state = Config.State(do_server_sync=True,
21 + verbose=False,
22 + do_gui=False,
23 + search_direction=None)
10 24 self.config_home = user_config_dir('nncli', 'djmoch')
11 25 self.cache_home = user_cache_dir('nncli', 'djmoch')
12 26
13 27 defaults = \
14 28 {
15 - 'cfg_nn_username' : '',
16 - 'cfg_nn_password' : '',
17 - 'cfg_nn_password_eval' : '',
18 - 'cfg_db_path' : self.cache_home,
19 - 'cfg_search_categories' : 'yes', # with regex searches
20 - 'cfg_sort_mode' : 'date', # 'alpha' or 'date'
21 - 'cfg_favorite_ontop' : 'yes',
22 - 'cfg_tabstop' : '4',
23 - 'cfg_format_strftime' : '%Y/%m/%d',
24 - 'cfg_format_note_title' : '[%D] %F %-N %T',
25 - 'cfg_status_bar' : 'yes',
26 - 'cfg_editor' : os.environ['EDITOR'] if 'EDITOR' in os.environ else 'vim {fname} +{line}',
27 - 'cfg_pager' : os.environ['PAGER'] if 'PAGER' in os.environ else 'less -c',
28 - 'cfg_max_logs' : '5',
29 - 'cfg_log_timeout' : '5',
30 - 'cfg_log_reversed' : 'yes',
31 - 'cfg_nn_host' : '',
32 - 'cfg_tempdir' : '',
29 + 'cfg_nn_username' : '',
30 + 'cfg_nn_password' : '',
31 + 'cfg_nn_password_eval' : '',
32 + 'cfg_db_path' : self.cache_home,
33 + 'cfg_search_categories' : 'yes', # with regex searches
34 + 'cfg_sort_mode' : 'date', # 'alpha' or 'date'
35 + 'cfg_favorite_ontop' : 'yes',
36 + 'cfg_tabstop' : '4',
37 + 'cfg_format_strftime' : '%Y/%m/%d',
38 + 'cfg_format_note_title' : '[%D] %F %-N %T',
39 + 'cfg_status_bar' : 'yes',
40 + 'cfg_editor' : os.environ['EDITOR'] \
41 + if 'EDITOR' in os.environ else 'vim {fname} +{line}',
42 + 'cfg_pager' : os.environ['PAGER'] \
43 + if 'PAGER' in os.environ else 'less -c',
44 + 'cfg_max_logs' : '5',
45 + 'cfg_log_timeout' : '5',
46 + 'cfg_log_reversed' : 'yes',
47 + 'cfg_nn_host' : '',
48 + 'cfg_tempdir' : '',
33 49
34 - 'kb_help' : 'h',
35 - 'kb_quit' : 'q',
36 - 'kb_sync' : 'S',
37 - 'kb_down' : 'j',
38 - 'kb_up' : 'k',
39 - 'kb_page_down' : 'space',
40 - 'kb_page_up' : 'b',
41 - 'kb_half_page_down' : 'ctrl d',
42 - 'kb_half_page_up' : 'ctrl u',
43 - 'kb_bottom' : 'G',
44 - 'kb_top' : 'g',
45 - 'kb_status' : 's',
46 - 'kb_create_note' : 'C',
47 - 'kb_edit_note' : 'e',
48 - 'kb_view_note' : 'enter',
49 - 'kb_view_note_ext' : 'meta enter',
50 - 'kb_view_note_json' : 'O',
51 - 'kb_pipe_note' : '|',
52 - 'kb_view_next_note' : 'J',
53 - 'kb_view_prev_note' : 'K',
54 - 'kb_view_log' : 'l',
55 - 'kb_tabstop2' : '2',
56 - 'kb_tabstop4' : '4',
57 - 'kb_tabstop8' : '8',
58 - 'kb_search_gstyle' : '/',
59 - 'kb_search_regex' : 'meta /',
60 - 'kb_search_prev_gstyle' : '?',
61 - 'kb_search_prev_regex' : 'meta ?',
62 - 'kb_search_next' : 'n',
63 - 'kb_search_prev' : 'N',
64 - 'kb_clear_search' : 'A',
65 - 'kb_sort_date' : 'd',
66 - 'kb_sort_alpha' : 'a',
67 - 'kb_sort_categories' : 'ctrl t',
68 - 'kb_note_delete' : 'D',
69 - 'kb_note_favorite' : 'p',
70 - 'kb_note_category' : 't',
71 - 'kb_copy_note_text' : 'y',
50 + 'kb_help' : 'h',
51 + 'kb_quit' : 'q',
52 + 'kb_sync' : 'S',
53 + 'kb_down' : 'j',
54 + 'kb_up' : 'k',
55 + 'kb_page_down' : 'space',
56 + 'kb_page_up' : 'b',
57 + 'kb_half_page_down' : 'ctrl d',
58 + 'kb_half_page_up' : 'ctrl u',
59 + 'kb_bottom' : 'G',
60 + 'kb_top' : 'g',
61 + 'kb_status' : 's',
62 + 'kb_create_note' : 'C',
63 + 'kb_edit_note' : 'e',
64 + 'kb_view_note' : 'enter',
65 + 'kb_view_note_ext' : 'meta enter',
66 + 'kb_view_note_json' : 'O',
67 + 'kb_pipe_note' : '|',
68 + 'kb_view_next_note' : 'J',
69 + 'kb_view_prev_note' : 'K',
70 + 'kb_view_log' : 'l',
71 + 'kb_tabstop2' : '2',
72 + 'kb_tabstop4' : '4',
73 + 'kb_tabstop8' : '8',
74 + 'kb_search_gstyle' : '/',
75 + 'kb_search_regex' : 'meta /',
76 + 'kb_search_prev_gstyle' : '?',
77 + 'kb_search_prev_regex' : 'meta ?',
78 + 'kb_search_next' : 'n',
79 + 'kb_search_prev' : 'N',
80 + 'kb_clear_search' : 'A',
81 + 'kb_sort_date' : 'd',
82 + 'kb_sort_alpha' : 'a',
83 + 'kb_sort_categories' : 'ctrl t',
84 + 'kb_note_delete' : 'D',
85 + 'kb_note_favorite' : 'p',
86 + 'kb_note_category' : 't',
87 + 'kb_copy_note_text' : 'y',
72 88
73 - 'clr_default_fg' : 'default',
74 - 'clr_default_bg' : 'default',
75 - 'clr_status_bar_fg' : 'dark gray',
76 - 'clr_status_bar_bg' : 'light gray',
77 - 'clr_log_fg' : 'dark gray',
78 - 'clr_log_bg' : 'light gray',
79 - 'clr_user_input_bar_fg' : 'white',
80 - 'clr_user_input_bar_bg' : 'light red',
81 - 'clr_note_focus_fg' : 'white',
82 - 'clr_note_focus_bg' : 'light red',
83 - 'clr_note_title_day_fg' : 'light red',
84 - 'clr_note_title_day_bg' : 'default',
85 - 'clr_note_title_week_fg' : 'light green',
86 - 'clr_note_title_week_bg' : 'default',
87 - 'clr_note_title_month_fg' : 'brown',
88 - 'clr_note_title_month_bg' : 'default',
89 - 'clr_note_title_year_fg' : 'light blue',
90 - 'clr_note_title_year_bg' : 'default',
91 - 'clr_note_title_ancient_fg' : 'light blue',
92 - 'clr_note_title_ancient_bg' : 'default',
93 - 'clr_note_date_fg' : 'dark blue',
94 - 'clr_note_date_bg' : 'default',
95 - 'clr_note_flags_fg' : 'dark magenta',
96 - 'clr_note_flags_bg' : 'default',
97 - 'clr_note_category_fg' : 'dark red',
98 - 'clr_note_category_bg' : 'default',
99 - 'clr_note_content_fg' : 'default',
100 - 'clr_note_content_bg' : 'default',
101 - 'clr_note_content_focus_fg' : 'white',
102 - 'clr_note_content_focus_bg' : 'light red',
103 - 'clr_note_content_old_fg' : 'yellow',
104 - 'clr_note_content_old_bg' : 'dark gray',
105 - 'clr_note_content_old_focus_fg' : 'white',
106 - 'clr_note_content_old_focus_bg' : 'light red',
107 - 'clr_help_focus_fg' : 'white',
108 - 'clr_help_focus_bg' : 'light red',
109 - 'clr_help_header_fg' : 'dark blue',
110 - 'clr_help_header_bg' : 'default',
111 - 'clr_help_config_fg' : 'dark green',
112 - 'clr_help_config_bg' : 'default',
113 - 'clr_help_value_fg' : 'dark red',
114 - 'clr_help_value_bg' : 'default',
115 - 'clr_help_descr_fg' : 'default',
116 - 'clr_help_descr_bg' : 'default'
89 + 'clr_default_fg' : 'default',
90 + 'clr_default_bg' : 'default',
91 + 'clr_status_bar_fg' : 'dark gray',
92 + 'clr_status_bar_bg' : 'light gray',
93 + 'clr_log_fg' : 'dark gray',
94 + 'clr_log_bg' : 'light gray',
95 + 'clr_user_input_bar_fg' : 'white',
96 + 'clr_user_input_bar_bg' : 'light red',
97 + 'clr_note_focus_fg' : 'white',
98 + 'clr_note_focus_bg' : 'light red',
99 + 'clr_note_title_day_fg' : 'light red',
100 + 'clr_note_title_day_bg' : 'default',
101 + 'clr_note_title_week_fg' : 'light green',
102 + 'clr_note_title_week_bg' : 'default',
103 + 'clr_note_title_month_fg' : 'brown',
104 + 'clr_note_title_month_bg' : 'default',
105 + 'clr_note_title_year_fg' : 'light blue',
106 + 'clr_note_title_year_bg' : 'default',
107 + 'clr_note_title_ancient_fg' : 'light blue',
108 + 'clr_note_title_ancient_bg' : 'default',
109 + 'clr_note_date_fg' : 'dark blue',
110 + 'clr_note_date_bg' : 'default',
111 + 'clr_note_flags_fg' : 'dark magenta',
112 + 'clr_note_flags_bg' : 'default',
113 + 'clr_note_category_fg' : 'dark red',
114 + 'clr_note_category_bg' : 'default',
115 + 'clr_note_content_fg' : 'default',
116 + 'clr_note_content_bg' : 'default',
117 + 'clr_note_content_focus_fg' : 'white',
118 + 'clr_note_content_focus_bg' : 'light red',
119 + 'clr_note_content_old_fg' : 'yellow',
120 + 'clr_note_content_old_bg' : 'dark gray',
121 + 'clr_note_content_old_focus_fg' : 'white',
122 + 'clr_note_content_old_focus_bg' : 'light red',
123 + 'clr_help_focus_fg' : 'white',
124 + 'clr_help_focus_bg' : 'light red',
125 + 'clr_help_header_fg' : 'dark blue',
126 + 'clr_help_header_bg' : 'default',
127 + 'clr_help_config_fg' : 'dark green',
128 + 'clr_help_config_bg' : 'default',
129 + 'clr_help_value_fg' : 'dark red',
130 + 'clr_help_value_bg' : 'default',
131 + 'clr_help_descr_fg' : 'default',
132 + 'clr_help_descr_bg' : 'default'
117 133 }
118 134
119 - cp = configparser.SafeConfigParser(defaults)
135 + parser = configparser.SafeConfigParser(defaults)
120 136 if custom_file is not None:
121 - cp.read([custom_file])
137 + parser.read([custom_file])
122 138 else:
123 - cp.read([os.path.join(self.config_home, 'config')])
139 + parser.read([os.path.join(self.config_home, 'config')])
124 140
125 141 cfg_sec = 'nncli'
126 142
127 - if not cp.has_section(cfg_sec):
128 - cp.add_section(cfg_sec)
143 + if not parser.has_section(cfg_sec):
144 + parser.add_section(cfg_sec)
129 145
146 + # ordered dicts used to ease help
147 + self._create_configs_dict(parser, cfg_sec)
148 + self._create_keybinds_dict(parser, cfg_sec)
149 + self._create_colors_dict(parser, cfg_sec)
130 150
131 - # special handling for password so we can retrieve it by running a command
132 - nn_password = cp.get(cfg_sec, 'cfg_nn_password', raw=True)
151 + def _create_keybinds_dict(self, parser, cfg_sec):
152 + """Create an OrderedDict object with the keybinds"""
153 + self.keybinds = collections.OrderedDict()
154 + self.keybinds['help'] = \
155 + [parser.get(cfg_sec, 'kb_help'), ['common'], 'Help']
156 + self.keybinds['quit'] = \
157 + [parser.get(cfg_sec, 'kb_quit'), ['common'], 'Quit']
158 + self.keybinds['sync'] = \
159 + [parser.get(cfg_sec, 'kb_sync'), ['common'], 'Full sync']
160 + self.keybinds['down'] = \
161 + [
162 + parser.get(cfg_sec, 'kb_down'),
163 + ['common'],
164 + 'Scroll down one line'
165 + ]
166 + self.keybinds['up'] = \
167 + [
168 + parser.get(cfg_sec, 'kb_up'),
169 + ['common'],
170 + 'Scroll up one line'
171 + ]
172 + self.keybinds['page_down'] = \
173 + [
174 + parser.get(cfg_sec, 'kb_page_down'),
175 + ['common'],
176 + 'Page down'
177 + ]
178 + self.keybinds['page_up'] = \
179 + [parser.get(cfg_sec, 'kb_page_up'), ['common'], 'Page up']
180 + self.keybinds['half_page_down'] = \
181 + [
182 + parser.get(cfg_sec, 'kb_half_page_down'),
183 + ['common'],
184 + 'Half page down'
185 + ]
186 + self.keybinds['half_page_up'] = \
187 + [
188 + parser.get(cfg_sec, 'kb_half_page_up'),
189 + ['common'],
190 + 'Half page up'
191 + ]
192 + self.keybinds['bottom'] = \
193 + [
194 + parser.get(cfg_sec, 'kb_bottom'),
195 + ['common'],
196 + 'Goto bottom'
197 + ]
198 + self.keybinds['top'] = \
199 + [parser.get(cfg_sec, 'kb_top'), ['common'], 'Goto top']
200 + self.keybinds['status'] = \
201 + [
202 + parser.get(cfg_sec, 'kb_status'),
203 + ['common'],
204 + 'Toggle status bar'
205 + ]
206 + self.keybinds['view_log'] = \
207 + [
208 + parser.get(cfg_sec, 'kb_view_log'),
209 + ['common'],
210 + 'View log'
211 + ]
212 + self.keybinds['create_note'] = \
213 + [
214 + parser.get(cfg_sec, 'kb_create_note'),
215 + ['titles'],
216 + 'Create a new note'
217 + ]
218 + self.keybinds['edit_note'] = \
219 + [
220 + parser.get(cfg_sec, 'kb_edit_note'),
221 + ['titles', 'notes'],
222 + 'Edit note'
223 + ]
224 + self.keybinds['view_note'] = \
225 + [
226 + parser.get(cfg_sec, 'kb_view_note'),
227 + ['titles'],
228 + 'View note'
229 + ]
230 + self.keybinds['view_note_ext'] = \
231 + [
232 + parser.get(cfg_sec, 'kb_view_note_ext'),
233 + ['titles', 'notes'],
234 + 'View note with pager'
235 + ]
236 + self.keybinds['view_note_json'] = \
237 + [
238 + parser.get(cfg_sec, 'kb_view_note_json'),
239 + ['titles', 'notes'],
240 + 'View note raw json'
241 + ]
242 + self.keybinds['pipe_note'] = \
243 + [
244 + parser.get(cfg_sec, 'kb_pipe_note'),
245 + ['titles', 'notes'],
246 + 'Pipe note contents'
247 + ]
248 + self.keybinds['view_next_note'] = \
249 + [
250 + parser.get(cfg_sec, 'kb_view_next_note'),
251 + ['notes'],
252 + 'View next note'
253 + ]
254 + self.keybinds['view_prev_note'] = \
255 + [
256 + parser.get(cfg_sec, 'kb_view_prev_note'),
257 + ['notes'],
258 + 'View previous note'
259 + ]
260 + self.keybinds['tabstop2'] = \
261 + [
262 + parser.get(cfg_sec, 'kb_tabstop2'),
263 + ['notes'],
264 + 'View with tabstop=2'
265 + ]
266 + self.keybinds['tabstop4'] = \
267 + [
268 + parser.get(cfg_sec, 'kb_tabstop4'),
269 + ['notes'],
270 + 'View with tabstop=4'
271 + ]
272 + self.keybinds['tabstop8'] = \
273 + [
274 + parser.get(cfg_sec, 'kb_tabstop8'),
275 + ['notes'],
276 + 'View with tabstop=8'
277 + ]
278 + self.keybinds['search_gstyle'] = \
279 + [
280 + parser.get(cfg_sec, 'kb_search_gstyle'),
281 + ['titles', 'notes'],
282 + 'Search using gstyle'
283 + ]
284 + self.keybinds['search_prev_gstyle'] = \
285 + [
286 + parser.get(cfg_sec, 'kb_search_prev_gstyle'),
287 + ['notes'],
288 + 'Search backwards using gstyle'
289 + ]
290 + self.keybinds['search_regex'] = \
291 + [
292 + parser.get(cfg_sec, 'kb_search_regex'),
293 + ['titles', 'notes'],
294 + 'Search using regex'
295 + ]
296 + self.keybinds['search_prev_regex'] = \
297 + [
298 + parser.get(cfg_sec, 'kb_search_prev_regex'),
299 + ['notes'],
300 + 'Search backwards using regex'
301 + ]
302 + self.keybinds['search_next'] = \
303 + [
304 + parser.get(cfg_sec, 'kb_search_next'),
305 + ['notes'],
306 + 'Go to next search result'
307 + ]
308 + self.keybinds['search_prev'] = \
309 + [
310 + parser.get(cfg_sec, 'kb_search_prev'),
311 + ['notes'],
312 + 'Go to previous search result'
313 + ]
314 + self.keybinds['clear_search'] = \
315 + [
316 + parser.get(cfg_sec, 'kb_clear_search'),
317 + ['titles'],
318 + 'Show all notes'
319 + ]
320 + self.keybinds['sort_date'] = \
321 + [
322 + parser.get(cfg_sec, 'kb_sort_date'),
323 + ['titles'],
324 + 'Sort notes by date'
325 + ]
326 + self.keybinds['sort_alpha'] = \
327 + [
328 + parser.get(cfg_sec, 'kb_sort_alpha'),
329 + ['titles'],
330 + 'Sort notes by alpha'
331 + ]
332 + self.keybinds['sort_categories'] = \
333 + [
334 + parser.get(cfg_sec, 'kb_sort_categories'),
335 + ['titles'],
336 + 'Sort notes by categories'
337 + ]
338 + self.keybinds['note_delete'] = \
339 + [
340 + parser.get(cfg_sec, 'kb_note_delete'),
341 + ['titles', 'notes'],
342 + 'Delete a note'
343 + ]
344 + self.keybinds['note_favorite'] = \
345 + [
346 + parser.get(cfg_sec, 'kb_note_favorite'),
347 + ['titles', 'notes'],
348 + 'Favorite note'
349 + ]
350 + self.keybinds['note_category'] = \
351 + [
352 + parser.get(cfg_sec, 'kb_note_category'),
353 + ['titles', 'notes'],
354 + 'Edit note category'
355 + ]
356 + self.keybinds['copy_note_text'] = \
357 + [
358 + parser.get(cfg_sec, 'kb_copy_note_text'),
359 + ['notes'],
360 + 'Copy line (xsel/pbcopy)'
361 + ]
362 +
363 + def _create_colors_dict(self, parser, cfg_sec):
364 + """Create an OrderedDict object with the colors"""
365 + self.colors = collections.OrderedDict()
366 + self.colors['default_fg'] = \
367 + [parser.get(cfg_sec, 'clr_default_fg'), 'Default fg']
368 + self.colors['default_bg'] = \
369 + [parser.get(cfg_sec, 'clr_default_bg'), 'Default bg']
370 + self.colors['status_bar_fg'] = \
371 + [parser.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg']
372 + self.colors['status_bar_bg'] = \
373 + [parser.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg']
374 + self.colors['log_fg'] = \
375 + [parser.get(cfg_sec, 'clr_log_fg'), 'Log message fg']
376 + self.colors['log_bg'] = \
377 + [parser.get(cfg_sec, 'clr_log_bg'), 'Log message bg']
378 + self.colors['user_input_bar_fg'] = \
379 + [
380 + parser.get(cfg_sec, 'clr_user_input_bar_fg'),
381 + 'User input bar fg'
382 + ]
383 + self.colors['user_input_bar_bg'] = \
384 + [
385 + parser.get(cfg_sec, 'clr_user_input_bar_bg'),
386 + 'User input bar bg'
387 + ]
388 + self.colors['note_focus_fg'] = \
389 + [
390 + parser.get(cfg_sec, 'clr_note_focus_fg'),
391 + 'Note title focus fg'
392 + ]
393 + self.colors['note_focus_bg'] = \
394 + [
395 + parser.get(cfg_sec, 'clr_note_focus_bg'),
396 + 'Note title focus bg'
397 + ]
398 + self.colors['note_title_day_fg'] = \
399 + [
400 + parser.get(cfg_sec, 'clr_note_title_day_fg'),
401 + 'Day old note title fg'
402 + ]
403 + self.colors['note_title_day_bg'] = \
404 + [
405 + parser.get(cfg_sec, 'clr_note_title_day_bg'),
406 + 'Day old note title bg'
407 + ]
408 + self.colors['note_title_week_fg'] = \
409 + [
410 + parser.get(cfg_sec, 'clr_note_title_week_fg'),
411 + 'Week old note title fg'
412 + ]
413 + self.colors['note_title_week_bg'] = \
414 + [
415 + parser.get(cfg_sec, 'clr_note_title_week_bg'),
416 + 'Week old note title bg'
417 + ]
418 + self.colors['note_title_month_fg'] = \
419 + [
420 + parser.get(cfg_sec, 'clr_note_title_month_fg'),
421 + 'Month old note title fg'
422 + ]
423 + self.colors['note_title_month_bg'] = \
424 + [
425 + parser.get(cfg_sec, 'clr_note_title_month_bg'),
426 + 'Month old note title bg'
427 + ]
428 + self.colors['note_title_year_fg'] = \
429 + [
430 + parser.get(cfg_sec, 'clr_note_title_year_fg'),
431 + 'Year old note title fg'
432 + ]
433 + self.colors['note_title_year_bg'] = \
434 + [
435 + parser.get(cfg_sec, 'clr_note_title_year_bg'),
436 + 'Year old note title bg'
437 + ]
438 + self.colors['note_title_ancient_fg'] = \
439 + [
440 + parser.get(cfg_sec, 'clr_note_title_ancient_fg'),
441 + 'Ancient note title fg'
442 + ]
443 + self.colors['note_title_ancient_bg'] = \
444 + [
445 + parser.get(cfg_sec, 'clr_note_title_ancient_bg'),
446 + 'Ancient note title bg'
447 + ]
448 + self.colors['note_date_fg'] = \
449 + [parser.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg']
450 + self.colors['note_date_bg'] = \
451 + [parser.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg']
452 + self.colors['note_flags_fg'] = \
453 + [parser.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg']
454 + self.colors['note_flags_bg'] = \
455 + [parser.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg']
456 + self.colors['note_category_fg'] = \
457 + [
458 + parser.get(cfg_sec, 'clr_note_category_fg'),
459 + 'Note category fg'
460 + ]
461 + self.colors['note_category_bg'] = \
462 + [
463 + parser.get(cfg_sec, 'clr_note_category_bg'),
464 + 'Note category bg'
465 + ]
466 + self.colors['note_content_fg'] = \
467 + [parser.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg']
468 + self.colors['note_content_bg'] = \
469 + [parser.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg']
470 + self.colors['note_content_focus_fg'] = \
471 + [
472 + parser.get(cfg_sec, 'clr_note_content_focus_fg'),
473 + 'Note content focus fg'
474 + ]
475 + self.colors['note_content_focus_bg'] = \
476 + [
477 + parser.get(cfg_sec, 'clr_note_content_focus_bg'),
478 + 'Note content focus bg'
479 + ]
480 + self.colors['note_content_old_fg'] = \
481 + [
482 + parser.get(cfg_sec, 'clr_note_content_old_fg'),
483 + 'Old note content fg'
484 + ]
485 + self.colors['note_content_old_bg'] = \
486 + [
487 + parser.get(cfg_sec, 'clr_note_content_old_bg'),
488 + 'Old note content bg'
489 + ]
490 + self.colors['note_content_old_focus_fg'] = \
491 + [
492 + parser.get(cfg_sec, 'clr_note_content_old_focus_fg'),
493 + 'Old note content focus fg'
494 + ]
495 + self.colors['note_content_old_focus_bg'] = \
496 + [
497 + parser.get(cfg_sec, 'clr_note_content_old_focus_bg'),
498 + 'Old note content focus bg'
499 + ]
500 + self.colors['help_focus_fg'] = \
501 + [parser.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg']
502 + self.colors['help_focus_bg'] = \
503 + [parser.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg']
504 + self.colors['help_header_fg'] = \
505 + [parser.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg']
506 + self.colors['help_header_bg'] = \
507 + [parser.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg']
508 + self.colors['help_config_fg'] = \
509 + [parser.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg']
510 + self.colors['help_config_bg'] = \
511 + [parser.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg']
512 + self.colors['help_value_fg'] = \
513 + [parser.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg']
514 + self.colors['help_value_bg'] = \
515 + [parser.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg']
516 + self.colors['help_descr_fg'] = \
517 + [
518 + parser.get(cfg_sec, 'clr_help_descr_fg'),
519 + 'Help description fg'
520 + ]
521 + self.colors['help_descr_bg'] = \
522 + [
523 + parser.get(cfg_sec, 'clr_help_descr_bg'),
524 + 'Help description bg'
525 + ]
526 +
527 + def _create_configs_dict(self, parser, cfg_sec):
528 + """Create an OrderedDict object with the configs"""
529 +
530 + # special handling for password so we can retrieve it by
531 + # running a command
532 + nn_password = parser.get(cfg_sec, 'cfg_nn_password', raw=True)
133 533 if not nn_password:
134 - command = cp.get(cfg_sec, 'cfg_nn_password_eval', raw=True)
534 + command = parser.get(cfg_sec, 'cfg_nn_password_eval', raw=True)
135 535 if command:
136 536 try:
137 - nn_password = subprocess.check_output(command, shell=True, universal_newlines=True)
138 - # remove trailing newlines to avoid requiring butchering shell commands (they can't usually be in passwords anyway)
537 + nn_password = subprocess.check_output(
538 + command,
539 + shell=True,
540 + universal_newlines=True
541 + )
542 + # remove trailing newlines to avoid requiring
543 + # butchering shell commands (they can't usually be
544 + # in passwords anyway)
139 545 nn_password = nn_password.rstrip('\n')
140 - except subprocess.CalledProcessError as e:
141 - print('Error evaluating command for password.')
142 - print(e)
546 + except subprocess.CalledProcessError as ex:
547 + print('Error evaluating command for password: %s' % ex)
143 548 sys.exit(1)
144 -
145 - # ordered dicts used to ease help
146 549
147 550 self.configs = collections.OrderedDict()
148 - self.configs['nn_username'] = [ cp.get(cfg_sec, 'cfg_nn_username', raw=True), 'NextCloud Username' ]
149 - self.configs['nn_password'] = [ nn_password, 'NextCloud Password' ]
150 - self.configs['nn_host'] = [ cp.get(cfg_sec, 'cfg_nn_host', raw=True), 'NextCloud server hostname' ]
151 - self.configs['db_path'] = [ cp.get(cfg_sec, 'cfg_db_path'), 'Note storage path' ]
152 - self.configs['search_categories'] = [ cp.get(cfg_sec, 'cfg_search_categories'), 'Search categories as well' ]
153 - self.configs['sort_mode'] = [ cp.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode' ]
154 - self.configs['favorite_ontop'] = [ cp.get(cfg_sec, 'cfg_favorite_ontop'), 'Favorite at top of list' ]
155 - self.configs['tabstop'] = [ cp.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces' ]
156 - self.configs['format_strftime'] = [ cp.get(cfg_sec, 'cfg_format_strftime', raw=True), 'Date strftime format' ]
157 - self.configs['format_note_title'] = [ cp.get(cfg_sec, 'cfg_format_note_title', raw=True), 'Note title format' ]
158 - self.configs['status_bar'] = [ cp.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar' ]
159 - self.configs['editor'] = [ cp.get(cfg_sec, 'cfg_editor'), 'Editor command' ]
160 - self.configs['pager'] = [ cp.get(cfg_sec, 'cfg_pager'), 'External pager command' ]
161 - self.configs['max_logs'] = [ cp.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer' ]
162 - self.configs['log_timeout'] = [ cp.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout' ]
163 - self.configs['log_reversed'] = [ cp.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed' ]
164 - self.configs['tempdir'] = [ cp.get(cfg_sec, 'cfg_tempdir'), 'Temporary directory for note storage' ]
165 -
166 - self.keybinds = collections.OrderedDict()
167 - self.keybinds['help'] = [ cp.get(cfg_sec, 'kb_help'), [ 'common' ], 'Help' ]
168 - self.keybinds['quit'] = [ cp.get(cfg_sec, 'kb_quit'), [ 'common' ], 'Quit' ]
169 - self.keybinds['sync'] = [ cp.get(cfg_sec, 'kb_sync'), [ 'common' ], 'Full sync' ]
170 - self.keybinds['down'] = [ cp.get(cfg_sec, 'kb_down'), [ 'common' ], 'Scroll down one line' ]
171 - self.keybinds['up'] = [ cp.get(cfg_sec, 'kb_up'), [ 'common' ], 'Scroll up one line' ]
172 - self.keybinds['page_down'] = [ cp.get(cfg_sec, 'kb_page_down'), [ 'common' ], 'Page down' ]
173 - self.keybinds['page_up'] = [ cp.get(cfg_sec, 'kb_page_up'), [ 'common' ], 'Page up' ]
174 - self.keybinds['half_page_down'] = [ cp.get(cfg_sec, 'kb_half_page_down'), [ 'common' ], 'Half page down' ]
175 - self.keybinds['half_page_up'] = [ cp.get(cfg_sec, 'kb_half_page_up'), [ 'common' ], 'Half page up' ]
176 - self.keybinds['bottom'] = [ cp.get(cfg_sec, 'kb_bottom'), [ 'common' ], 'Goto bottom' ]
177 - self.keybinds['top'] = [ cp.get(cfg_sec, 'kb_top'), [ 'common' ], 'Goto top' ]
178 - self.keybinds['status'] = [ cp.get(cfg_sec, 'kb_status'), [ 'common' ], 'Toggle status bar' ]
179 - self.keybinds['view_log'] = [ cp.get(cfg_sec, 'kb_view_log'), [ 'common' ], 'View log' ]
180 - self.keybinds['create_note'] = [ cp.get(cfg_sec, 'kb_create_note'), [ 'titles' ], 'Create a new note' ]
181 - self.keybinds['edit_note'] = [ cp.get(cfg_sec, 'kb_edit_note'), [ 'titles', 'notes' ], 'Edit note' ]
182 - self.keybinds['view_note'] = [ cp.get(cfg_sec, 'kb_view_note'), [ 'titles' ], 'View note' ]
183 - self.keybinds['view_note_ext'] = [ cp.get(cfg_sec, 'kb_view_note_ext'), [ 'titles', 'notes' ], 'View note with pager' ]
184 - self.keybinds['view_note_json'] = [ cp.get(cfg_sec, 'kb_view_note_json'), [ 'titles', 'notes' ], 'View note raw json' ]
185 - self.keybinds['pipe_note'] = [ cp.get(cfg_sec, 'kb_pipe_note'), [ 'titles', 'notes' ], 'Pipe note contents' ]
186 - self.keybinds['view_next_note'] = [ cp.get(cfg_sec, 'kb_view_next_note'), [ 'notes' ], 'View next note' ]
187 - self.keybinds['view_prev_note'] = [ cp.get(cfg_sec, 'kb_view_prev_note'), [ 'notes' ], 'View previous note' ]
188 - self.keybinds['tabstop2'] = [ cp.get(cfg_sec, 'kb_tabstop2'), [ 'notes' ], 'View with tabstop=2' ]
189 - self.keybinds['tabstop4'] = [ cp.get(cfg_sec, 'kb_tabstop4'), [ 'notes' ], 'View with tabstop=4' ]
190 - self.keybinds['tabstop8'] = [ cp.get(cfg_sec, 'kb_tabstop8'), [ 'notes' ], 'View with tabstop=8' ]
191 - self.keybinds['search_gstyle'] = [ cp.get(cfg_sec, 'kb_search_gstyle'), [ 'titles', 'notes' ], 'Search using gstyle' ]
192 - self.keybinds['search_prev_gstyle'] = [ cp.get(cfg_sec, 'kb_search_prev_gstyle'), [ 'notes' ], 'Search backwards using gstyle' ]
193 - self.keybinds['search_regex'] = [ cp.get(cfg_sec, 'kb_search_regex'), [ 'titles', 'notes' ], 'Search using regex' ]
194 - self.keybinds['search_prev_regex'] = [ cp.get(cfg_sec, 'kb_search_prev_regex'), [ 'notes' ], 'Search backwards using regex' ]
195 - self.keybinds['search_next'] = [ cp.get(cfg_sec, 'kb_search_next'), [ 'notes' ], 'Go to next search result' ]
196 - self.keybinds['search_prev'] = [ cp.get(cfg_sec, 'kb_search_prev'), [ 'notes' ], 'Go to previous search result' ]
197 - self.keybinds['clear_search'] = [ cp.get(cfg_sec, 'kb_clear_search'), [ 'titles' ], 'Show all notes' ]
198 - self.keybinds['sort_date'] = [ cp.get(cfg_sec, 'kb_sort_date'), [ 'titles' ], 'Sort notes by date' ]
199 - self.keybinds['sort_alpha'] = [ cp.get(cfg_sec, 'kb_sort_alpha'), [ 'titles' ], 'Sort notes by alpha' ]
200 - self.keybinds['sort_categories'] = [ cp.get(cfg_sec, 'kb_sort_categories'), [ 'titles' ], 'Sort notes by categories' ]
201 - self.keybinds['note_delete'] = [ cp.get(cfg_sec,'kb_note_delete'), [ 'titles', 'notes' ], 'Delete a note' ]
202 - self.keybinds['note_favorite'] = [ cp.get(cfg_sec, 'kb_note_favorite'), [ 'titles', 'notes' ], 'Favorite note' ]
203 - self.keybinds['note_category'] = [ cp.get(cfg_sec, 'kb_note_category'), [ 'titles', 'notes' ], 'Edit note category' ]
204 - self.keybinds['copy_note_text'] = [ cp.get(cfg_sec, 'kb_copy_note_text'), [ 'notes' ], 'Copy line (xsel/pbcopy)' ]
205 -
206 - self.colors = collections.OrderedDict()
207 - self.colors['default_fg'] = [ cp.get(cfg_sec, 'clr_default_fg'), 'Default fg' ]
208 - self.colors['default_bg'] = [ cp.get(cfg_sec, 'clr_default_bg'), 'Default bg' ]
209 - self.colors['status_bar_fg'] = [ cp.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg' ]
210 - self.colors['status_bar_bg'] = [ cp.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg' ]
211 - self.colors['log_fg'] = [ cp.get(cfg_sec, 'clr_log_fg'), 'Log message fg' ]
212 - self.colors['log_bg'] = [ cp.get(cfg_sec, 'clr_log_bg'), 'Log message bg' ]
213 - self.colors['user_input_bar_fg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_fg'), 'User input bar fg' ]
214 - self.colors['user_input_bar_bg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_bg'), 'User input bar bg' ]
215 - self.colors['note_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_focus_fg'), 'Note title focus fg' ]
216 - self.colors['note_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_focus_bg'), 'Note title focus bg' ]
217 - self.colors['note_title_day_fg'] = [ cp.get(cfg_sec, 'clr_note_title_day_fg'), 'Day old note title fg' ]
218 - self.colors['note_title_day_bg'] = [ cp.get(cfg_sec, 'clr_note_title_day_bg'), 'Day old note title bg' ]
219 - self.colors['note_title_week_fg'] = [ cp.get(cfg_sec, 'clr_note_title_week_fg'), 'Week old note title fg' ]
220 - self.colors['note_title_week_bg'] = [ cp.get(cfg_sec, 'clr_note_title_week_bg'), 'Week old note title bg' ]
221 - self.colors['note_title_month_fg'] = [ cp.get(cfg_sec, 'clr_note_title_month_fg'), 'Month old note title fg' ]
222 - self.colors['note_title_month_bg'] = [ cp.get(cfg_sec, 'clr_note_title_month_bg'), 'Month old note title bg' ]
223 - self.colors['note_title_year_fg'] = [ cp.get(cfg_sec, 'clr_note_title_year_fg'), 'Year old note title fg' ]
224 - self.colors['note_title_year_bg'] = [ cp.get(cfg_sec, 'clr_note_title_year_bg'), 'Year old note title bg' ]
225 - self.colors['note_title_ancient_fg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_fg'), 'Ancient note title fg' ]
226 - self.colors['note_title_ancient_bg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_bg'), 'Ancient note title bg' ]
227 - self.colors['note_date_fg'] = [ cp.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg' ]
228 - self.colors['note_date_bg'] = [ cp.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg' ]
229 - self.colors['note_flags_fg'] = [ cp.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg' ]
230 - self.colors['note_flags_bg'] = [ cp.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg' ]
231 - self.colors['note_category_fg'] = [ cp.get(cfg_sec, 'clr_note_category_fg'), 'Note category fg' ]
232 - self.colors['note_category_bg'] = [ cp.get(cfg_sec, 'clr_note_category_bg'), 'Note category bg' ]
233 - self.colors['note_content_fg'] = [ cp.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg' ]
234 - self.colors['note_content_bg'] = [ cp.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg' ]
235 - self.colors['note_content_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_fg'), 'Note content focus fg' ]
236 - self.colors['note_content_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_bg'), 'Note content focus bg' ]
237 - self.colors['note_content_old_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_fg'), 'Old note content fg' ]
238 - self.colors['note_content_old_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_bg'), 'Old note content bg' ]
239 - self.colors['note_content_old_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_fg'), 'Old note content focus fg' ]
240 - self.colors['note_content_old_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_bg'), 'Old note content focus bg' ]
241 - self.colors['help_focus_fg'] = [ cp.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg' ]
242 - self.colors['help_focus_bg'] = [ cp.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg' ]
243 - self.colors['help_header_fg'] = [ cp.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg' ]
244 - self.colors['help_header_bg'] = [ cp.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg' ]
245 - self.colors['help_config_fg'] = [ cp.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg' ]
246 - self.colors['help_config_bg'] = [ cp.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg' ]
247 - self.colors['help_value_fg'] = [ cp.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg' ]
248 - self.colors['help_value_bg'] = [ cp.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg' ]
249 - self.colors['help_descr_fg'] = [ cp.get(cfg_sec, 'clr_help_descr_fg'), 'Help description fg' ]
250 - self.colors['help_descr_bg'] = [ cp.get(cfg_sec, 'clr_help_descr_bg'), 'Help description bg' ]
551 + self.configs['nn_username'] = \
552 + [
553 + parser.get(cfg_sec, 'cfg_nn_username', raw=True),
554 + 'NextCloud Username'
555 + ]
556 + self.configs['nn_password'] = [nn_password, 'NextCloud Password']
557 + self.configs['nn_host'] = \
558 + [
559 + parser.get(cfg_sec, 'cfg_nn_host', raw=True),
560 + 'NextCloud server hostname'
561 + ]
562 + self.configs['db_path'] = \
563 + [parser.get(cfg_sec, 'cfg_db_path'), 'Note storage path']
564 + self.configs['search_categories'] = \
565 + [
566 + parser.get(cfg_sec, 'cfg_search_categories'),
567 + 'Search categories as well'
568 + ]
569 + self.configs['sort_mode'] = \
570 + [parser.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode']
571 + self.configs['favorite_ontop'] = \
572 + [
573 + parser.get(cfg_sec, 'cfg_favorite_ontop'),
574 + 'Favorite at top of list'
575 + ]
576 + self.configs['tabstop'] = \
577 + [parser.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces']
578 + self.configs['format_strftime'] = \
579 + [
580 + parser.get(cfg_sec, 'cfg_format_strftime', raw=True),
581 + 'Date strftime format'
582 + ]
583 + self.configs['format_note_title'] = \
584 + [
585 + parser.get(cfg_sec, 'cfg_format_note_title', raw=True),
586 + 'Note title format'
587 + ]
588 + self.configs['status_bar'] = \
589 + [parser.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar']
590 + self.configs['editor'] = \
591 + [parser.get(cfg_sec, 'cfg_editor'), 'Editor command']
592 + self.configs['pager'] = \
593 + [parser.get(cfg_sec, 'cfg_pager'), 'External pager command']
594 + self.configs['max_logs'] = \
595 + [parser.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer']
596 + self.configs['log_timeout'] = \
597 + [parser.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout']
598 + self.configs['log_reversed'] = \
599 + [parser.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed']
600 + self.configs['tempdir'] = \
601 + [
602 + None if parser.get(cfg_sec, 'cfg_tempdir') == '' \
603 + else parser.get(cfg_sec, 'cfg_tempdir'),
604 + 'Temporary directory for note storage'
605 + ]
251 606
252 607 def get_config(self, name):
608 + """Get a config value"""
253 609 return self.configs[name][0]
254 610
255 611 def get_config_descr(self, name):
612 + """Get a config description"""
256 613 return self.configs[name][1]
257 614
258 615 def get_keybind(self, name):
616 + """Get a keybinding value"""
259 617 return self.keybinds[name][0]
260 618
261 619 def get_keybind_use(self, name):
620 + """Get the context(s) where a keybinding is valid"""
262 621 return self.keybinds[name][1]
263 622
264 623 def get_keybind_descr(self, name):
624 + """Get a keybinding description"""
265 625 return self.keybinds[name][2]
266 626
267 627 def get_color(self, name):
628 + """Get a color value"""
268 629 return self.colors[name][0]
269 630
270 631 def get_color_descr(self, name):
632 + """Get a color description"""
271 633 return self.colors[name][1]

nncli/gui.py (created)

1 +# -*- coding: utf-8 -*-
2 +"""nncli_gui module"""
3 +import hashlib
4 +import subprocess
5 +import threading
6 +
7 +import urwid
8 +from . import view_titles, view_note, view_help, view_log, user_input
9 +from .utils import exec_cmd_on_note, get_pager
10 +
11 +# pylint: disable=too-many-instance-attributes, unused-argument
12 +class NncliGui:
13 + """NncliGui class. Responsible for the console GUI view logic."""
14 + def __init__(self, config, logger, ndb, key=None):
15 + self.ndb = ndb
16 + self.logger = logger
17 + self.config = config
18 + self.last_view = []
19 + self.status_bar = self.config.get_config('status_bar')
20 + self.config.state.current_sort_mode = \
21 + self.config.get_config('sort_mode')
22 +
23 +
24 + self.log_lock = threading.Lock()
25 + self.log_alarms = 0
26 + self.logs = []
27 +
28 + self.thread_sync = threading.Thread(
29 + target=self.ndb.sync_worker,
30 + args=[self.config.state.do_server_sync]
31 + )
32 + self.thread_sync.setDaemon(True)
33 +
34 + self.view_titles = \
35 + view_titles.ViewTitles(
36 + self.config,
37 + {
38 + 'ndb' : self.ndb,
39 + 'search_string' : None,
40 + 'log' : self.log
41 + }
42 + )
43 + self.view_note = \
44 + view_note.ViewNote(
45 + self.config,
46 + {
47 + 'ndb' : self.ndb,
48 + 'id' : key, # initial key to view or None
49 + 'log' : self.log
50 + }
51 + )
52 +
53 + self.view_log = view_log.ViewLog(self.config)
54 + self.view_help = view_help.ViewHelp(self.config)
55 +
56 + palette = \
57 + [
58 + (
59 + 'default',
60 + self.config.get_color('default_fg'),
61 + self.config.get_color('default_bg')
62 + ),
63 + (
64 + 'status_bar',
65 + self.config.get_color('status_bar_fg'),
66 + self.config.get_color('status_bar_bg')
67 + ),
68 + (
69 + 'log',
70 + self.config.get_color('log_fg'),
71 + self.config.get_color('log_bg')
72 + ),
73 + (
74 + 'user_input_bar',
75 + self.config.get_color('user_input_bar_fg'),
76 + self.config.get_color('user_input_bar_bg')
77 + ),
78 + (
79 + 'note_focus',
80 + self.config.get_color('note_focus_fg'),
81 + self.config.get_color('note_focus_bg')
82 + ),
83 + (
84 + 'note_title_day',
85 + self.config.get_color('note_title_day_fg'),
86 + self.config.get_color('note_title_day_bg')
87 + ),
88 + (
89 + 'note_title_week',
90 + self.config.get_color('note_title_week_fg'),
91 + self.config.get_color('note_title_week_bg')
92 + ),
93 + (
94 + 'note_title_month',
95 + self.config.get_color('note_title_month_fg'),
96 + self.config.get_color('note_title_month_bg')
97 + ),
98 + (
99 + 'note_title_year',
100 + self.config.get_color('note_title_year_fg'),
101 + self.config.get_color('note_title_year_bg')
102 + ),
103 + (
104 + 'note_title_ancient',
105 + self.config.get_color('note_title_ancient_fg'),
106 + self.config.get_color('note_title_ancient_bg')
107 + ),
108 + (
109 + 'note_date',
110 + self.config.get_color('note_date_fg'),
111 + self.config.get_color('note_date_bg')
112 + ),
113 + (
114 + 'note_flags',
115 + self.config.get_color('note_flags_fg'),
116 + self.config.get_color('note_flags_bg')
117 + ),
118 + (
119 + 'note_category',
120 + self.config.get_color('note_category_fg'),
121 + self.config.get_color('note_category_bg')
122 + ),
123 + (
124 + 'note_content',
125 + self.config.get_color('note_content_fg'),
126 + self.config.get_color('note_content_bg')
127 + ),
128 + (
129 + 'note_content_focus',
130 + self.config.get_color('note_content_focus_fg'),
131 + self.config.get_color('note_content_focus_bg')
132 + ),
133 + (
134 + 'note_content_old',
135 + self.config.get_color('note_content_old_fg'),
136 + self.config.get_color('note_content_old_bg')
137 + ),
138 + (
139 + 'note_content_old_focus',
140 + self.config.get_color(
141 + 'note_content_old_focus_fg'
142 + ),
143 + self.config.get_color(
144 + 'note_content_old_focus_bg'
145 + )
146 + ),
147 + (
148 + 'help_focus',
149 + self.config.get_color('help_focus_fg'),
150 + self.config.get_color('help_focus_bg')
151 + ),
152 + (
153 + 'help_header',
154 + self.config.get_color('help_header_fg'),
155 + self.config.get_color('help_header_bg')
156 + ),
157 + (
158 + 'help_config',
159 + self.config.get_color('help_config_fg'),
160 + self.config.get_color('help_config_bg')
161 + ),
162 + (
163 + 'help_value',
164 + self.config.get_color('help_value_fg'),
165 + self.config.get_color('help_value_bg')
166 + ),
167 + (
168 + 'help_descr',
169 + self.config.get_color('help_descr_fg'),
170 + self.config.get_color('help_descr_bg')
171 + )
172 + ]
173 +
174 + self.master_frame = urwid.Frame(
175 + body=urwid.Filler(urwid.Text('')),
176 + header=None,
177 + footer=urwid.Pile([urwid.Pile([]), urwid.Pile([])]),
178 + focus_part='body')
179 +
180 + self.nncli_loop = urwid.MainLoop(self.master_frame,
181 + palette,
182 + handle_mouse=False)
183 +
184 + self.nncli_loop.set_alarm_in(0, self._gui_init_view, \
185 + True if key else False)
186 +
187 + def run(self):
188 + """Run the GUI"""
189 + self.nncli_loop.run()
190 +
191 + def _gui_header_clear(self):
192 + """Clear the console GUI header row"""
193 + self.master_frame.contents['header'] = (None, None)
194 + self.nncli_loop.draw_screen()
195 +
196 + def _gui_header_set(self, widget):
197 + """Set the content of the console GUI header row"""
198 + self.master_frame.contents['header'] = (widget, None)
199 + self.nncli_loop.draw_screen()
200 +
201 + def _gui_footer_log_clear(self):
202 + """Clear the log at the bottom of the GUI"""
203 + gui = self._gui_footer_input_get()
204 + self.master_frame.contents['footer'] = \
205 + (urwid.Pile([urwid.Pile([]), urwid.Pile([gui])]), None)
206 + self.nncli_loop.draw_screen()
207 +
208 + def _gui_footer_log_set(self, pile):
209 + """Set the log at the bottom of the GUI"""
210 + gui = self._gui_footer_input_get()
211 + self.master_frame.contents['footer'] = \
212 + (urwid.Pile([urwid.Pile(pile), urwid.Pile([gui])]), None)
213 + self.nncli_loop.draw_screen()
214 +
215 + def _gui_footer_log_get(self):
216 + """Get the log at the bottom of the GUI"""
217 + return self.master_frame.contents['footer'][0].contents[0][0]
218 +
219 + def _gui_footer_input_clear(self):
220 + """Clear the input at the bottom of the GUI"""
221 + pile = self._gui_footer_log_get()
222 + self.master_frame.contents['footer'] = \
223 + (urwid.Pile([urwid.Pile([pile]), urwid.Pile([])]), None)
224 + self.nncli_loop.draw_screen()
225 +
226 + def _gui_footer_input_set(self, gui):
227 + """Set the input at the bottom of the GUI"""
228 + pile = self._gui_footer_log_get()
229 + self.master_frame.contents['footer'] = \
230 + (urwid.Pile([urwid.Pile([pile]), urwid.Pile([gui])]), None)
231 + self.nncli_loop.draw_screen()
232 +
233 + def _gui_footer_input_get(self):
234 + """Get the input at the bottom of the GUI"""
235 + return self.master_frame.contents['footer'][0].contents[1][0]
236 +
237 + def _gui_footer_focus_input(self):
238 + """Set the GUI focus to the input at the bottom of the GUI"""
239 + self.master_frame.focus_position = 'footer'
240 + self.master_frame.contents['footer'][0].focus_position = 1
241 +
242 + def _gui_body_set(self, widget):
243 + """Set the GUI body"""
244 + self.master_frame.contents['body'] = (widget, None)
245 + self._gui_update_status_bar()
246 + self.nncli_loop.draw_screen()
247 +
248 + def gui_body_get(self):
249 + """Get the GUI body"""
250 + return self.master_frame.contents['body'][0]
251 +
252 + def _gui_body_focus(self):
253 + """Set the GUI focus to the body"""
254 + self.master_frame.focus_position = 'body'
255 +
256 + def gui_update_view(self):
257 + """Update the GUI"""
258 + if not self.config.state.do_gui:
259 + return
260 +
261 + try:
262 + cur_key = self.view_titles.note_list \
263 + [self.view_titles.focus_position].note['localkey']
264 + except IndexError:
265 + cur_key = None
266 +
267 + self.view_titles.update_note_list(
268 + self.view_titles.search_string,
269 + sort_mode=self.config.state.current_sort_mode
270 + )
271 + self.view_titles.focus_note(cur_key)
272 +
273 + if self.gui_body_get().__class__ == view_note.ViewNote:
274 + self.view_note.update_note_view()
275 +
276 + self._gui_update_status_bar()
277 +
278 + def _gui_update_status_bar(self):
279 + """Update the GUI status bar"""
280 + if self.status_bar != 'yes':
281 + self._gui_header_clear()
282 + else:
283 + self._gui_header_set(self.gui_body_get().get_status_bar())
284 +
285 + def _gui_switch_frame_body(self, new_view, save_current_view=True):
286 + """
287 + Switch the body frame of the GUI. Used to switch to a new
288 + view
289 + """
290 + if new_view is None:
291 + if not self.last_view:
292 + self._gui_stop()
293 + else:
294 + self._gui_body_set(self.last_view.pop())
295 + else:
296 + if self.gui_body_get().__class__ != new_view.__class__:
297 + if save_current_view:
298 + self.last_view.append(self.gui_body_get())
299 + self._gui_body_set(new_view)
300 +
301 + def _delete_note_callback(self, key, delete):
302 + """Update the GUI after deleting a note"""
303 + if not delete:
304 + return
305 + self.ndb.set_note_deleted(key, True)
306 +
307 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
308 + self.view_titles.update_note_title()
309 +
310 + self._gui_update_status_bar()
311 + self.ndb.sync_worker_go()
312 +
313 + def _gui_yes_no_input(self, args, yes_no):
314 + """Create a yes/no input dialog at the GUI footer"""
315 + self._gui_footer_input_clear()
316 + self._gui_body_focus()
317 + self.master_frame.keypress = self._gui_frame_keypress
318 + args[0](args[1],
319 + True if yes_no in ['YES', 'Yes', 'yes', 'Y', 'y'] \
320 + else False
321 + )
322 +
323 + def _gui_search_input(self, args, search_string):
324 + """Create a search input dialog at the GUI footer"""
325 + self._gui_footer_input_clear()
326 + self._gui_body_focus()
327 + self.master_frame.keypress = self._gui_frame_keypress
328 + if search_string:
329 + if self.gui_body_get() == self.view_note:
330 + self.config.state.search_direction = args[1]
331 + self.view_note.search_note_view_next(
332 + search_string=search_string,
333 + search_mode=args[0]
334 + )
335 + else:
336 + self.view_titles.update_note_list(
337 + search_string,
338 + args[0],
339 + sort_mode=self.config.state.current_sort_mode
340 + )
341 + self._gui_body_set(self.view_titles)
342 +
343 + def _gui_category_input(self, args, category):
344 + """Create a category input at the GUI footer"""
345 + self._gui_footer_input_clear()
346 + self._gui_body_focus()
347 + self.master_frame.keypress = self._gui_frame_keypress
348 + if category is not None:
349 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
350 + note = self.view_titles.note_list \
351 + [self.view_titles.focus_position].note
352 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
353 + note = self.view_note.note
354 +
355 + self.ndb.set_note_category(note['localkey'], category)
356 +
357 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
358 + self.view_titles.update_note_title()
359 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
360 + self.view_note.update_note_view()
361 +
362 + self._gui_update_status_bar()
363 + self.ndb.sync_worker_go()
364 +
365 + def _gui_pipe_input(self, args, cmd):
366 + """Create a pipe input dialog at the GUI footoer"""
367 + self._gui_footer_input_clear()
368 + self._gui_body_focus()
369 + self.master_frame.keypress = self._gui_frame_keypress
370 + if cmd is not None:
371 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
372 + note = self.view_titles.note_list \
373 + [self.view_titles.focus_position].note
374 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
375 + note = self.view_note.old_note \
376 + if self.view_note.old_note \
377 + else self.view_note.note
378 + try:
379 + self._gui_clear()
380 + pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True)
381 + pipe.communicate(note['content'].encode('utf-8'))
382 + pipe.stdin.close()
383 + pipe.wait()
384 + except OSError as ex:
385 + self.log('Pipe error: %s' % ex)
386 + finally:
387 + self._gui_reset()
388 +
389 + # pylint: disable=too-many-return-statements, too-many-branches
390 + # pylint: disable=too-many-statements
391 + def _gui_frame_keypress(self, size, key):
392 + """Keypress handler for the GUI"""
393 + # convert space character into name
394 + if key == ' ':
395 + key = 'space'
396 +
397 + contents = self.gui_body_get()
398 +
399 + if key == self.config.get_keybind('quit'):
400 + self._gui_switch_frame_body(None)
401 +
402 + elif key == self.config.get_keybind('help'):
403 + self._gui_switch_frame_body(self.view_help)
404 +
405 + elif key == self.config.get_keybind('sync'):
406 + self.ndb.last_sync = 0
407 + self.ndb.sync_worker_go()
408 +
409 + elif key == self.config.get_keybind('view_log'):
410 + self.view_log.update_log()
411 + self._gui_switch_frame_body(self.view_log)
412 +
413 + elif key == self.config.get_keybind('down'):
414 + if not contents.body.positions():
415 + return None
416 + last = len(contents.body.positions())
417 + if contents.focus_position == (last - 1):
418 + return None
419 + contents.focus_position += 1
420 + contents.render(size)
421 +
422 + elif key == self.config.get_keybind('up'):
423 + if not contents.body.positions():
424 + return None
425 + if contents.focus_position == 0:
426 + return None
427 + contents.focus_position -= 1
428 + contents.render(size)
429 +
430 + elif key == self.config.get_keybind('page_down'):
431 + if not contents.body.positions():
432 + return None
433 + last = len(contents.body.positions())
434 + next_focus = contents.focus_position + size[1]
435 + if next_focus >= last:
436 + next_focus = last - 1
437 + contents.change_focus(size, next_focus,
438 + offset_inset=0,
439 + coming_from='above')
440 +
441 + elif key == self.config.get_keybind('page_up'):
442 + if not contents.body.positions():
443 + return None
444 + if 'bottom' in contents.ends_visible(size):
445 + last = len(contents.body.positions())
446 + next_focus = last - size[1] - size[1]
447 + else:
448 + next_focus = contents.focus_position - size[1]
449 + if next_focus < 0:
450 + next_focus = 0
451 + contents.change_focus(size, next_focus,
452 + offset_inset=0,
453 + coming_from='below')
454 +
455 + elif key == self.config.get_keybind('half_page_down'):
456 + if not contents.body.positions():
457 + return None
458 + last = len(contents.body.positions())
459 + next_focus = contents.focus_position + (size[1] // 2)
460 + if next_focus >= last:
461 + next_focus = last - 1
462 + contents.change_focus(size, next_focus,
463 + offset_inset=0,
464 + coming_from='above')
465 +
466 + elif key == self.config.get_keybind('half_page_up'):
467 + if not contents.body.positions():
468 + return None
469 + if 'bottom' in contents.ends_visible(size):
470 + last = len(contents.body.positions())
471 + next_focus = last - size[1] - (size[1] // 2)
472 + else:
473 + next_focus = contents.focus_position - (size[1] // 2)
474 + if next_focus < 0:
475 + next_focus = 0
476 + contents.change_focus(size, next_focus,
477 + offset_inset=0,
478 + coming_from='below')
479 +
480 + elif key == self.config.get_keybind('bottom'):
481 + if not contents.body.positions():
482 + return None
483 + contents.change_focus(size, (len(contents.body.positions()) - 1),
484 + offset_inset=0,
485 + coming_from='above')
486 +
487 + elif key == self.config.get_keybind('top'):
488 + if not contents.body.positions():
489 + return None
490 + contents.change_focus(size, 0,
491 + offset_inset=0,
492 + coming_from='below')
493 +
494 + elif key == self.config.get_keybind('view_next_note'):
495 + if self.gui_body_get().__class__ != view_note.ViewNote:
496 + return key
497 +
498 + if not self.view_titles.body.positions():
499 + return None
500 + last = len(self.view_titles.body.positions())
501 + if self.view_titles.focus_position == (last - 1):
502 + return None
503 + self.view_titles.focus_position += 1
504 + contents.update_note_view(
505 + self.view_titles. \
506 + note_list[self.view_titles. \
507 + focus_position].note['localkey']
508 + )
509 + self._gui_switch_frame_body(self.view_note)
510 +
511 + elif key == self.config.get_keybind('view_prev_note'):
512 + if self.gui_body_get().__class__ != view_note.ViewNote:
513 + return key
514 +
515 + if not self.view_titles.body.positions():
516 + return None
517 + if self.view_titles.focus_position == 0:
518 + return None
519 + self.view_titles.focus_position -= 1
520 + contents.update_note_view(
521 + self.view_titles. \
522 + note_list[self.view_titles. \
523 + focus_position].note['localkey']
524 + )
525 + self._gui_switch_frame_body(self.view_note)
526 +
527 + elif key == self.config.get_keybind('status'):
528 + if self.status_bar == 'yes':
529 + self.status_bar = 'no'
530 + else:
531 + self.status_bar = self.config.get_config('status_bar')
532 +
533 + elif key == self.config.get_keybind('create_note'):
534 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
535 + return key
536 +
537 + self._gui_clear()
538 + content = exec_cmd_on_note(None, self.config, self, self.logger)
539 + self._gui_reset()
540 +
541 + if content:
542 + self.log('New note created')
543 + self.ndb.create_note(content)
544 + self.gui_update_view()
545 + self.ndb.sync_worker_go()
546 +
547 + elif key == self.config.get_keybind('edit_note') or \
548 + key == self.config.get_keybind('view_note_ext') or \
549 + key == self.config.get_keybind('view_note_json'):
550 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
551 + self.gui_body_get().__class__ != view_note.ViewNote:
552 + return key
553 +
554 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
555 + if not contents.body.positions():
556 + return None
557 + note = contents.note_list[contents.focus_position].note
558 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
559 + if key == self.config.get_keybind('edit_note'):
560 + note = contents.note
561 + else:
562 + note = contents.old_note if contents.old_note \
563 + else contents.note
564 +
565 + self._gui_clear()
566 + if key == self.config.get_keybind('edit_note'):
567 + content = exec_cmd_on_note(note, self.config, self,
568 + self.logger)
569 + elif key == self.config.get_keybind('view_note_ext'):
570 + content = exec_cmd_on_note(
571 + note,
572 + self.config,
573 + self,
574 + self.logger,
575 + cmd=get_pager(self.config, self.logger))
576 + else: # key == self.config.get_keybind('view_note_json')
577 + content = exec_cmd_on_note(
578 + note,
579 + self.config,
580 + self,
581 + self.logger,
582 + cmd=get_pager(self.config, self.logger),
583 + raw=True
584 + )
585 +
586 + self._gui_reset()
587 +
588 + if not content:
589 + return None
590 +
591 + md5_old = hashlib.md5(note['content'].encode('utf-8')).digest()
592 + md5_new = hashlib.md5(content.encode('utf-8')).digest()
593 +
594 + if md5_old != md5_new:
595 + self.log('Note updated')
596 + self.ndb.set_note_content(note['localkey'], content)
597 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
598 + contents.update_note_title()
599 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
600 + contents.update_note_view()
601 + self.ndb.sync_worker_go()
602 + else:
603 + self.log('Note unchanged')
604 +
605 + elif key == self.config.get_keybind('view_note'):
606 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
607 + return key
608 +
609 + if not contents.body.positions():
610 + return None
611 + self.view_note.update_note_view(
612 + contents.note_list[contents.focus_position]. \
613 + note['localkey'])
614 + self._gui_switch_frame_body(self.view_note)
615 +
616 + elif key == self.config.get_keybind('pipe_note'):
617 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
618 + self.gui_body_get().__class__ != view_note.ViewNote:
619 + return key
620 +
621 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
622 + if not contents.body.positions():
623 + return None
624 + note = contents.note_list[contents.focus_position].note
625 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
626 + note = contents.old_note if contents.old_note else contents.note
627 +
628 + self._gui_footer_input_set(
629 + urwid.AttrMap(
630 + user_input.UserInput(
631 + self.config,
632 + key,
633 + '',
634 + self._gui_pipe_input,
635 + None
636 + ),
637 + 'user_input_bar'
638 + )
639 + )
640 + self._gui_footer_focus_input()
641 + self.master_frame.keypress = \
642 + self._gui_footer_input_get().keypress
643 +
644 + elif key == self.config.get_keybind('note_delete'):
645 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
646 + self.gui_body_get().__class__ != view_note.ViewNote:
647 + return key
648 +
649 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
650 + if not contents.body.positions():
651 + return None
652 + note = contents.note_list[contents.focus_position].note
653 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
654 + note = contents.note
655 +
656 + self._gui_footer_input_set(
657 + urwid.AttrMap(
658 + user_input.UserInput(
659 + self.config,
660 + 'Delete (y/n): ',
661 + '',
662 + self._gui_yes_no_input,
663 + [
664 + self._delete_note_callback,
665 + note['localkey']
666 + ]
667 + ),
668 + 'user_input_bar'
669 + )
670 + )
671 + self._gui_footer_focus_input()
672 + self.master_frame.keypress = \
673 + self._gui_footer_input_get().keypress
674 +
675 + elif key == self.config.get_keybind('note_favorite'):
676 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
677 + self.gui_body_get().__class__ != view_note.ViewNote:
678 + return key
679 +
680 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
681 + if not contents.body.positions():
682 + return None
683 + note = contents.note_list[contents.focus_position].note
684 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
685 + note = contents.note
686 +
687 + favorite = not note['favorite']
688 +
689 + self.ndb.set_note_favorite(note['localkey'], favorite)
690 +
691 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
692 + contents.update_note_title()
693 +
694 + self.ndb.sync_worker_go()
695 +
696 + elif key == self.config.get_keybind('note_category'):
697 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
698 + self.gui_body_get().__class__ != view_note.ViewNote:
699 + return key
700 +
701 + if self.gui_body_get().__class__ == view_titles.ViewTitles:
702 + if not contents.body.positions():
703 + return None
704 + note = contents.note_list[contents.focus_position].note
705 + else: # self.gui_body_get().__class__ == view_note.ViewNote:
706 + note = contents.note
707 +
708 + self._gui_footer_input_set(
709 + urwid.AttrMap(
710 + user_input.UserInput(
711 + self.config,
712 + 'Category: ',
713 + note['category'],
714 + self._gui_category_input,
715 + None
716 + ),
717 + 'user_input_bar'
718 + )
719 + )
720 + self._gui_footer_focus_input()
721 + self.master_frame.keypress = \
722 + self._gui_footer_input_get().keypress
723 +
724 + elif key == self.config.get_keybind('search_gstyle') or \
725 + key == self.config.get_keybind('search_regex') or \
726 + key == self.config.get_keybind('search_prev_gstyle') or \
727 + key == self.config.get_keybind('search_prev_regex'):
728 + if self.gui_body_get().__class__ != view_titles.ViewTitles and \
729 + self.gui_body_get().__class__ != view_note.ViewNote:
730 + return key
731 +
732 + if self.gui_body_get().__class__ == view_note.ViewNote:
733 + if key == self.config.get_keybind('search_prev_gstyle') or \
734 + key == self.config.get_keybind('search_prev_regex'):
735 + self.view_note.search_direction = 'backward'
736 + else:
737 + self.view_note.search_direction = 'forward'
738 +
739 + options = [
740 + 'gstyle' if key == self.config.get_keybind('search_gstyle')
741 + or key == self.config.get_keybind('search_prev_gstyle')
742 + else 'regex',
743 + 'backward' if key ==
744 + self.config.get_keybind('search_prev_gstyle')
745 + or key == self.config.get_keybind('search_prev_regex')
746 + else 'forward'
747 + ]
748 +
749 + caption = '{}{}'.format('(regex) '
750 + if options[0] == 'regex'
751 + else '',
752 + '/' if options[1] == 'forward'
753 + else '?')
754 +
755 + self._gui_footer_input_set(
756 + urwid.AttrMap(
757 + user_input.UserInput(
758 + self.config,
759 + caption,
760 + '',
761 + self._gui_search_input,
762 + options
763 + ),
764 + 'user_input_bar'
765 + )
766 + )
767 + self._gui_footer_focus_input()
768 + self.master_frame.keypress = \
769 + self._gui_footer_input_get().keypress
770 +
771 + elif key == self.config.get_keybind('search_next'):
772 + if self.gui_body_get().__class__ != view_note.ViewNote:
773 + return key
774 +
775 + self.view_note.search_note_view_next()
776 +
777 + elif key == self.config.get_keybind('search_prev'):
778 + if self.gui_body_get().__class__ != view_note.ViewNote:
779 + return key
780 +
781 + self.view_note.search_note_view_prev()
782 +
783 + elif key == self.config.get_keybind('clear_search'):
784 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
785 + return key
786 +
787 + self.view_titles.update_note_list(
788 + None,
789 + sort_mode=self.config.state.current_sort_mode
790 + )
791 + self._gui_body_set(self.view_titles)
792 +
793 + elif key == self.config.get_keybind('sort_date'):
794 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
795 + return key
796 +
797 + self.config.state.current_sort_mode = 'date'
798 + self.view_titles.sort_note_list('date')
799 +
800 + elif key == self.config.get_keybind('sort_alpha'):
801 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
802 + return key
803 +
804 + self.config.state.current_sort_mode = 'alpha'
805 + self.view_titles.sort_note_list('alpha')
806 +
807 + elif key == self.config.get_keybind('sort_categories'):
808 + if self.gui_body_get().__class__ != view_titles.ViewTitles:
809 + return key
810 +
811 + self.config.state.current_sort_mode = 'categories'
812 + self.view_titles.sort_note_list('categories')
813 +
814 + elif key == self.config.get_keybind('copy_note_text'):
815 + if self.gui_body_get().__class__ != view_note.ViewNote:
816 + return key
817 +
818 + self.view_note.copy_note_text()
819 +
820 + else:
821 + return contents.keypress(size, key)
822 +
823 + self._gui_update_status_bar()
824 + return None
825 +
826 + def _gui_init_view(self, loop, show_note):
827 + """Initialize the GUI"""
828 + self.master_frame.keypress = self._gui_frame_keypress
829 + self._gui_body_set(self.view_titles)
830 +
831 + if show_note:
832 + # note that title view set first to prime the view stack
833 + self._gui_switch_frame_body(self.view_note)
834 +
835 + self.thread_sync.start()
836 +
837 + def _gui_clear(self):
838 + """Clear the GUI"""
839 + self.nncli_loop.widget = urwid.Filler(urwid.Text(''))
840 + self.nncli_loop.draw_screen()
841 +
842 + def _gui_reset(self):
843 + """Reset the GUI"""
844 + self.nncli_loop.widget = self.master_frame
845 + self.nncli_loop.draw_screen()
846 +
847 + def _gui_stop(self):
848 + """Stop the GUI"""
849 + # don't exit if there are any notes not yet saved to the disk
850 +
851 + # NOTE: this was originally causing hangs on exit with urllib2
852 + # should not be a problem now since using the requests library
853 + # ref https://github.com/insanum/sncli/issues/18#issuecomment-105517773
854 + if self.ndb.verify_all_saved():
855 + # clear the screen and exit the urwid run loop
856 + self._gui_clear()
857 + raise urwid.ExitMainLoop()
858 + else:
859 + self.log('WARNING: Not all notes saved'
860 + 'to disk (wait for sync worker)')
861 +
862 + def log(self, msg):
863 + """Log as message, displaying to the user as appropriate"""
864 + self.logger.log(msg)
865 +
866 + self.log_lock.acquire()
867 +
868 + self.log_alarms += 1
869 + self.logs.append(msg)
870 +
871 + if len(self.logs) > int(self.config.get_config('max_logs')):
872 + self.log_alarms -= 1
873 + self.logs.pop(0)
874 +
875 + log_pile = []
876 + for log in self.logs:
877 + log_pile.append(urwid.AttrMap(urwid.Text(log), 'log'))
878 +
879 + if self.config.state.verbose:
880 + self._gui_footer_log_set(log_pile)
881 +
882 + self.nncli_loop.set_alarm_in(
883 + int(self.config.get_config('log_timeout')),
884 + self._log_timeout, None)
885 +
886 + self.log_lock.release()
887 +
888 + def _log_timeout(self, loop, arg):
889 + """
890 + Run periodically to check for new log entries to append to
891 + the GUI footer
892 + """
893 + self.log_lock.acquire()
894 +
895 + self.log_alarms -= 1
896 +
897 + if self.log_alarms == 0:
898 + self._gui_footer_log_clear()
899 + self.logs = []
900 + else:
901 + # for some reason having problems with this being empty?
902 + if not self.logs:
903 + self.logs.pop(0)
904 +
905 + log_pile = []
906 +
907 + for log in self.logs:
908 + log_pile.append(urwid.AttrMap(urwid.Text(log), 'log'))
909 +
910 + if self.config.state.verbose:
911 + self._gui_footer_log_set(log_pile)
912 +
913 + self.log_lock.release()

nncli/log.py (created)

1 +# -*- coding: utf-8 -*-
2 +"""log module"""
3 +import logging
4 +from logging.handlers import RotatingFileHandler
5 +
6 +import os
7 +
8 +# pylint: disable=unused-argument, too-few-public-methods
9 +class Logger:
10 + """Handles logging for the application"""
11 + def __init__(self, config):
12 + self.config = config
13 + self.logfile = os.path.join(
14 + config.get_config('db_path'),
15 + 'nncli.log'
16 + )
17 + self.loghandler = RotatingFileHandler(
18 + self.logfile,
19 + maxBytes=100000,
20 + backupCount=1
21 + )
22 + self.loghandler.setLevel(logging.DEBUG)
23 + self.loghandler.setFormatter(
24 + logging.Formatter(
25 + fmt='%(asctime)s [%(levelname)s] %(message)s'
26 + )
27 + )
28 + self.logger = logging.getLogger()
29 + self.logger.setLevel(logging.DEBUG)
30 + self.logger.addHandler(self.loghandler)
31 +
32 + logging.debug('nncli logging initialized')
33 +
34 + def log(self, msg):
35 + """Log as message, displaying to the user as appropriate"""
36 + logging.debug(msg)
37 +
38 + if not self.config.state.do_gui:
39 + if self.config.state.verbose:
40 + print(msg)

nncli/nextcloud_note.py

1 1 # -*- coding: utf-8 -*-
2 -
3 -from requests.exceptions import RequestException, ConnectionError
4 -import time
5 -import datetime
2 +"""nextcloud_note module"""
6 3 import logging
7 -import requests
4 +import time
8 5 import traceback
9 6
10 -try:
11 - import json
12 -except ImportError:
13 - try:
14 - import simplejson as json
15 - except ImportError:
16 - # For Google AppEngine
17 - from django.utils import simplejson as json
7 +import requests
8 +from requests.exceptions import RequestException
18 9
19 -class NextcloudNote(object):
10 +class NextcloudNote:
20 11 """ Class for interacting with the NextCloud Notes web service """
21 12
22 13 def __init__(self, username, password, host):
. . .
49 40 res.raise_for_status()
50 41 note = res.json()
51 42 self.status = 'online'
52 - except ConnectionError as e:
43 + except ConnectionError as ex:
53 44 self.status = 'offline, connection error'
54 - return e, -1
55 - except RequestException as e:
45 + return ex, -1
46 + except RequestException as ex:
56 47 # logging.debug('RESPONSE ERROR: ' + str(e))
57 - return e, -1
58 - except ValueError as e:
59 - return e, -1
48 + return ex, -1
49 + except ValueError as ex:
50 + return ex, -1
60 51
61 52 # # use UTF-8 encoding
62 53 # note["content"] = note["content"].encode('utf-8')
. . .
96 87
97 88 #logging.debug('REQUEST: ' + url + ' - ' + str(note))
98 89 try:
99 - logging.debug('NOTE: ' + str(note))
90 + logging.debug('NOTE: %s', note)
100 91 if url != self.url:
101 - res = requests.put(url, auth=(self.username,
102 - self.password), json=note)
92 + res = requests.put(
93 + url,
94 + auth=(self.username, self.password),
95 + json=note
96 + )
103 97 else:
104 - res = requests.post(url, auth=(self.username,
105 - self.password), json=note)
98 + res = requests.post(
99 + url, auth=(self.username, self.password),
100 + json=note
101 + )
106 102 note = res.json()
107 103 res.raise_for_status()
108 - logging.debug('NOTE (from response): ' + str(res.json()))
104 + logging.debug('NOTE (from response): %s', res.json())
109 105 self.status = 'online'
110 - except ConnectionError as e:
106 + except ConnectionError as ex:
111 107 self.status = 'offline, connection error'
112 - return e, -1
113 - except RequestException as e:
114 - logging.debug('RESPONSE ERROR: ' + str(e))
108 + raise ex
109 + except RequestException as ex:
110 + logging.debug('RESPONSE ERROR: %s', ex)
115 111 logging.debug(traceback.print_exc())
116 112 self.status = 'error updating note, check log'
117 - return e, -1
118 - except ValueError as e:
119 - return e, -1
113 + raise ex
114 + except ValueError as ex:
115 + raise ex
120 116 #logging.debug('RESPONSE OK: ' + str(note))
121 117 return note, 0
122 118
. . .
149 145
150 146 # perform initial HTTP request
151 147 try:
152 - logging.debug('REQUEST: ' + self.url + \
153 - '?exclude=content')
154 - res = requests.get(self.url, auth=(self.username, self.password), params=params)
148 + logging.debug('REQUEST: %s', self.url + '?exclude=content')
149 + res = requests.get(
150 + self.url,
151 + auth=(self.username, self.password),
152 + params=params
153 + )
155 154 res.raise_for_status()
156 155 #logging.debug('RESPONSE OK: ' + str(res))
157 156 note_list = res.json()
158 157 self.status = 'online'
159 - except ConnectionError as e:
158 + except ConnectionError:
160 159 logging.exception('connection error')
161 160 self.status = 'offline, connection error'
162 161 status = -1
163 - except RequestException as e:
162 + except RequestException:
164 163 # if problem with network request/response
165 164 logging.exception('request error')
166 165 status = -1
167 - except ValueError as e:
166 + except ValueError:
168 167 # if invalid json data
169 168 status = -1
170 169 logging.exception('request returned bad JSON data')
. . .
194 193 url = '{}/{}'.format(self.url, str(note['id']))
195 194
196 195 try:
197 - logging.debug('REQUEST DELETE: ' + url)
196 + logging.debug('REQUEST DELETE: %s', url)
198 197 res = requests.delete(url, auth=(self.username, self.password))
199 198 res.raise_for_status()
200 199 self.status = 'online'
201 - except ConnectionError as e:
200 + except ConnectionError as ex:
202 201 self.status = 'offline, connection error'
203 - return e, -1
204 - except RequestException as e:
205 - return e, -1
202 + raise ex
203 + except RequestException as ex:
204 + raise ex
206 205 return {}, 0

nncli/nncli.py

1 1 # -*- coding: utf-8 -*-
2 +"""nncli module"""
3 +import hashlib
4 +import json
5 +import os
6 +import signal
7 +import sys
8 +import time
2 9
3 -import os, sys, getopt, re, signal, time, datetime, shlex, hashlib
4 -import subprocess, threading, logging
5 -import copy, json, urwid, datetime
6 -from . import view_titles, view_note, view_help, view_log, user_input
7 -from . import utils, temp, __version__
10 +from . import utils, __version__
8 11 from .config import Config
9 -from .nextcloud_note import NextcloudNote
12 +from .gui import NncliGui
13 +from .log import Logger
10 14 from .notes_db import NotesDB, ReadError, WriteError
11 -from logging.handlers import RotatingFileHandler
15 +from .utils import exec_cmd_on_note
12 16
17 +# pylint: disable=unused-argument
13 18 class Nncli:
14 -
19 + """Nncli class. Responsible for most of the application logic"""
15 20 def __init__(self, do_server_sync, verbose=False, config_file=None):
16 - self.config = Config(config_file)
17 - self.do_server_sync = do_server_sync
18 - self.verbose = verbose
19 - self.do_gui = False
20 - force_full_sync = False
21 - self.current_sort_mode = self.config.get_config('sort_mode')
22 -
23 - self.tempdir = self.config.get_config('tempdir')
24 - if self.tempdir == '':
25 - self.tempdir = None
21 + self.config = Config(config_file)
22 + self.config.state.do_server_sync = do_server_sync
23 + self.config.state.verbose = verbose
24 + force_full_sync = False
26 25
27 26 if not os.path.exists(self.config.get_config('db_path')):
28 27 os.mkdir(self.config.get_config('db_path'))
29 28 force_full_sync = True
30 29
31 - # configure the logging module
32 - self.logfile = os.path.join(self.config.get_config('db_path'), 'nncli.log')
33 - self.loghandler = RotatingFileHandler(self.logfile, maxBytes=100000, backupCount=1)
34 - self.loghandler.setLevel(logging.DEBUG)
35 - self.loghandler.setFormatter(logging.Formatter(fmt='%(asctime)s [%(levelname)s] %(message)s'))
36 - self.logger = logging.getLogger()
37 - self.logger.setLevel(logging.DEBUG)
38 - self.logger.addHandler(self.loghandler)
39 - self.config.logfile = self.logfile
40 -
41 - logging.debug('nncli logging initialized')
42 -
43 - self.logs = []
30 + self.logger = Logger(self.config)
44 31
45 32 try:
46 - self.ndb = NotesDB(self.config, self.log, self.gui_update_view)
47 - except Exception as e:
48 - self.log(str(e))
33 + self.ndb = NotesDB(
34 + self.config,
35 + self.logger.log
36 + )
37 + except (ReadError, WriteError) as ex:
38 + self.logger.log(str(ex))
49 39 sys.exit(1)
40 +
41 + self.nncli_gui = NncliGui(self.config, self.logger, self.ndb)
42 + self.ndb.set_update_view(self.nncli_gui.gui_update_view)
50 43
51 44 if force_full_sync:
52 45 # The note database doesn't exist so force a full sync. It is
53 46 # important to do this outside of the gui because an account
54 47 # with hundreds of notes will cause a recursion panic under
55 48 # urwid. This simple workaround gets the job done. :-)
56 - self.verbose = True
57 - self.log('nncli database doesn\'t exist, forcing full sync...')
58 - self.sync_notes()
59 - self.verbose = verbose
60 -
61 - def sync_notes(self):
62 - self.ndb.sync_now(self.do_server_sync)
63 -
64 - def get_editor(self):
65 - editor = self.config.get_config('editor')
66 - if not editor:
67 - self.log('No editor configured!')
68 - return None
69 - return editor
70 -
71 - def get_pager(self):
72 - pager = self.config.get_config('pager')
73 - if not pager:
74 - self.log('No pager configured!')
75 - return None
76 - return pager
77 -
78 - def exec_cmd_on_note(self, note, cmd=None, raw=False):
79 -
80 - if not cmd:
81 - cmd = self.get_editor()
82 - if not cmd:
83 - return None
84 -
85 - tf = temp.tempfile_create(note if note else None, raw=raw, tempdir=self.tempdir)
86 - fname = temp.tempfile_name(tf)
87 -
88 - focus_position = 0
89 - try:
90 - focus_position = self.gui_body_get().focus_position
91 - except IndexError:
92 - # focus position will fail if no notes available (listbox empty)
93 - # TODO: find a neater way to check than try/except
94 - pass
95 - except AttributeError:
96 - # we're running in CLI mode
97 - pass
98 -
99 - subs = {
100 - 'fname': fname,
101 - 'line': focus_position + 1,
102 - }
103 - cmd_list = [c.format(**subs) for c in shlex.split(cmd)]
104 -
105 - # if the filename wasn't able to be subbed, append it
106 - # this makes it fully backwards compatible with previous configs
107 - if '{fname}' not in cmd:
108 - cmd_list.append(fname)
109 -
110 - self.log("EXECUTING: {}".format(cmd_list))
111 -
112 - try:
113 - subprocess.check_call(cmd_list)
114 - except Exception as e:
115 - self.log('Command error: ' + str(e))
116 - temp.tempfile_delete(tf)
117 - return None
118 -
119 - content = None
120 - if not raw:
121 - content = temp.tempfile_content(tf)
122 - if not content or content == '\n':
123 - content = None
124 -
125 - temp.tempfile_delete(tf)
126 -
127 - if self.do_gui:
128 - self.nncli_loop.screen.clear()
129 - self.nncli_loop.draw_screen()
130 -
131 - return content
132 -
133 - def gui_header_clear(self):
134 - self.master_frame.contents['header'] = ( None, None )
135 - self.nncli_loop.draw_screen()
136 -
137 - def gui_header_set(self, w):
138 - self.master_frame.contents['header'] = ( w, None )
139 - self.nncli_loop.draw_screen()
140 -
141 - def gui_footer_log_clear(self):
142 - ui = self.gui_footer_input_get()
143 - self.master_frame.contents['footer'] = \
144 - (urwid.Pile([ urwid.Pile([]), urwid.Pile([ui]) ]), None)
145 - self.nncli_loop.draw_screen()
146 -
147 - def gui_footer_log_set(self, pl):
148 - ui = self.gui_footer_input_get()
149 - self.master_frame.contents['footer'] = \
150 - (urwid.Pile([ urwid.Pile(pl), urwid.Pile([ui]) ]), None)
151 - self.nncli_loop.draw_screen()
152 -
153 - def gui_footer_log_get(self):
154 - return self.master_frame.contents['footer'][0].contents[0][0]
155 -
156 - def gui_footer_input_clear(self):
157 - pl = self.gui_footer_log_get()
158 - self.master_frame.contents['footer'] = \
159 - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([]) ]), None)
160 - self.nncli_loop.draw_screen()
161 -
162 - def gui_footer_input_set(self, ui):
163 - pl = self.gui_footer_log_get()
164 - self.master_frame.contents['footer'] = \
165 - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([ui]) ]), None)
166 - self.nncli_loop.draw_screen()
167 -
168 - def gui_footer_input_get(self):
169 - return self.master_frame.contents['footer'][0].contents[1][0]
170 -
171 - def gui_footer_focus_input(self):
172 - self.master_frame.focus_position = 'footer'
173 - self.master_frame.contents['footer'][0].focus_position = 1
174 -
175 - def gui_body_set(self, w):
176 - self.master_frame.contents['body'] = ( w, None )
177 - self.gui_update_status_bar()
178 - self.nncli_loop.draw_screen()
179 -
180 - def gui_body_get(self):
181 - return self.master_frame.contents['body'][0]
182 -
183 - def gui_body_focus(self):
184 - self.master_frame.focus_position = 'body'
185 -
186 - def log_timeout(self, loop, arg):
187 - self.log_lock.acquire()
188 -
189 - self.log_alarms -= 1
190 -
191 - if self.log_alarms == 0:
192 - self.gui_footer_log_clear()
193 - self.logs = []
194 - else:
195 - # for some reason having problems with this being empty?
196 - if len(self.logs) > 0:
197 - self.logs.pop(0)
198 -
199 - log_pile = []
200 -
201 - for l in self.logs:
202 - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log'))
203 -
204 - if self.verbose:
205 - self.gui_footer_log_set(log_pile)
206 -
207 - self.log_lock.release()
208 -
209 - def log(self, msg):
210 - logging.debug(msg)
211 -
212 - if not self.do_gui:
213 - if self.verbose:
214 - print(msg)
215 - return
216 -
217 - self.log_lock.acquire()
218 -
219 - self.log_alarms += 1
220 - self.logs.append(msg)
221 -
222 - if len(self.logs) > int(self.config.get_config('max_logs')):
223 - self.log_alarms -= 1
224 - self.logs.pop(0)
225 -
226 - log_pile = []
227 - for l in self.logs:
228 - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log'))
229 -
230 - if self.verbose:
231 - self.gui_footer_log_set(log_pile)
232 -
233 - self.nncli_loop.set_alarm_in(
234 - int(self.config.get_config('log_timeout')),
235 - self.log_timeout, None)
236 -
237 - self.log_lock.release()
238 -
239 - def gui_update_view(self):
240 - if not self.do_gui:
241 - return
242 -
243 - try:
244 - cur_key = self.view_titles.note_list[self.view_titles.focus_position].note['localkey']
245 - except IndexError as e:
246 - cur_key = None
247 - pass
248 -
249 - self.view_titles.update_note_list(self.view_titles.search_string, sort_mode=self.current_sort_mode)
250 - self.view_titles.focus_note(cur_key)
251 -
252 - if self.gui_body_get().__class__ == view_note.ViewNote:
253 - self.view_note.update_note_view()
254 -
255 - self.gui_update_status_bar()
256 -
257 - def gui_update_status_bar(self):
258 - if self.status_bar != 'yes':
259 - self.gui_header_clear()
260 - else:
261 - self.gui_header_set(self.gui_body_get().get_status_bar())
262 -
263 - def gui_switch_frame_body(self, new_view, save_current_view=True):
264 - if new_view == None:
265 - if len(self.last_view) == 0:
266 - # XXX verify all notes saved...
267 - self.gui_stop()
268 - else:
269 - self.gui_body_set(self.last_view.pop())
270 - else:
271 - if self.gui_body_get().__class__ != new_view.__class__:
272 - if save_current_view:
273 - self.last_view.append(self.gui_body_get())
274 - self.gui_body_set(new_view)
275 -
276 - def delete_note_callback(self, key, delete):
277 - if not delete:
278 - return
279 - note = self.ndb.get_note(key)
280 - self.ndb.set_note_deleted(key, True)
281 -
282 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
283 - self.view_titles.update_note_title()
284 -
285 - self.gui_update_status_bar()
286 - self.ndb.sync_worker_go()
287 -
288 - def gui_yes_no_input(self, args, yes_no):
289 - self.gui_footer_input_clear()
290 - self.gui_body_focus()
291 - self.master_frame.keypress = self.gui_frame_keypress
292 - args[0](args[1],
293 - True if yes_no in [ 'YES', 'Yes', 'yes', 'Y', 'y' ]
294 - else False)
295 -
296 - def gui_search_input(self, args, search_string):
297 - self.gui_footer_input_clear()
298 - self.gui_body_focus()
299 - self.master_frame.keypress = self.gui_frame_keypress
300 - if search_string:
301 - if (self.gui_body_get() == self.view_note):
302 - self.search_direction = args[1]
303 - self.view_note.search_note_view_next(search_string=search_string, search_mode=args[0])
304 - else:
305 - self.view_titles.update_note_list(search_string, args[0], sort_mode=self.current_sort_mode)
306 - self.gui_body_set(self.view_titles)
307 -
308 - def gui_category_input(self, args, category):
309 - self.gui_footer_input_clear()
310 - self.gui_body_focus()
311 - self.master_frame.keypress = self.gui_frame_keypress
312 - if category != None:
313 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
314 - note = self.view_titles.note_list[self.view_titles.focus_position].note
315 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
316 - note = self.view_note.note
317 -
318 - self.ndb.set_note_category(note['localkey'], category)
319 -
320 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
321 - self.view_titles.update_note_title()
322 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
323 - self.view_note.update_note_view()
324 -
325 - self.gui_update_status_bar()
326 - self.ndb.sync_worker_go()
327 -
328 - def gui_pipe_input(self, args, cmd):
329 - self.gui_footer_input_clear()
330 - self.gui_body_focus()
331 - self.master_frame.keypress = self.gui_frame_keypress
332 - if cmd != None:
333 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
334 - note = self.view_titles.note_list[self.view_titles.focus_position].note
335 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
336 - note = self.view_note.old_note if self.view_note.old_note \
337 - else self.view_note.note
338 - args = shlex.split(cmd)
339 - try:
340 - self.gui_clear()
341 - pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True)
342 - pipe.communicate(note['content'].encode('utf-8'))
343 - pipe.stdin.close()
344 - pipe.wait()
345 - except OSError as e:
346 - self.log('Pipe error: ' + str(e))
347 - finally:
348 - self.gui_reset()
349 -
350 - def gui_frame_keypress(self, size, key):
351 - # convert space character into name
352 - if key == ' ':
353 - key = 'space'
354 -
355 - lb = self.gui_body_get()
356 -
357 - if key == self.config.get_keybind('quit'):
358 - self.gui_switch_frame_body(None)
359 -
360 - elif key == self.config.get_keybind('help'):
361 - self.gui_switch_frame_body(self.view_help)
362 -
363 - elif key == self.config.get_keybind('sync'):
364 - self.ndb.last_sync = 0
365 - self.ndb.sync_worker_go()
366 -
367 - elif key == self.config.get_keybind('view_log'):
368 - self.view_log.update_log()
369 - self.gui_switch_frame_body(self.view_log)
370 -
371 - elif key == self.config.get_keybind('down'):
372 - if len(lb.body.positions()) <= 0:
373 - return None
374 - last = len(lb.body.positions())
375 - if lb.focus_position == (last - 1):
376 - return None
377 - lb.focus_position += 1
378 - lb.render(size)
379 -
380 - elif key == self.config.get_keybind('up'):
381 - if len(lb.body.positions()) <= 0:
382 - return None
383 - if lb.focus_position == 0:
384 - return None
385 - lb.focus_position -= 1
386 - lb.render(size)
387 -
388 - elif key == self.config.get_keybind('page_down'):
389 - if len(lb.body.positions()) <= 0:
390 - return None
391 - last = len(lb.body.positions())
392 - next_focus = lb.focus_position + size[1]
393 - if next_focus >= last:
394 - next_focus = last - 1
395 - lb.change_focus(size, next_focus,
396 - offset_inset=0,
397 - coming_from='above')
398 -
399 - elif key == self.config.get_keybind('page_up'):
400 - if len(lb.body.positions()) <= 0:
401 - return None
402 - if 'bottom' in lb.ends_visible(size):
403 - last = len(lb.body.positions())
404 - next_focus = last - size[1] - size[1]
405 - else:
406 - next_focus = lb.focus_position - size[1]
407 - if next_focus < 0:
408 - next_focus = 0
409 - lb.change_focus(size, next_focus,
410 - offset_inset=0,
411 - coming_from='below')
412 -
413 - elif key == self.config.get_keybind('half_page_down'):
414 - if len(lb.body.positions()) <= 0:
415 - return None
416 - last = len(lb.body.positions())
417 - next_focus = lb.focus_position + (size[1] // 2)
418 - if next_focus >= last:
419 - next_focus = last - 1
420 - lb.change_focus(size, next_focus,
421 - offset_inset=0,
422 - coming_from='above')
423 -
424 - elif key == self.config.get_keybind('half_page_up'):
425 - if len(lb.body.positions()) <= 0:
426 - return None
427 - if 'bottom' in lb.ends_visible(size):
428 - last = len(lb.body.positions())
429 - next_focus = last - size[1] - (size[1] // 2)
430 - else:
431 - next_focus = lb.focus_position - (size[1] // 2)
432 - if next_focus < 0:
433 - next_focus = 0
434 - lb.change_focus(size, next_focus,
435 - offset_inset=0,
436 - coming_from='below')
437 -
438 - elif key == self.config.get_keybind('bottom'):
439 - if len(lb.body.positions()) <= 0:
440 - return None
441 - lb.change_focus(size, (len(lb.body.positions()) - 1),
442 - offset_inset=0,
443 - coming_from='above')
444 -
445 - elif key == self.config.get_keybind('top'):
446 - if len(lb.body.positions()) <= 0:
447 - return None
448 - lb.change_focus(size, 0,
449 - offset_inset=0,
450 - coming_from='below')
451 -
452 - elif key == self.config.get_keybind('view_next_note'):
453 - if self.gui_body_get().__class__ != view_note.ViewNote:
454 - return key
455 -
456 - if len(self.view_titles.body.positions()) <= 0:
457 - return None
458 - last = len(self.view_titles.body.positions())
459 - if self.view_titles.focus_position == (last - 1):
460 - return None
461 - self.view_titles.focus_position += 1
462 - lb.update_note_view(
463 - self.view_titles.note_list[self.view_titles.focus_position].note['localkey'])
464 - self.gui_switch_frame_body(self.view_note)
465 -
466 - elif key == self.config.get_keybind('view_prev_note'):
467 - if self.gui_body_get().__class__ != view_note.ViewNote:
468 - return key
469 -
470 - if len(self.view_titles.body.positions()) <= 0:
471 - return None
472 - if self.view_titles.focus_position == 0:
473 - return None
474 - self.view_titles.focus_position -= 1
475 - lb.update_note_view(
476 - self.view_titles.note_list[self.view_titles.focus_position].note['localkey'])
477 - self.gui_switch_frame_body(self.view_note)
478 -
479 - elif key == self.config.get_keybind('status'):
480 - if self.status_bar == 'yes':
481 - self.status_bar = 'no'
482 - else:
483 - self.status_bar = self.config.get_config('status_bar')
484 -
485 - elif key == self.config.get_keybind('create_note'):
486 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
487 - return key
488 -
489 - self.gui_clear()
490 - content = self.exec_cmd_on_note(None)
491 - self.gui_reset()
492 -
493 - if content:
494 - self.log('New note created')
495 - self.ndb.create_note(content)
496 - self.gui_update_view()
497 - self.ndb.sync_worker_go()
498 -
499 - elif key == self.config.get_keybind('edit_note') or \
500 - key == self.config.get_keybind('view_note_ext') or \
501 - key == self.config.get_keybind('view_note_json'):
502 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
503 - self.gui_body_get().__class__ != view_note.ViewNote:
504 - return key
505 -
506 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
507 - if len(lb.body.positions()) <= 0:
508 - return None
509 - note = lb.note_list[lb.focus_position].note
510 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
511 - if key == self.config.get_keybind('edit_note'):
512 - note = lb.note
513 - else:
514 - note = lb.old_note if lb.old_note else lb.note
515 -
516 - self.gui_clear()
517 - if key == self.config.get_keybind('edit_note'):
518 - content = self.exec_cmd_on_note(note)
519 - elif key == self.config.get_keybind('view_note_ext'):
520 - content = self.exec_cmd_on_note(note, cmd=self.get_pager())
521 - else: # key == self.config.get_keybind('view_note_json')
522 - content = self.exec_cmd_on_note(note, cmd=self.get_pager(), raw=True)
523 -
524 - self.gui_reset()
525 -
526 - if not content:
527 - return None
528 -
529 - md5_old = hashlib.md5(note['content'].encode('utf-8')).digest()
530 - md5_new = hashlib.md5(content.encode('utf-8')).digest()
531 -
532 - if md5_old != md5_new:
533 - self.log('Note updated')
534 - self.ndb.set_note_content(note['localkey'], content)
535 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
536 - lb.update_note_title()
537 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
538 - lb.update_note_view()
539 - self.ndb.sync_worker_go()
540 - else:
541 - self.log('Note unchanged')
542 -
543 - elif key == self.config.get_keybind('view_note'):
544 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
545 - return key
546 -
547 - if len(lb.body.positions()) <= 0:
548 - return None
549 - self.view_note.update_note_view(
550 - lb.note_list[lb.focus_position].note['localkey'])
551 - self.gui_switch_frame_body(self.view_note)
552 -
553 - elif key == self.config.get_keybind('pipe_note'):
554 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
555 - self.gui_body_get().__class__ != view_note.ViewNote:
556 - return key
557 -
558 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
559 - if len(lb.body.positions()) <= 0:
560 - return None
561 - note = lb.note_list[lb.focus_position].note
562 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
563 - note = lb.old_note if lb.old_note else lb.note
564 -
565 - self.gui_footer_input_set(
566 - urwid.AttrMap(
567 - user_input.UserInput(
568 - self.config,
569 - key,
570 - '',
571 - self.gui_pipe_input,
572 - None),
573 - 'user_input_bar'))
574 - self.gui_footer_focus_input()
575 - self.master_frame.keypress = self.gui_footer_input_get().keypress
576 -
577 - elif key == self.config.get_keybind('note_delete'):
578 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
579 - self.gui_body_get().__class__ != view_note.ViewNote:
580 - return key
581 -
582 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
583 - if len(lb.body.positions()) <= 0:
584 - return None
585 - note = lb.note_list[lb.focus_position].note
586 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
587 - note = lb.note
588 -
589 - self.gui_footer_input_set(
590 - urwid.AttrMap(
591 - user_input.UserInput(
592 - self.config,
593 - 'Delete (y/n): ',
594 - '',
595 - self.gui_yes_no_input,
596 - [ self.delete_note_callback, note['localkey'] ]),
597 - 'user_input_bar'))
598 - self.gui_footer_focus_input()
599 - self.master_frame.keypress = self.gui_footer_input_get().keypress
600 -
601 - elif key == self.config.get_keybind('note_favorite'):
602 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
603 - self.gui_body_get().__class__ != view_note.ViewNote:
604 - return key
605 -
606 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
607 - if len(lb.body.positions()) <= 0:
608 - return None
609 - note = lb.note_list[lb.focus_position].note
610 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
611 - note = lb.note
612 -
613 - favorite = not note['favorite']
614 -
615 - self.ndb.set_note_favorite(note['localkey'], favorite)
616 -
617 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
618 - lb.update_note_title()
619 -
620 - self.ndb.sync_worker_go()
621 -
622 - elif key == self.config.get_keybind('note_category'):
623 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
624 - self.gui_body_get().__class__ != view_note.ViewNote:
625 - return key
626 -
627 - if self.gui_body_get().__class__ == view_titles.ViewTitles:
628 - if len(lb.body.positions()) <= 0:
629 - return None
630 - note = lb.note_list[lb.focus_position].note
631 - else: # self.gui_body_get().__class__ == view_note.ViewNote:
632 - note = lb.note
633 -
634 - self.gui_footer_input_set(
635 - urwid.AttrMap(
636 - user_input.UserInput(
637 - self.config,
638 - 'Category: ',
639 - note['category'],
640 - self.gui_category_input,
641 - None),
642 - 'user_input_bar'))
643 - self.gui_footer_focus_input()
644 - self.master_frame.keypress = self.gui_footer_input_get().keypress
645 -
646 - elif key == self.config.get_keybind('search_gstyle') or \
647 - key == self.config.get_keybind('search_regex') or \
648 - key == self.config.get_keybind('search_prev_gstyle') or \
649 - key == self.config.get_keybind('search_prev_regex'):
650 - if self.gui_body_get().__class__ != view_titles.ViewTitles and \
651 - self.gui_body_get().__class__ != view_note.ViewNote:
652 - return key
653 -
654 - if self.gui_body_get().__class__ == view_note.ViewNote:
655 - if key == self.config.get_keybind('search_prev_gstyle') or \
656 - key == self.config.get_keybind('search_prev_regex'):
657 - self.view_note.search_direction = 'backward'
658 - else:
659 - self.view_note.search_direction = 'forward'
660 -
661 - options = [
662 - 'gstyle' if key == self.config.get_keybind('search_gstyle')
663 - or key == self.config.get_keybind('search_prev_gstyle')
664 - else 'regex',
665 - 'backward' if key == self.config.get_keybind('search_prev_gstyle')
666 - or key == self.config.get_keybind('search_prev_regex')
667 - else 'forward'
668 - ]
669 -
670 - caption = '{}{}'.format('(regex) ' if options[0] == 'regex' else '', '/' if options[1] == 'forward' else '?')
671 -
672 - self.gui_footer_input_set(
673 - urwid.AttrMap(
674 - user_input.UserInput(
675 - self.config,
676 - caption,
677 - '',
678 - self.gui_search_input,
679 - options),
680 - 'user_input_bar'))
681 - self.gui_footer_focus_input()
682 - self.master_frame.keypress = self.gui_footer_input_get().keypress
683 -
684 - elif key == self.config.get_keybind('search_next'):
685 - if self.gui_body_get().__class__ != view_note.ViewNote:
686 - return key
687 -
688 - self.view_note.search_note_view_next()
689 -
690 - elif key == self.config.get_keybind('search_prev'):
691 - if self.gui_body_get().__class__ != view_note.ViewNote:
692 - return key
693 -
694 - self.view_note.search_note_view_prev()
695 -
696 - elif key == self.config.get_keybind('clear_search'):
697 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
698 - return key
699 -
700 - self.view_titles.update_note_list(None, sort_mode=self.current_sort_mode)
701 - self.gui_body_set(self.view_titles)
702 -
703 - elif key == self.config.get_keybind('sort_date'):
704 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
705 - return key
706 -
707 - self.current_sort_mode = 'date'
708 - self.view_titles.sort_note_list('date')
709 -
710 - elif key == self.config.get_keybind('sort_alpha'):
711 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
712 - return key
713 -
714 - self.current_sort_mode = 'alpha'
715 - self.view_titles.sort_note_list('alpha')
716 -
717 - elif key == self.config.get_keybind('sort_categories'):
718 - if self.gui_body_get().__class__ != view_titles.ViewTitles:
719 - return key
720 -
721 - self.current_sort_mode = 'categories'
722 - self.view_titles.sort_note_list('categories')
723 -
724 - elif key == self.config.get_keybind('copy_note_text'):
725 - if self.gui_body_get().__class__ != view_note.ViewNote:
726 - return key
727 -
728 - self.view_note.copy_note_text()
729 -
730 - else:
731 - return lb.keypress(size, key)
732 -
733 - self.gui_update_status_bar()
734 - return None
735 -
736 - def gui_init_view(self, loop, view_note):
737 - self.master_frame.keypress = self.gui_frame_keypress
738 - self.gui_body_set(self.view_titles)
739 -
740 - if view_note:
741 - # note that title view set first to prime the view stack
742 - self.gui_switch_frame_body(self.view_note)
743 -
744 - self.thread_sync.start()
745 -
746 - def gui_clear(self):
747 - self.nncli_loop.widget = urwid.Filler(urwid.Text(''))
748 - self.nncli_loop.draw_screen()
749 -