diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .pylintrc | 3 | ||||
-rw-r--r-- | .travis.yml | 6 | ||||
-rw-r--r-- | .vulture_whitelist.py | 31 | ||||
-rw-r--r-- | Makefile | 6 | ||||
-rw-r--r-- | Pipfile.lock | 151 | ||||
-rw-r--r-- | nncli/__init__.py | 2 | ||||
-rw-r--r-- | nncli/__main__.py | 1 | ||||
-rw-r--r-- | nncli/clipboard.py | 28 | ||||
-rw-r--r-- | nncli/config.py | 802 | ||||
-rw-r--r-- | nncli/gui.py | 913 | ||||
-rw-r--r-- | nncli/log.py | 40 | ||||
-rw-r--r-- | nncli/nextcloud_note.py | 87 | ||||
-rw-r--r-- | nncli/nncli.py | 1031 | ||||
-rw-r--r-- | nncli/notes_db.py | 525 | ||||
-rw-r--r-- | nncli/temp.py | 48 | ||||
-rw-r--r-- | nncli/user_input.py | 7 | ||||
-rw-r--r-- | nncli/utils.py | 131 | ||||
-rw-r--r-- | nncli/view_help.py | 172 | ||||
-rw-r--r-- | nncli/view_log.py | 30 | ||||
-rw-r--r-- | nncli/view_note.py | 93 | ||||
-rw-r--r-- | nncli/view_titles.py | 175 | ||||
-rw-r--r-- | pyproject.toml | 1 | ||||
-rw-r--r-- | tests/test_config.py | 105 | ||||
-rw-r--r-- | tests/test_gui.py | 129 | ||||
-rw-r--r-- | tests/test_nncli.py | 457 | ||||
-rw-r--r-- | tox.ini | 7 |
27 files changed, 2978 insertions, 2004 deletions
@@ -6,3 +6,4 @@ MANIFEST .pytest_cache/ docs/build/ .tox +htmlcov/ @@ -140,7 +140,8 @@ disable=unsubscriptable-object, xreadlines-attribute, deprecated-sys-function, exception-escape, - comprehension-escape + comprehension-escape, + duplicate-code # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/.travis.yml b/.travis.yml index d2ae20f..d2a0aa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,9 +35,9 @@ jobs: script: make test <<: *xenial-mixin python: 3.7 - # - stage: lint - # script: make lint - # python: 3.6 + - stage: lint + script: make lint + python: 3.6 - stage: coverage script: make coverage after_success: coveralls diff --git a/.vulture_whitelist.py b/.vulture_whitelist.py index a68dc1b..03544ca 100644 --- a/.vulture_whitelist.py +++ b/.vulture_whitelist.py @@ -1,18 +1,13 @@ -loop # unused variable (nncli/nncli.py:185) -loop # unused variable (nncli/nncli.py:735) -_.widget # unused attribute (nncli/nncli.py:746) -_.widget # unused attribute (nncli/nncli.py:750) -all_notes_cnt # unused variable (nncli/nncli.py:885) -match_regex # unused variable (nncli/nncli.py:885) -all_notes_cnt # unused variable (nncli/nncli.py:922) -match_regex # unused variable (nncli/nncli.py:922) -all_notes_cnt # unused variable (nncli/nncli.py:976) -match_regex # unused variable (nncli/nncli.py:976) -frame # unused variable (nncli/nncli.py:1057) -signum # unused variable (nncli/nncli.py:1057) -note_index # unused variable (nncli/notes_db.py:410) -note_index # unused variable (nncli/notes_db.py:510) -_.all_notes_cnt # unused attribute (nncli/view_titles.py:13) -_.match_regex # unused attribute (nncli/view_titles.py:13) -_.all_notes_cnt # unused attribute (nncli/view_titles.py:20) -_.match_regex # unused attribute (nncli/view_titles.py:20) +convert # unused function (nncli/cli.py:12) +param # unused variable (nncli/cli.py:12) +loop # unused variable (nncli/gui.py:826) +_.widget # unused attribute (nncli/gui.py:839) +_.widget # unused attribute (nncli/gui.py:844) +arg # unused variable (nncli/gui.py:888) +loop # unused variable (nncli/gui.py:888) +frame # unused variable (nncli/nncli.py:254) +signum # unused variable (nncli/nncli.py:254) +_.all_notes_cnt # unused attribute (nncli/view_titles.py:21) +_.match_regex # unused attribute (nncli/view_titles.py:21) +_.all_notes_cnt # unused attribute (nncli/view_titles.py:33) +_.match_regex # unused attribute (nncli/view_titles.py:33) @@ -27,7 +27,7 @@ export PRINT_HELP_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" PIPENV := pipenv PIPRUN := $(PIPENV) run -PIPINST := $(PIPENV) --bare install --dev +PIPINST := $(PIPENV) --bare install --dev --skip-lock help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) @@ -57,7 +57,7 @@ clean-test: ## remove test and coverage artifacts lint: ## check style with pylint $(PIPRUN) pylint nncli tests --disable=parse-error - $(PIPRUN) vulture nncli .vulture_whitelist + $(PIPRUN) vulture nncli .vulture_whitelist.py test: ## run tests quickly with the default Python $(PIPRUN) python -m pytest @@ -76,7 +76,7 @@ coverage-html: coverage ## generate an HTML report and open in browser $(BROWSER) htmlcov/index.html release: dist ## package and upload a release - $(PIPRUN) flit publish + twine upload -s dist/* dist: ## builds source and wheel package $(PIPRUN) flit build diff --git a/Pipfile.lock b/Pipfile.lock index 7b64a27..8f2008b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -24,10 +24,10 @@ }, "certifi": { "hashes": [ - "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", - "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" ], - "version": "==2018.8.24" + "version": "==2018.10.15" }, "chardet": { "hashes": [ @@ -38,11 +38,11 @@ }, "click": { "hashes": [ - "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", - "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], "index": "pypi", - "version": "==6.7" + "version": "==7.0" }, "idna": { "hashes": [ @@ -53,19 +53,18 @@ }, "requests": { "hashes": [ - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" ], "index": "pypi", - "version": "==2.19.1" + "version": "==2.20.0" }, "urllib3": { "hashes": [ - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae", + "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59" ], - "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'", - "version": "==1.23" + "version": "==1.24" }, "urwid": { "hashes": [ @@ -78,10 +77,10 @@ "develop": { "alabaster": { "hashes": [ - "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", - "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7" + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" ], - "version": "==0.7.11" + "version": "==0.7.12" }, "astroid": { "hashes": [ @@ -95,7 +94,6 @@ "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" ], - "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.*'", "version": "==1.2.1" }, "attrs": { @@ -114,10 +112,10 @@ }, "certifi": { "hashes": [ - "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", - "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" ], - "version": "==2018.8.24" + "version": "==2018.10.15" }, "chardet": { "hashes": [ @@ -130,6 +128,7 @@ "hashes": [ "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:0bf8cbbd71adfff0ef1f3a1531e6402d13b7b01ac50a79c97ca15f030dba6306", "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", @@ -158,9 +157,9 @@ "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f05a636b4564104120111800021a92e43397bc12a5c72fed7036be8556e0029e", "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" ], - "markers": "python_version < '4' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", "version": "==4.5.1" }, "docutils": { @@ -171,13 +170,20 @@ ], "version": "==0.14" }, + "filelock": { + "hashes": [ + "sha256:86fe6af56ae08ebc9c66d54ba3398c35b98916d0862d782b276a65816ff39392", + "sha256:97694f181bdf58f213cca0a7cb556dc7bf90e2f8eb9aa3151260adac56701afb" + ], + "version": "==3.0.9" + }, "flit": { "hashes": [ - "sha256:178e6865185b1802aa3b1944f4957d2c83fc56294dc8047d2c4722131f696e61", - "sha256:da823d4acae9bda42dcc0c7ab1d9be475a8a47aae5fd6dde63841d9f430ccb2f" + "sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1", + "sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f" ], "index": "pypi", - "version": "==1.1" + "version": "==1.2.1" }, "idna": { "hashes": [ @@ -191,7 +197,6 @@ "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" ], - "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.*'", "version": "==1.1.0" }, "isort": { @@ -200,7 +205,6 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], - "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.*'", "version": "==4.3.4" }, "jinja2": { @@ -275,10 +279,10 @@ }, "packaging": { "hashes": [ - "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", - "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" + "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", + "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" ], - "version": "==17.1" + "version": "==18.0" }, "pathlib2": { "hashes": [ @@ -291,18 +295,17 @@ }, "pbr": { "hashes": [ - "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45", - "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa" + "sha256:8fc938b1123902f5610b06756a31b1e6febf0d105ae393695b0c9d4244ed2910", + "sha256:f20ec0abbf132471b68963bb34d9c78e603a5cf9e24473f14358e66551d47475" ], - "version": "==4.2.0" + "version": "==5.1.0" }, "pluggy": { "hashes": [ - "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", - "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" ], - "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'", - "version": "==0.7.1" + "version": "==0.8.0" }, "pudb": { "hashes": [ @@ -313,11 +316,10 @@ }, "py": { "hashes": [ - "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", - "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6" + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" ], - "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'", - "version": "==1.6.0" + "version": "==1.7.0" }, "pygments": { "hashes": [ @@ -336,18 +338,18 @@ }, "pyparsing": { "hashes": [ - "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", - "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" + "sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a", + "sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401" ], - "version": "==2.2.0" + "version": "==2.2.2" }, "pytest": { "hashes": [ - "sha256:2d7c49e931316cc7d1638a3e5f54f5d7b4e5225972b3c9838f3584788d27f349", - "sha256:ad0c7db7b5d4081631e0155f5c61b80ad76ce148551aaafe3a718d65a7508b18" + "sha256:a9e5e8d7ab9d5b0747f37740276eb362e6a76275d76cebbb52c6049d93b475db", + "sha256:bf47e8ed20d03764f963f0070ff1c8fda6e2671fc5dd562a4d3b7148ad60f5ca" ], "index": "pypi", - "version": "==3.7.4" + "version": "==3.9.3" }, "pytest-cov": { "hashes": [ @@ -367,31 +369,24 @@ }, "pytoml": { "hashes": [ - "sha256:dae3c4e31d09eb06a6076d671f2281ee5d2c43cbeae16599c3af20881bb818ac" + "sha256:ca2d0cb127c938b8b76a9a0d0f855cf930c1d50cc3a0af6d3595b566519a1013" ], - "version": "==0.1.18" + "version": "==0.1.20" }, "pytz": { "hashes": [ - "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", - "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", + "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" ], - "version": "==2018.5" + "version": "==2018.7" }, "requests": { "hashes": [ - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" ], "index": "pypi", - "version": "==2.19.1" - }, - "requests-download": { - "hashes": [ - "sha256:92d895a6ca51ea51aa42bab864bddaee31b5601c7e7e1ade4c27b0eb6695d846", - "sha256:994d9d332befae6616f562769bab163f08d6404dc7e28fb7bfed4a0a43a754ad" - ], - "version": "==0.1.2" + "version": "==2.20.0" }, "scandir": { "hashes": [ @@ -427,35 +422,40 @@ }, "sphinx": { "hashes": [ - "sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4", - "sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86" + "sha256:652eb8c566f18823a022bb4b6dbc868d366df332a11a0226b5bc3a798a479f17", + "sha256:d222626d8356de702431e813a05c68a35967e3d66c6cd1c2c89539bb179a7464" ], "index": "pypi", - "version": "==1.7.9" + "version": "==1.8.1" }, "sphinxcontrib-websupport": { "hashes": [ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" ], - "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.*'", "version": "==1.1.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, "tox": { "hashes": [ - "sha256:37cf240781b662fb790710c6998527e65ca6851eace84d1595ee71f7af4e85f7", - "sha256:eb61aa5bcce65325538686f09848f04ef679b5cd9b83cc491272099b28739600" + "sha256:513e32fdf2f9e2d583c2f248f47ba9886428c949f068ac54a0469cac55df5862", + "sha256:75fa30e8329b41b664585f5fb837e23ce1d7e6fa1f7811f2be571c990f9d911b" ], "index": "pypi", - "version": "==3.2.1" + "version": "==3.5.3" }, "urllib3": { "hashes": [ - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae", + "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59" ], - "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'", - "version": "==1.23" + "version": "==1.24" }, "urwid": { "hashes": [ @@ -469,16 +469,15 @@ "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==16.0.0" }, "vulture": { "hashes": [ - "sha256:79c89ef5e3f2365467bcf491f425f777ae8fd157584dcc550d4591920b00fe3f", - "sha256:e794345a19c76f93f48f4519653038df90ad468ddea7912e14b07a07f6412e32" + "sha256:4b5a8980c338e9c068d43e7164555a1e4c9c7d84961ce2bc6f3ed975f6e5bc9d", + "sha256:524b6b9642d0bbe74ea21478bf260937d1ba9b3b86676ca0b17cd10b4b51ba01" ], "index": "pypi", - "version": "==0.29" + "version": "==1.0" }, "wrapt": { "hashes": [ diff --git a/nncli/__init__.py b/nncli/__init__.py index 1e36700..fd9cba4 100644 --- a/nncli/__init__.py +++ b/nncli/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """NextCloud Notes Command Line Interface""" -__version__ = '0.3.0' +__version__ = '0.3.1' diff --git a/nncli/__main__.py b/nncli/__main__.py index f3ce514..bd96fc2 100644 --- a/nncli/__main__.py +++ b/nncli/__main__.py @@ -2,5 +2,6 @@ """nncli main module""" import nncli.cli +# pylint: disable=no-value-for-parameter if __name__ == '__main__': nncli.cli.main() diff --git a/nncli/clipboard.py b/nncli/clipboard.py index 091fc36..cb4ba41 100644 --- a/nncli/clipboard.py +++ b/nncli/clipboard.py @@ -1,19 +1,33 @@ # -*- coding: utf-8 -*- - +"""clipboard module""" import os -from distutils import spawn +import subprocess +from subprocess import CalledProcessError -class Clipboard(object): +class Clipboard: + """Class implements copying note content to the clipboard""" def __init__(self): self.copy_command = self.get_copy_command() - def get_copy_command(self): - if (spawn.find_executable('xsel')): + @staticmethod + def get_copy_command(): + """Defines the copy command based on the contents of $PATH""" + + try: + subprocess.check_output(['which', 'xsel']) return 'echo "%s" | xsel -ib' - if (spawn.find_executable('pbcopy')): + except CalledProcessError: + pass + + try: + subprocess.check_output(['which', 'pbcopy']) return 'echo "%s" | pbcopy' + except CalledProcessError: + pass + return None def copy(self, text): - if (self.copy_command): + """Copies text to the system clipboard""" + if self.copy_command: os.system(self.copy_command % text) diff --git a/nncli/config.py b/nncli/config.py index 8070e4c..c0980c5 100644 --- a/nncli/config.py +++ b/nncli/config.py @@ -1,271 +1,633 @@ # -*- coding: utf-8 -*- - -import os, sys, urwid, collections, configparser, subprocess +"""config module""" +import collections +import configparser +import os +import subprocess +import sys from appdirs import user_cache_dir, user_config_dir +# pylint: disable=too-few-public-methods class Config: + """A class to contain all configuration data for nncli""" + class State: + """A container class for state information""" + def __init__(self, **kwargs): + self.__dict__.update(kwargs) def __init__(self, custom_file=None): + self.state = Config.State(do_server_sync=True, + verbose=False, + do_gui=False, + search_direction=None) self.config_home = user_config_dir('nncli', 'djmoch') self.cache_home = user_cache_dir('nncli', 'djmoch') defaults = \ { - 'cfg_nn_username' : '', - 'cfg_nn_password' : '', - 'cfg_nn_password_eval' : '', - 'cfg_db_path' : self.cache_home, - 'cfg_search_categories' : 'yes', # with regex searches - 'cfg_sort_mode' : 'date', # 'alpha' or 'date' - 'cfg_favorite_ontop' : 'yes', - 'cfg_tabstop' : '4', - 'cfg_format_strftime' : '%Y/%m/%d', - 'cfg_format_note_title' : '[%D] %F %-N %T', - 'cfg_status_bar' : 'yes', - 'cfg_editor' : os.environ['EDITOR'] if 'EDITOR' in os.environ else 'vim {fname} +{line}', - 'cfg_pager' : os.environ['PAGER'] if 'PAGER' in os.environ else 'less -c', - 'cfg_max_logs' : '5', - 'cfg_log_timeout' : '5', - 'cfg_log_reversed' : 'yes', - 'cfg_nn_host' : '', - 'cfg_tempdir' : '', + 'cfg_nn_username' : '', + 'cfg_nn_password' : '', + 'cfg_nn_password_eval' : '', + 'cfg_db_path' : self.cache_home, + 'cfg_search_categories' : 'yes', # with regex searches + 'cfg_sort_mode' : 'date', # 'alpha' or 'date' + 'cfg_favorite_ontop' : 'yes', + 'cfg_tabstop' : '4', + 'cfg_format_strftime' : '%Y/%m/%d', + 'cfg_format_note_title' : '[%D] %F %-N %T', + 'cfg_status_bar' : 'yes', + 'cfg_editor' : os.environ['EDITOR'] \ + if 'EDITOR' in os.environ else 'vim {fname} +{line}', + 'cfg_pager' : os.environ['PAGER'] \ + if 'PAGER' in os.environ else 'less -c', + 'cfg_max_logs' : '5', + 'cfg_log_timeout' : '5', + 'cfg_log_reversed' : 'yes', + 'cfg_nn_host' : '', + 'cfg_tempdir' : '', - 'kb_help' : 'h', - 'kb_quit' : 'q', - 'kb_sync' : 'S', - 'kb_down' : 'j', - 'kb_up' : 'k', - 'kb_page_down' : 'space', - 'kb_page_up' : 'b', - 'kb_half_page_down' : 'ctrl d', - 'kb_half_page_up' : 'ctrl u', - 'kb_bottom' : 'G', - 'kb_top' : 'g', - 'kb_status' : 's', - 'kb_create_note' : 'C', - 'kb_edit_note' : 'e', - 'kb_view_note' : 'enter', - 'kb_view_note_ext' : 'meta enter', - 'kb_view_note_json' : 'O', - 'kb_pipe_note' : '|', - 'kb_view_next_note' : 'J', - 'kb_view_prev_note' : 'K', - 'kb_view_log' : 'l', - 'kb_tabstop2' : '2', - 'kb_tabstop4' : '4', - 'kb_tabstop8' : '8', - 'kb_search_gstyle' : '/', - 'kb_search_regex' : 'meta /', - 'kb_search_prev_gstyle' : '?', - 'kb_search_prev_regex' : 'meta ?', - 'kb_search_next' : 'n', - 'kb_search_prev' : 'N', - 'kb_clear_search' : 'A', - 'kb_sort_date' : 'd', - 'kb_sort_alpha' : 'a', - 'kb_sort_categories' : 'ctrl t', - 'kb_note_delete' : 'D', - 'kb_note_favorite' : 'p', - 'kb_note_category' : 't', - 'kb_copy_note_text' : 'y', + 'kb_help' : 'h', + 'kb_quit' : 'q', + 'kb_sync' : 'S', + 'kb_down' : 'j', + 'kb_up' : 'k', + 'kb_page_down' : 'space', + 'kb_page_up' : 'b', + 'kb_half_page_down' : 'ctrl d', + 'kb_half_page_up' : 'ctrl u', + 'kb_bottom' : 'G', + 'kb_top' : 'g', + 'kb_status' : 's', + 'kb_create_note' : 'C', + 'kb_edit_note' : 'e', + 'kb_view_note' : 'enter', + 'kb_view_note_ext' : 'meta enter', + 'kb_view_note_json' : 'O', + 'kb_pipe_note' : '|', + 'kb_view_next_note' : 'J', + 'kb_view_prev_note' : 'K', + 'kb_view_log' : 'l', + 'kb_tabstop2' : '2', + 'kb_tabstop4' : '4', + 'kb_tabstop8' : '8', + 'kb_search_gstyle' : '/', + 'kb_search_regex' : 'meta /', + 'kb_search_prev_gstyle' : '?', + 'kb_search_prev_regex' : 'meta ?', + 'kb_search_next' : 'n', + 'kb_search_prev' : 'N', + 'kb_clear_search' : 'A', + 'kb_sort_date' : 'd', + 'kb_sort_alpha' : 'a', + 'kb_sort_categories' : 'ctrl t', + 'kb_note_delete' : 'D', + 'kb_note_favorite' : 'p', + 'kb_note_category' : 't', + 'kb_copy_note_text' : 'y', - 'clr_default_fg' : 'default', - 'clr_default_bg' : 'default', - 'clr_status_bar_fg' : 'dark gray', - 'clr_status_bar_bg' : 'light gray', - 'clr_log_fg' : 'dark gray', - 'clr_log_bg' : 'light gray', - 'clr_user_input_bar_fg' : 'white', - 'clr_user_input_bar_bg' : 'light red', - 'clr_note_focus_fg' : 'white', - 'clr_note_focus_bg' : 'light red', - 'clr_note_title_day_fg' : 'light red', - 'clr_note_title_day_bg' : 'default', - 'clr_note_title_week_fg' : 'light green', - 'clr_note_title_week_bg' : 'default', - 'clr_note_title_month_fg' : 'brown', - 'clr_note_title_month_bg' : 'default', - 'clr_note_title_year_fg' : 'light blue', - 'clr_note_title_year_bg' : 'default', - 'clr_note_title_ancient_fg' : 'light blue', - 'clr_note_title_ancient_bg' : 'default', - 'clr_note_date_fg' : 'dark blue', - 'clr_note_date_bg' : 'default', - 'clr_note_flags_fg' : 'dark magenta', - 'clr_note_flags_bg' : 'default', - 'clr_note_category_fg' : 'dark red', - 'clr_note_category_bg' : 'default', - 'clr_note_content_fg' : 'default', - 'clr_note_content_bg' : 'default', - 'clr_note_content_focus_fg' : 'white', - 'clr_note_content_focus_bg' : 'light red', - 'clr_note_content_old_fg' : 'yellow', - 'clr_note_content_old_bg' : 'dark gray', - 'clr_note_content_old_focus_fg' : 'white', - 'clr_note_content_old_focus_bg' : 'light red', - 'clr_help_focus_fg' : 'white', - 'clr_help_focus_bg' : 'light red', - 'clr_help_header_fg' : 'dark blue', - 'clr_help_header_bg' : 'default', - 'clr_help_config_fg' : 'dark green', - 'clr_help_config_bg' : 'default', - 'clr_help_value_fg' : 'dark red', - 'clr_help_value_bg' : 'default', - 'clr_help_descr_fg' : 'default', - 'clr_help_descr_bg' : 'default' + 'clr_default_fg' : 'default', + 'clr_default_bg' : 'default', + 'clr_status_bar_fg' : 'dark gray', + 'clr_status_bar_bg' : 'light gray', + 'clr_log_fg' : 'dark gray', + 'clr_log_bg' : 'light gray', + 'clr_user_input_bar_fg' : 'white', + 'clr_user_input_bar_bg' : 'light red', + 'clr_note_focus_fg' : 'white', + 'clr_note_focus_bg' : 'light red', + 'clr_note_title_day_fg' : 'light red', + 'clr_note_title_day_bg' : 'default', + 'clr_note_title_week_fg' : 'light green', + 'clr_note_title_week_bg' : 'default', + 'clr_note_title_month_fg' : 'brown', + 'clr_note_title_month_bg' : 'default', + 'clr_note_title_year_fg' : 'light blue', + 'clr_note_title_year_bg' : 'default', + 'clr_note_title_ancient_fg' : 'light blue', + 'clr_note_title_ancient_bg' : 'default', + 'clr_note_date_fg' : 'dark blue', + 'clr_note_date_bg' : 'default', + 'clr_note_flags_fg' : 'dark magenta', + 'clr_note_flags_bg' : 'default', + 'clr_note_category_fg' : 'dark red', + 'clr_note_category_bg' : 'default', + 'clr_note_content_fg' : 'default', + 'clr_note_content_bg' : 'default', + 'clr_note_content_focus_fg' : 'white', + 'clr_note_content_focus_bg' : 'light red', + 'clr_note_content_old_fg' : 'yellow', + 'clr_note_content_old_bg' : 'dark gray', + 'clr_note_content_old_focus_fg' : 'white', + 'clr_note_content_old_focus_bg' : 'light red', + 'clr_help_focus_fg' : 'white', + 'clr_help_focus_bg' : 'light red', + 'clr_help_header_fg' : 'dark blue', + 'clr_help_header_bg' : 'default', + 'clr_help_config_fg' : 'dark green', + 'clr_help_config_bg' : 'default', + 'clr_help_value_fg' : 'dark red', + 'clr_help_value_bg' : 'default', + 'clr_help_descr_fg' : 'default', + 'clr_help_descr_bg' : 'default' } - cp = configparser.SafeConfigParser(defaults) + parser = configparser.SafeConfigParser(defaults) if custom_file is not None: - cp.read([custom_file]) + parser.read([custom_file]) else: - cp.read([os.path.join(self.config_home, 'config')]) + parser.read([os.path.join(self.config_home, 'config')]) cfg_sec = 'nncli' - if not cp.has_section(cfg_sec): - cp.add_section(cfg_sec) + if not parser.has_section(cfg_sec): + parser.add_section(cfg_sec) + + # ordered dicts used to ease help + self._create_configs_dict(parser, cfg_sec) + self._create_keybinds_dict(parser, cfg_sec) + self._create_colors_dict(parser, cfg_sec) + + def _create_keybinds_dict(self, parser, cfg_sec): + """Create an OrderedDict object with the keybinds""" + self.keybinds = collections.OrderedDict() + self.keybinds['help'] = \ + [parser.get(cfg_sec, 'kb_help'), ['common'], 'Help'] + self.keybinds['quit'] = \ + [parser.get(cfg_sec, 'kb_quit'), ['common'], 'Quit'] + self.keybinds['sync'] = \ + [parser.get(cfg_sec, 'kb_sync'), ['common'], 'Full sync'] + self.keybinds['down'] = \ + [ + parser.get(cfg_sec, 'kb_down'), + ['common'], + 'Scroll down one line' + ] + self.keybinds['up'] = \ + [ + parser.get(cfg_sec, 'kb_up'), + ['common'], + 'Scroll up one line' + ] + self.keybinds['page_down'] = \ + [ + parser.get(cfg_sec, 'kb_page_down'), + ['common'], + 'Page down' + ] + self.keybinds['page_up'] = \ + [parser.get(cfg_sec, 'kb_page_up'), ['common'], 'Page up'] + self.keybinds['half_page_down'] = \ + [ + parser.get(cfg_sec, 'kb_half_page_down'), + ['common'], + 'Half page down' + ] + self.keybinds['half_page_up'] = \ + [ + parser.get(cfg_sec, 'kb_half_page_up'), + ['common'], + 'Half page up' + ] + self.keybinds['bottom'] = \ + [ + parser.get(cfg_sec, 'kb_bottom'), + ['common'], + 'Goto bottom' + ] + self.keybinds['top'] = \ + [parser.get(cfg_sec, 'kb_top'), ['common'], 'Goto top'] + self.keybinds['status'] = \ + [ + parser.get(cfg_sec, 'kb_status'), + ['common'], + 'Toggle status bar' + ] + self.keybinds['view_log'] = \ + [ + parser.get(cfg_sec, 'kb_view_log'), + ['common'], + 'View log' + ] + self.keybinds['create_note'] = \ + [ + parser.get(cfg_sec, 'kb_create_note'), + ['titles'], + 'Create a new note' + ] + self.keybinds['edit_note'] = \ + [ + parser.get(cfg_sec, 'kb_edit_note'), + ['titles', 'notes'], + 'Edit note' + ] + self.keybinds['view_note'] = \ + [ + parser.get(cfg_sec, 'kb_view_note'), + ['titles'], + 'View note' + ] + self.keybinds['view_note_ext'] = \ + [ + parser.get(cfg_sec, 'kb_view_note_ext'), + ['titles', 'notes'], + 'View note with pager' + ] + self.keybinds['view_note_json'] = \ + [ + parser.get(cfg_sec, 'kb_view_note_json'), + ['titles', 'notes'], + 'View note raw json' + ] + self.keybinds['pipe_note'] = \ + [ + parser.get(cfg_sec, 'kb_pipe_note'), + ['titles', 'notes'], + 'Pipe note contents' + ] + self.keybinds['view_next_note'] = \ + [ + parser.get(cfg_sec, 'kb_view_next_note'), + ['notes'], + 'View next note' + ] + self.keybinds['view_prev_note'] = \ + [ + parser.get(cfg_sec, 'kb_view_prev_note'), + ['notes'], + 'View previous note' + ] + self.keybinds['tabstop2'] = \ + [ + parser.get(cfg_sec, 'kb_tabstop2'), + ['notes'], + 'View with tabstop=2' + ] + self.keybinds['tabstop4'] = \ + [ + parser.get(cfg_sec, 'kb_tabstop4'), + ['notes'], + 'View with tabstop=4' + ] + self.keybinds['tabstop8'] = \ + [ + parser.get(cfg_sec, 'kb_tabstop8'), + ['notes'], + 'View with tabstop=8' + ] + self.keybinds['search_gstyle'] = \ + [ + parser.get(cfg_sec, 'kb_search_gstyle'), + ['titles', 'notes'], + 'Search using gstyle' + ] + self.keybinds['search_prev_gstyle'] = \ + [ + parser.get(cfg_sec, 'kb_search_prev_gstyle'), + ['notes'], + 'Search backwards using gstyle' + ] + self.keybinds['search_regex'] = \ + [ + parser.get(cfg_sec, 'kb_search_regex'), + ['titles', 'notes'], + 'Search using regex' + ] + self.keybinds['search_prev_regex'] = \ + [ + parser.get(cfg_sec, 'kb_search_prev_regex'), + ['notes'], + 'Search backwards using regex' + ] + self.keybinds['search_next'] = \ + [ + parser.get(cfg_sec, 'kb_search_next'), + ['notes'], + 'Go to next search result' + ] + self.keybinds['search_prev'] = \ + [ + parser.get(cfg_sec, 'kb_search_prev'), + ['notes'], + 'Go to previous search result' + ] + self.keybinds['clear_search'] = \ + [ + parser.get(cfg_sec, 'kb_clear_search'), + ['titles'], + 'Show all notes' + ] + self.keybinds['sort_date'] = \ + [ + parser.get(cfg_sec, 'kb_sort_date'), + ['titles'], + 'Sort notes by date' + ] + self.keybinds['sort_alpha'] = \ + [ + parser.get(cfg_sec, 'kb_sort_alpha'), + ['titles'], + 'Sort notes by alpha' + ] + self.keybinds['sort_categories'] = \ + [ + parser.get(cfg_sec, 'kb_sort_categories'), + ['titles'], + 'Sort notes by categories' + ] + self.keybinds['note_delete'] = \ + [ + parser.get(cfg_sec, 'kb_note_delete'), + ['titles', 'notes'], + 'Delete a note' + ] + self.keybinds['note_favorite'] = \ + [ + parser.get(cfg_sec, 'kb_note_favorite'), + ['titles', 'notes'], + 'Favorite note' + ] + self.keybinds['note_category'] = \ + [ + parser.get(cfg_sec, 'kb_note_category'), + ['titles', 'notes'], + 'Edit note category' + ] + self.keybinds['copy_note_text'] = \ + [ + parser.get(cfg_sec, 'kb_copy_note_text'), + ['notes'], + 'Copy line (xsel/pbcopy)' + ] + + def _create_colors_dict(self, parser, cfg_sec): + """Create an OrderedDict object with the colors""" + self.colors = collections.OrderedDict() + self.colors['default_fg'] = \ + [parser.get(cfg_sec, 'clr_default_fg'), 'Default fg'] + self.colors['default_bg'] = \ + [parser.get(cfg_sec, 'clr_default_bg'), 'Default bg'] + self.colors['status_bar_fg'] = \ + [parser.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg'] + self.colors['status_bar_bg'] = \ + [parser.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg'] + self.colors['log_fg'] = \ + [parser.get(cfg_sec, 'clr_log_fg'), 'Log message fg'] + self.colors['log_bg'] = \ + [parser.get(cfg_sec, 'clr_log_bg'), 'Log message bg'] + self.colors['user_input_bar_fg'] = \ + [ + parser.get(cfg_sec, 'clr_user_input_bar_fg'), + 'User input bar fg' + ] + self.colors['user_input_bar_bg'] = \ + [ + parser.get(cfg_sec, 'clr_user_input_bar_bg'), + 'User input bar bg' + ] + self.colors['note_focus_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_focus_fg'), + 'Note title focus fg' + ] + self.colors['note_focus_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_focus_bg'), + 'Note title focus bg' + ] + self.colors['note_title_day_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_day_fg'), + 'Day old note title fg' + ] + self.colors['note_title_day_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_day_bg'), + 'Day old note title bg' + ] + self.colors['note_title_week_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_week_fg'), + 'Week old note title fg' + ] + self.colors['note_title_week_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_week_bg'), + 'Week old note title bg' + ] + self.colors['note_title_month_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_month_fg'), + 'Month old note title fg' + ] + self.colors['note_title_month_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_month_bg'), + 'Month old note title bg' + ] + self.colors['note_title_year_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_year_fg'), + 'Year old note title fg' + ] + self.colors['note_title_year_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_year_bg'), + 'Year old note title bg' + ] + self.colors['note_title_ancient_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_ancient_fg'), + 'Ancient note title fg' + ] + self.colors['note_title_ancient_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_title_ancient_bg'), + 'Ancient note title bg' + ] + self.colors['note_date_fg'] = \ + [parser.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg'] + self.colors['note_date_bg'] = \ + [parser.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg'] + self.colors['note_flags_fg'] = \ + [parser.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg'] + self.colors['note_flags_bg'] = \ + [parser.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg'] + self.colors['note_category_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_category_fg'), + 'Note category fg' + ] + self.colors['note_category_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_category_bg'), + 'Note category bg' + ] + self.colors['note_content_fg'] = \ + [parser.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg'] + self.colors['note_content_bg'] = \ + [parser.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg'] + self.colors['note_content_focus_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_focus_fg'), + 'Note content focus fg' + ] + self.colors['note_content_focus_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_focus_bg'), + 'Note content focus bg' + ] + self.colors['note_content_old_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_old_fg'), + 'Old note content fg' + ] + self.colors['note_content_old_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_old_bg'), + 'Old note content bg' + ] + self.colors['note_content_old_focus_fg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_old_focus_fg'), + 'Old note content focus fg' + ] + self.colors['note_content_old_focus_bg'] = \ + [ + parser.get(cfg_sec, 'clr_note_content_old_focus_bg'), + 'Old note content focus bg' + ] + self.colors['help_focus_fg'] = \ + [parser.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg'] + self.colors['help_focus_bg'] = \ + [parser.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg'] + self.colors['help_header_fg'] = \ + [parser.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg'] + self.colors['help_header_bg'] = \ + [parser.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg'] + self.colors['help_config_fg'] = \ + [parser.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg'] + self.colors['help_config_bg'] = \ + [parser.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg'] + self.colors['help_value_fg'] = \ + [parser.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg'] + self.colors['help_value_bg'] = \ + [parser.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg'] + self.colors['help_descr_fg'] = \ + [ + parser.get(cfg_sec, 'clr_help_descr_fg'), + 'Help description fg' + ] + self.colors['help_descr_bg'] = \ + [ + parser.get(cfg_sec, 'clr_help_descr_bg'), + 'Help description bg' + ] + def _create_configs_dict(self, parser, cfg_sec): + """Create an OrderedDict object with the configs""" - # special handling for password so we can retrieve it by running a command - nn_password = cp.get(cfg_sec, 'cfg_nn_password', raw=True) + # special handling for password so we can retrieve it by + # running a command + nn_password = parser.get(cfg_sec, 'cfg_nn_password', raw=True) if not nn_password: - command = cp.get(cfg_sec, 'cfg_nn_password_eval', raw=True) + command = parser.get(cfg_sec, 'cfg_nn_password_eval', raw=True) if command: try: - nn_password = subprocess.check_output(command, shell=True, universal_newlines=True) - # remove trailing newlines to avoid requiring butchering shell commands (they can't usually be in passwords anyway) + nn_password = subprocess.check_output( + command, + shell=True, + universal_newlines=True + ) + # remove trailing newlines to avoid requiring + # butchering shell commands (they can't usually be + # in passwords anyway) nn_password = nn_password.rstrip('\n') - except subprocess.CalledProcessError as e: - print('Error evaluating command for password.') - print(e) + except subprocess.CalledProcessError as ex: + print('Error evaluating command for password: %s' % ex) sys.exit(1) - # ordered dicts used to ease help - self.configs = collections.OrderedDict() - self.configs['nn_username'] = [ cp.get(cfg_sec, 'cfg_nn_username', raw=True), 'NextCloud Username' ] - self.configs['nn_password'] = [ nn_password, 'NextCloud Password' ] - self.configs['nn_host'] = [ cp.get(cfg_sec, 'cfg_nn_host', raw=True), 'NextCloud server hostname' ] - self.configs['db_path'] = [ cp.get(cfg_sec, 'cfg_db_path'), 'Note storage path' ] - self.configs['search_categories'] = [ cp.get(cfg_sec, 'cfg_search_categories'), 'Search categories as well' ] - self.configs['sort_mode'] = [ cp.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode' ] - self.configs['favorite_ontop'] = [ cp.get(cfg_sec, 'cfg_favorite_ontop'), 'Favorite at top of list' ] - self.configs['tabstop'] = [ cp.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces' ] - self.configs['format_strftime'] = [ cp.get(cfg_sec, 'cfg_format_strftime', raw=True), 'Date strftime format' ] - self.configs['format_note_title'] = [ cp.get(cfg_sec, 'cfg_format_note_title', raw=True), 'Note title format' ] - self.configs['status_bar'] = [ cp.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar' ] - self.configs['editor'] = [ cp.get(cfg_sec, 'cfg_editor'), 'Editor command' ] - self.configs['pager'] = [ cp.get(cfg_sec, 'cfg_pager'), 'External pager command' ] - self.configs['max_logs'] = [ cp.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer' ] - self.configs['log_timeout'] = [ cp.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout' ] - self.configs['log_reversed'] = [ cp.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed' ] - self.configs['tempdir'] = [ cp.get(cfg_sec, 'cfg_tempdir'), 'Temporary directory for note storage' ] - - self.keybinds = collections.OrderedDict() - self.keybinds['help'] = [ cp.get(cfg_sec, 'kb_help'), [ 'common' ], 'Help' ] - self.keybinds['quit'] = [ cp.get(cfg_sec, 'kb_quit'), [ 'common' ], 'Quit' ] - self.keybinds['sync'] = [ cp.get(cfg_sec, 'kb_sync'), [ 'common' ], 'Full sync' ] - self.keybinds['down'] = [ cp.get(cfg_sec, 'kb_down'), [ 'common' ], 'Scroll down one line' ] - self.keybinds['up'] = [ cp.get(cfg_sec, 'kb_up'), [ 'common' ], 'Scroll up one line' ] - self.keybinds['page_down'] = [ cp.get(cfg_sec, 'kb_page_down'), [ 'common' ], 'Page down' ] - self.keybinds['page_up'] = [ cp.get(cfg_sec, 'kb_page_up'), [ 'common' ], 'Page up' ] - self.keybinds['half_page_down'] = [ cp.get(cfg_sec, 'kb_half_page_down'), [ 'common' ], 'Half page down' ] - self.keybinds['half_page_up'] = [ cp.get(cfg_sec, 'kb_half_page_up'), [ 'common' ], 'Half page up' ] - self.keybinds['bottom'] = [ cp.get(cfg_sec, 'kb_bottom'), [ 'common' ], 'Goto bottom' ] - self.keybinds['top'] = [ cp.get(cfg_sec, 'kb_top'), [ 'common' ], 'Goto top' ] - self.keybinds['status'] = [ cp.get(cfg_sec, 'kb_status'), [ 'common' ], 'Toggle status bar' ] - self.keybinds['view_log'] = [ cp.get(cfg_sec, 'kb_view_log'), [ 'common' ], 'View log' ] - self.keybinds['create_note'] = [ cp.get(cfg_sec, 'kb_create_note'), [ 'titles' ], 'Create a new note' ] - self.keybinds['edit_note'] = [ cp.get(cfg_sec, 'kb_edit_note'), [ 'titles', 'notes' ], 'Edit note' ] - self.keybinds['view_note'] = [ cp.get(cfg_sec, 'kb_view_note'), [ 'titles' ], 'View note' ] - self.keybinds['view_note_ext'] = [ cp.get(cfg_sec, 'kb_view_note_ext'), [ 'titles', 'notes' ], 'View note with pager' ] - self.keybinds['view_note_json'] = [ cp.get(cfg_sec, 'kb_view_note_json'), [ 'titles', 'notes' ], 'View note raw json' ] - self.keybinds['pipe_note'] = [ cp.get(cfg_sec, 'kb_pipe_note'), [ 'titles', 'notes' ], 'Pipe note contents' ] - self.keybinds['view_next_note'] = [ cp.get(cfg_sec, 'kb_view_next_note'), [ 'notes' ], 'View next note' ] - self.keybinds['view_prev_note'] = [ cp.get(cfg_sec, 'kb_view_prev_note'), [ 'notes' ], 'View previous note' ] - self.keybinds['tabstop2'] = [ cp.get(cfg_sec, 'kb_tabstop2'), [ 'notes' ], 'View with tabstop=2' ] - self.keybinds['tabstop4'] = [ cp.get(cfg_sec, 'kb_tabstop4'), [ 'notes' ], 'View with tabstop=4' ] - self.keybinds['tabstop8'] = [ cp.get(cfg_sec, 'kb_tabstop8'), [ 'notes' ], 'View with tabstop=8' ] - self.keybinds['search_gstyle'] = [ cp.get(cfg_sec, 'kb_search_gstyle'), [ 'titles', 'notes' ], 'Search using gstyle' ] - self.keybinds['search_prev_gstyle'] = [ cp.get(cfg_sec, 'kb_search_prev_gstyle'), [ 'notes' ], 'Search backwards using gstyle' ] - self.keybinds['search_regex'] = [ cp.get(cfg_sec, 'kb_search_regex'), [ 'titles', 'notes' ], 'Search using regex' ] - self.keybinds['search_prev_regex'] = [ cp.get(cfg_sec, 'kb_search_prev_regex'), [ 'notes' ], 'Search backwards using regex' ] - self.keybinds['search_next'] = [ cp.get(cfg_sec, 'kb_search_next'), [ 'notes' ], 'Go to next search result' ] - self.keybinds['search_prev'] = [ cp.get(cfg_sec, 'kb_search_prev'), [ 'notes' ], 'Go to previous search result' ] - self.keybinds['clear_search'] = [ cp.get(cfg_sec, 'kb_clear_search'), [ 'titles' ], 'Show all notes' ] - self.keybinds['sort_date'] = [ cp.get(cfg_sec, 'kb_sort_date'), [ 'titles' ], 'Sort notes by date' ] - self.keybinds['sort_alpha'] = [ cp.get(cfg_sec, 'kb_sort_alpha'), [ 'titles' ], 'Sort notes by alpha' ] - self.keybinds['sort_categories'] = [ cp.get(cfg_sec, 'kb_sort_categories'), [ 'titles' ], 'Sort notes by categories' ] - self.keybinds['note_delete'] = [ cp.get(cfg_sec,'kb_note_delete'), [ 'titles', 'notes' ], 'Delete a note' ] - self.keybinds['note_favorite'] = [ cp.get(cfg_sec, 'kb_note_favorite'), [ 'titles', 'notes' ], 'Favorite note' ] - self.keybinds['note_category'] = [ cp.get(cfg_sec, 'kb_note_category'), [ 'titles', 'notes' ], 'Edit note category' ] - self.keybinds['copy_note_text'] = [ cp.get(cfg_sec, 'kb_copy_note_text'), [ 'notes' ], 'Copy line (xsel/pbcopy)' ] - - self.colors = collections.OrderedDict() - self.colors['default_fg'] = [ cp.get(cfg_sec, 'clr_default_fg'), 'Default fg' ] - self.colors['default_bg'] = [ cp.get(cfg_sec, 'clr_default_bg'), 'Default bg' ] - self.colors['status_bar_fg'] = [ cp.get(cfg_sec, 'clr_status_bar_fg'), 'Status bar fg' ] - self.colors['status_bar_bg'] = [ cp.get(cfg_sec, 'clr_status_bar_bg'), 'Status bar bg' ] - self.colors['log_fg'] = [ cp.get(cfg_sec, 'clr_log_fg'), 'Log message fg' ] - self.colors['log_bg'] = [ cp.get(cfg_sec, 'clr_log_bg'), 'Log message bg' ] - self.colors['user_input_bar_fg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_fg'), 'User input bar fg' ] - self.colors['user_input_bar_bg'] = [ cp.get(cfg_sec, 'clr_user_input_bar_bg'), 'User input bar bg' ] - self.colors['note_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_focus_fg'), 'Note title focus fg' ] - self.colors['note_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_focus_bg'), 'Note title focus bg' ] - self.colors['note_title_day_fg'] = [ cp.get(cfg_sec, 'clr_note_title_day_fg'), 'Day old note title fg' ] - self.colors['note_title_day_bg'] = [ cp.get(cfg_sec, 'clr_note_title_day_bg'), 'Day old note title bg' ] - self.colors['note_title_week_fg'] = [ cp.get(cfg_sec, 'clr_note_title_week_fg'), 'Week old note title fg' ] - self.colors['note_title_week_bg'] = [ cp.get(cfg_sec, 'clr_note_title_week_bg'), 'Week old note title bg' ] - self.colors['note_title_month_fg'] = [ cp.get(cfg_sec, 'clr_note_title_month_fg'), 'Month old note title fg' ] - self.colors['note_title_month_bg'] = [ cp.get(cfg_sec, 'clr_note_title_month_bg'), 'Month old note title bg' ] - self.colors['note_title_year_fg'] = [ cp.get(cfg_sec, 'clr_note_title_year_fg'), 'Year old note title fg' ] - self.colors['note_title_year_bg'] = [ cp.get(cfg_sec, 'clr_note_title_year_bg'), 'Year old note title bg' ] - self.colors['note_title_ancient_fg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_fg'), 'Ancient note title fg' ] - self.colors['note_title_ancient_bg'] = [ cp.get(cfg_sec, 'clr_note_title_ancient_bg'), 'Ancient note title bg' ] - self.colors['note_date_fg'] = [ cp.get(cfg_sec, 'clr_note_date_fg'), 'Note date fg' ] - self.colors['note_date_bg'] = [ cp.get(cfg_sec, 'clr_note_date_bg'), 'Note date bg' ] - self.colors['note_flags_fg'] = [ cp.get(cfg_sec, 'clr_note_flags_fg'), 'Note flags fg' ] - self.colors['note_flags_bg'] = [ cp.get(cfg_sec, 'clr_note_flags_bg'), 'Note flags bg' ] - self.colors['note_category_fg'] = [ cp.get(cfg_sec, 'clr_note_category_fg'), 'Note category fg' ] - self.colors['note_category_bg'] = [ cp.get(cfg_sec, 'clr_note_category_bg'), 'Note category bg' ] - self.colors['note_content_fg'] = [ cp.get(cfg_sec, 'clr_note_content_fg'), 'Note content fg' ] - self.colors['note_content_bg'] = [ cp.get(cfg_sec, 'clr_note_content_bg'), 'Note content bg' ] - self.colors['note_content_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_fg'), 'Note content focus fg' ] - self.colors['note_content_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_focus_bg'), 'Note content focus bg' ] - self.colors['note_content_old_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_fg'), 'Old note content fg' ] - self.colors['note_content_old_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_bg'), 'Old note content bg' ] - self.colors['note_content_old_focus_fg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_fg'), 'Old note content focus fg' ] - self.colors['note_content_old_focus_bg'] = [ cp.get(cfg_sec, 'clr_note_content_old_focus_bg'), 'Old note content focus bg' ] - self.colors['help_focus_fg'] = [ cp.get(cfg_sec, 'clr_help_focus_fg'), 'Help focus fg' ] - self.colors['help_focus_bg'] = [ cp.get(cfg_sec, 'clr_help_focus_bg'), 'Help focus bg' ] - self.colors['help_header_fg'] = [ cp.get(cfg_sec, 'clr_help_header_fg'), 'Help header fg' ] - self.colors['help_header_bg'] = [ cp.get(cfg_sec, 'clr_help_header_bg'), 'Help header bg' ] - self.colors['help_config_fg'] = [ cp.get(cfg_sec, 'clr_help_config_fg'), 'Help config fg' ] - self.colors['help_config_bg'] = [ cp.get(cfg_sec, 'clr_help_config_bg'), 'Help config bg' ] - self.colors['help_value_fg'] = [ cp.get(cfg_sec, 'clr_help_value_fg'), 'Help value fg' ] - self.colors['help_value_bg'] = [ cp.get(cfg_sec, 'clr_help_value_bg'), 'Help value bg' ] - self.colors['help_descr_fg'] = [ cp.get(cfg_sec, 'clr_help_descr_fg'), 'Help description fg' ] - self.colors['help_descr_bg'] = [ cp.get(cfg_sec, 'clr_help_descr_bg'), 'Help description bg' ] + self.configs['nn_username'] = \ + [ + parser.get(cfg_sec, 'cfg_nn_username', raw=True), + 'NextCloud Username' + ] + self.configs['nn_password'] = [nn_password, 'NextCloud Password'] + self.configs['nn_host'] = \ + [ + parser.get(cfg_sec, 'cfg_nn_host', raw=True), + 'NextCloud server hostname' + ] + self.configs['db_path'] = \ + [parser.get(cfg_sec, 'cfg_db_path'), 'Note storage path'] + self.configs['search_categories'] = \ + [ + parser.get(cfg_sec, 'cfg_search_categories'), + 'Search categories as well' + ] + self.configs['sort_mode'] = \ + [parser.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode'] + self.configs['favorite_ontop'] = \ + [ + parser.get(cfg_sec, 'cfg_favorite_ontop'), + 'Favorite at top of list' + ] + self.configs['tabstop'] = \ + [parser.get(cfg_sec, 'cfg_tabstop'), 'Tabstop spaces'] + self.configs['format_strftime'] = \ + [ + parser.get(cfg_sec, 'cfg_format_strftime', raw=True), + 'Date strftime format' + ] + self.configs['format_note_title'] = \ + [ + parser.get(cfg_sec, 'cfg_format_note_title', raw=True), + 'Note title format' + ] + self.configs['status_bar'] = \ + [parser.get(cfg_sec, 'cfg_status_bar'), 'Show the status bar'] + self.configs['editor'] = \ + [parser.get(cfg_sec, 'cfg_editor'), 'Editor command'] + self.configs['pager'] = \ + [parser.get(cfg_sec, 'cfg_pager'), 'External pager command'] + self.configs['max_logs'] = \ + [parser.get(cfg_sec, 'cfg_max_logs'), 'Max logs in footer'] + self.configs['log_timeout'] = \ + [parser.get(cfg_sec, 'cfg_log_timeout'), 'Log timeout'] + self.configs['log_reversed'] = \ + [parser.get(cfg_sec, 'cfg_log_reversed'), 'Log file reversed'] + self.configs['tempdir'] = \ + [ + None if parser.get(cfg_sec, 'cfg_tempdir') == '' \ + else parser.get(cfg_sec, 'cfg_tempdir'), + 'Temporary directory for note storage' + ] def get_config(self, name): + """Get a config value""" return self.configs[name][0] def get_config_descr(self, name): + """Get a config description""" return self.configs[name][1] def get_keybind(self, name): + """Get a keybinding value""" return self.keybinds[name][0] def get_keybind_use(self, name): + """Get the context(s) where a keybinding is valid""" return self.keybinds[name][1] def get_keybind_descr(self, name): + """Get a keybinding description""" return self.keybinds[name][2] def get_color(self, name): + """Get a color value""" return self.colors[name][0] def get_color_descr(self, name): + """Get a color description""" return self.colors[name][1] diff --git a/nncli/gui.py b/nncli/gui.py new file mode 100644 index 0000000..674dd68 --- /dev/null +++ b/nncli/gui.py @@ -0,0 +1,913 @@ +# -*- coding: utf-8 -*- +"""nncli_gui module""" +import hashlib +import subprocess +import threading + +import urwid +from . import view_titles, view_note, view_help, view_log, user_input +from .utils import exec_cmd_on_note, get_pager + +# pylint: disable=too-many-instance-attributes, unused-argument +class NncliGui: + """NncliGui class. Responsible for the console GUI view logic.""" + def __init__(self, config, logger, ndb, key=None): + self.ndb = ndb + self.logger = logger + self.config = config + self.last_view = [] + self.status_bar = self.config.get_config('status_bar') + self.config.state.current_sort_mode = \ + self.config.get_config('sort_mode') + + + self.log_lock = threading.Lock() + self.log_alarms = 0 + self.logs = [] + + self.thread_sync = threading.Thread( + target=self.ndb.sync_worker, + args=[self.config.state.do_server_sync] + ) + self.thread_sync.setDaemon(True) + + self.view_titles = \ + view_titles.ViewTitles( + self.config, + { + 'ndb' : self.ndb, + 'search_string' : None, + 'log' : self.log + } + ) + self.view_note = \ + view_note.ViewNote( + self.config, + { + 'ndb' : self.ndb, + 'id' : key, # initial key to view or None + 'log' : self.log + } + ) + + self.view_log = view_log.ViewLog(self.config) + self.view_help = view_help.ViewHelp(self.config) + + palette = \ + [ + ( + 'default', + self.config.get_color('default_fg'), + self.config.get_color('default_bg') + ), + ( + 'status_bar', + self.config.get_color('status_bar_fg'), + self.config.get_color('status_bar_bg') + ), + ( + 'log', + self.config.get_color('log_fg'), + self.config.get_color('log_bg') + ), + ( + 'user_input_bar', + self.config.get_color('user_input_bar_fg'), + self.config.get_color('user_input_bar_bg') + ), + ( + 'note_focus', + self.config.get_color('note_focus_fg'), + self.config.get_color('note_focus_bg') + ), + ( + 'note_title_day', + self.config.get_color('note_title_day_fg'), + self.config.get_color('note_title_day_bg') + ), + ( + 'note_title_week', + self.config.get_color('note_title_week_fg'), + self.config.get_color('note_title_week_bg') + ), + ( + 'note_title_month', + self.config.get_color('note_title_month_fg'), + self.config.get_color('note_title_month_bg') + ), + ( + 'note_title_year', + self.config.get_color('note_title_year_fg'), + self.config.get_color('note_title_year_bg') + ), + ( + 'note_title_ancient', + self.config.get_color('note_title_ancient_fg'), + self.config.get_color('note_title_ancient_bg') + ), + ( + 'note_date', + self.config.get_color('note_date_fg'), + self.config.get_color('note_date_bg') + ), + ( + 'note_flags', + self.config.get_color('note_flags_fg'), + self.config.get_color('note_flags_bg') + ), + ( + 'note_category', + self.config.get_color('note_category_fg'), + self.config.get_color('note_category_bg') + ), + ( + 'note_content', + self.config.get_color('note_content_fg'), + self.config.get_color('note_content_bg') + ), + ( + 'note_content_focus', + self.config.get_color('note_content_focus_fg'), + self.config.get_color('note_content_focus_bg') + ), + ( + 'note_content_old', + self.config.get_color('note_content_old_fg'), + self.config.get_color('note_content_old_bg') + ), + ( + 'note_content_old_focus', + self.config.get_color( + 'note_content_old_focus_fg' + ), + self.config.get_color( + 'note_content_old_focus_bg' + ) + ), + ( + 'help_focus', + self.config.get_color('help_focus_fg'), + self.config.get_color('help_focus_bg') + ), + ( + 'help_header', + self.config.get_color('help_header_fg'), + self.config.get_color('help_header_bg') + ), + ( + 'help_config', + self.config.get_color('help_config_fg'), + self.config.get_color('help_config_bg') + ), + ( + 'help_value', + self.config.get_color('help_value_fg'), + self.config.get_color('help_value_bg') + ), + ( + 'help_descr', + self.config.get_color('help_descr_fg'), + self.config.get_color('help_descr_bg') + ) + ] + + self.master_frame = urwid.Frame( + body=urwid.Filler(urwid.Text('')), + header=None, + footer=urwid.Pile([urwid.Pile([]), urwid.Pile([])]), + focus_part='body') + + self.nncli_loop = urwid.MainLoop(self.master_frame, + palette, + handle_mouse=False) + + self.nncli_loop.set_alarm_in(0, self._gui_init_view, \ + True if key else False) + + def run(self): + """Run the GUI""" + self.nncli_loop.run() + + def _gui_header_clear(self): + """Clear the console GUI header row""" + self.master_frame.contents['header'] = (None, None) + self.nncli_loop.draw_screen() + + def _gui_header_set(self, widget): + """Set the content of the console GUI header row""" + self.master_frame.contents['header'] = (widget, None) + self.nncli_loop.draw_screen() + + def _gui_footer_log_clear(self): + """Clear the log at the bottom of the GUI""" + gui = self._gui_footer_input_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([urwid.Pile([]), urwid.Pile([gui])]), None) + self.nncli_loop.draw_screen() + + def _gui_footer_log_set(self, pile): + """Set the log at the bottom of the GUI""" + gui = self._gui_footer_input_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([urwid.Pile(pile), urwid.Pile([gui])]), None) + self.nncli_loop.draw_screen() + + def _gui_footer_log_get(self): + """Get the log at the bottom of the GUI""" + return self.master_frame.contents['footer'][0].contents[0][0] + + def _gui_footer_input_clear(self): + """Clear the input at the bottom of the GUI""" + pile = self._gui_footer_log_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([urwid.Pile([pile]), urwid.Pile([])]), None) + self.nncli_loop.draw_screen() + + def _gui_footer_input_set(self, gui): + """Set the input at the bottom of the GUI""" + pile = self._gui_footer_log_get() + self.master_frame.contents['footer'] = \ + (urwid.Pile([urwid.Pile([pile]), urwid.Pile([gui])]), None) + self.nncli_loop.draw_screen() + + def _gui_footer_input_get(self): + """Get the input at the bottom of the GUI""" + return self.master_frame.contents['footer'][0].contents[1][0] + + def _gui_footer_focus_input(self): + """Set the GUI focus to the input at the bottom of the GUI""" + self.master_frame.focus_position = 'footer' + self.master_frame.contents['footer'][0].focus_position = 1 + + def _gui_body_set(self, widget): + """Set the GUI body""" + self.master_frame.contents['body'] = (widget, None) + self._gui_update_status_bar() + self.nncli_loop.draw_screen() + + def gui_body_get(self): + """Get the GUI body""" + return self.master_frame.contents['body'][0] + + def _gui_body_focus(self): + """Set the GUI focus to the body""" + self.master_frame.focus_position = 'body' + + def gui_update_view(self): + """Update the GUI""" + if not self.config.state.do_gui: + return + + try: + cur_key = self.view_titles.note_list \ + [self.view_titles.focus_position].note['localkey'] + except IndexError: + cur_key = None + + self.view_titles.update_note_list( + self.view_titles.search_string, + sort_mode=self.config.state.current_sort_mode + ) + self.view_titles.focus_note(cur_key) + + if self.gui_body_get().__class__ == view_note.ViewNote: + self.view_note.update_note_view() + + self._gui_update_status_bar() + + def _gui_update_status_bar(self): + """Update the GUI status bar""" + if self.status_bar != 'yes': + self._gui_header_clear() + else: + self._gui_header_set(self.gui_body_get().get_status_bar()) + + def _gui_switch_frame_body(self, new_view, save_current_view=True): + """ + Switch the body frame of the GUI. Used to switch to a new + view + """ + if new_view is None: + if not self.last_view: + self._gui_stop() + else: + self._gui_body_set(self.last_view.pop()) + else: + if self.gui_body_get().__class__ != new_view.__class__: + if save_current_view: + self.last_view.append(self.gui_body_get()) + self._gui_body_set(new_view) + + def _delete_note_callback(self, key, delete): + """Update the GUI after deleting a note""" + if not delete: + return + self.ndb.set_note_deleted(key, True) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + self.view_titles.update_note_title() + + self._gui_update_status_bar() + self.ndb.sync_worker_go() + + def _gui_yes_no_input(self, args, yes_no): + """Create a yes/no input dialog at the GUI footer""" + self._gui_footer_input_clear() + self._gui_body_focus() + self.master_frame.keypress = self._gui_frame_keypress + args[0](args[1], + True if yes_no in ['YES', 'Yes', 'yes', 'Y', 'y'] \ + else False + ) + + def _gui_search_input(self, args, search_string): + """Create a search input dialog at the GUI footer""" + self._gui_footer_input_clear() + self._gui_body_focus() + self.master_frame.keypress = self._gui_frame_keypress + if search_string: + if self.gui_body_get() == self.view_note: + self.config.state.search_direction = args[1] + self.view_note.search_note_view_next( + search_string=search_string, + search_mode=args[0] + ) + else: + self.view_titles.update_note_list( + search_string, + args[0], + sort_mode=self.config.state.current_sort_mode + ) + self._gui_body_set(self.view_titles) + + def _gui_category_input(self, args, category): + """Create a category input at the GUI footer""" + self._gui_footer_input_clear() + self._gui_body_focus() + self.master_frame.keypress = self._gui_frame_keypress + if category is not None: + if self.gui_body_get().__class__ == view_titles.ViewTitles: + note = self.view_titles.note_list \ + [self.view_titles.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = self.view_note.note + + self.ndb.set_note_category(note['localkey'], category) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + self.view_titles.update_note_title() + else: # self.gui_body_get().__class__ == view_note.ViewNote: + self.view_note.update_note_view() + + self._gui_update_status_bar() + self.ndb.sync_worker_go() + + def _gui_pipe_input(self, args, cmd): + """Create a pipe input dialog at the GUI footoer""" + self._gui_footer_input_clear() + self._gui_body_focus() + self.master_frame.keypress = self._gui_frame_keypress + if cmd is not None: + if self.gui_body_get().__class__ == view_titles.ViewTitles: + note = self.view_titles.note_list \ + [self.view_titles.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = self.view_note.old_note \ + if self.view_note.old_note \ + else self.view_note.note + try: + self._gui_clear() + pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) + pipe.communicate(note['content'].encode('utf-8')) + pipe.stdin.close() + pipe.wait() + except OSError as ex: + self.log('Pipe error: %s' % ex) + finally: + self._gui_reset() + + # pylint: disable=too-many-return-statements, too-many-branches + # pylint: disable=too-many-statements + def _gui_frame_keypress(self, size, key): + """Keypress handler for the GUI""" + # convert space character into name + if key == ' ': + key = 'space' + + contents = self.gui_body_get() + + if key == self.config.get_keybind('quit'): + self._gui_switch_frame_body(None) + + elif key == self.config.get_keybind('help'): + self._gui_switch_frame_body(self.view_help) + + elif key == self.config.get_keybind('sync'): + self.ndb.last_sync = 0 + self.ndb.sync_worker_go() + + elif key == self.config.get_keybind('view_log'): + self.view_log.update_log() + self._gui_switch_frame_body(self.view_log) + + elif key == self.config.get_keybind('down'): + if not contents.body.positions(): + return None + last = len(contents.body.positions()) + if contents.focus_position == (last - 1): + return None + contents.focus_position += 1 + contents.render(size) + + elif key == self.config.get_keybind('up'): + if not contents.body.positions(): + return None + if contents.focus_position == 0: + return None + contents.focus_position -= 1 + contents.render(size) + + elif key == self.config.get_keybind('page_down'): + if not contents.body.positions(): + return None + last = len(contents.body.positions()) + next_focus = contents.focus_position + size[1] + if next_focus >= last: + next_focus = last - 1 + contents.change_focus(size, next_focus, + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('page_up'): + if not contents.body.positions(): + return None + if 'bottom' in contents.ends_visible(size): + last = len(contents.body.positions()) + next_focus = last - size[1] - size[1] + else: + next_focus = contents.focus_position - size[1] + if next_focus < 0: + next_focus = 0 + contents.change_focus(size, next_focus, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('half_page_down'): + if not contents.body.positions(): + return None + last = len(contents.body.positions()) + next_focus = contents.focus_position + (size[1] // 2) + if next_focus >= last: + next_focus = last - 1 + contents.change_focus(size, next_focus, + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('half_page_up'): + if not contents.body.positions(): + return None + if 'bottom' in contents.ends_visible(size): + last = len(contents.body.positions()) + next_focus = last - size[1] - (size[1] // 2) + else: + next_focus = contents.focus_position - (size[1] // 2) + if next_focus < 0: + next_focus = 0 + contents.change_focus(size, next_focus, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('bottom'): + if not contents.body.positions(): + return None + contents.change_focus(size, (len(contents.body.positions()) - 1), + offset_inset=0, + coming_from='above') + + elif key == self.config.get_keybind('top'): + if not contents.body.positions(): + return None + contents.change_focus(size, 0, + offset_inset=0, + coming_from='below') + + elif key == self.config.get_keybind('view_next_note'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if not self.view_titles.body.positions(): + return None + last = len(self.view_titles.body.positions()) + if self.view_titles.focus_position == (last - 1): + return None + self.view_titles.focus_position += 1 + contents.update_note_view( + self.view_titles. \ + note_list[self.view_titles. \ + focus_position].note['localkey'] + ) + self._gui_switch_frame_body(self.view_note) + + elif key == self.config.get_keybind('view_prev_note'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if not self.view_titles.body.positions(): + return None + if self.view_titles.focus_position == 0: + return None + self.view_titles.focus_position -= 1 + contents.update_note_view( + self.view_titles. \ + note_list[self.view_titles. \ + focus_position].note['localkey'] + ) + self._gui_switch_frame_body(self.view_note) + + elif key == self.config.get_keybind('status'): + if self.status_bar == 'yes': + self.status_bar = 'no' + else: + self.status_bar = self.config.get_config('status_bar') + + elif key == self.config.get_keybind('create_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self._gui_clear() + content = exec_cmd_on_note(None, self.config, self, self.logger) + self._gui_reset() + + if content: + self.log('New note created') + self.ndb.create_note(content) + self.gui_update_view() + self.ndb.sync_worker_go() + + elif key == self.config.get_keybind('edit_note') or \ + key == self.config.get_keybind('view_note_ext') or \ + key == self.config.get_keybind('view_note_json'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if not contents.body.positions(): + return None + note = contents.note_list[contents.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + if key == self.config.get_keybind('edit_note'): + note = contents.note + else: + note = contents.old_note if contents.old_note \ + else contents.note + + self._gui_clear() + if key == self.config.get_keybind('edit_note'): + content = exec_cmd_on_note(note, self.config, self, + self.logger) + elif key == self.config.get_keybind('view_note_ext'): + content = exec_cmd_on_note( + note, + self.config, + self, + self.logger, + cmd=get_pager(self.config, self.logger)) + else: # key == self.config.get_keybind('view_note_json') + content = exec_cmd_on_note( + note, + self.config, + self, + self.logger, + cmd=get_pager(self.config, self.logger), + raw=True + ) + + self._gui_reset() + + if not content: + return None + + md5_old = hashlib.md5(note['content'].encode('utf-8')).digest() + md5_new = hashlib.md5(content.encode('utf-8')).digest() + + if md5_old != md5_new: + self.log('Note updated') + self.ndb.set_note_content(note['localkey'], content) + if self.gui_body_get().__class__ == view_titles.ViewTitles: + contents.update_note_title() + else: # self.gui_body_get().__class__ == view_note.ViewNote: + contents.update_note_view() + self.ndb.sync_worker_go() + else: + self.log('Note unchanged') + + elif key == self.config.get_keybind('view_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + if not contents.body.positions(): + return None + self.view_note.update_note_view( + contents.note_list[contents.focus_position]. \ + note['localkey']) + self._gui_switch_frame_body(self.view_note) + + elif key == self.config.get_keybind('pipe_note'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if not contents.body.positions(): + return None + note = contents.note_list[contents.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = contents.old_note if contents.old_note else contents.note + + self._gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + key, + '', + self._gui_pipe_input, + None + ), + 'user_input_bar' + ) + ) + self._gui_footer_focus_input() + self.master_frame.keypress = \ + self._gui_footer_input_get().keypress + + elif key == self.config.get_keybind('note_delete'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if not contents.body.positions(): + return None + note = contents.note_list[contents.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = contents.note + + self._gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + 'Delete (y/n): ', + '', + self._gui_yes_no_input, + [ + self._delete_note_callback, + note['localkey'] + ] + ), + 'user_input_bar' + ) + ) + self._gui_footer_focus_input() + self.master_frame.keypress = \ + self._gui_footer_input_get().keypress + + elif key == self.config.get_keybind('note_favorite'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if not contents.body.positions(): + return None + note = contents.note_list[contents.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = contents.note + + favorite = not note['favorite'] + + self.ndb.set_note_favorite(note['localkey'], favorite) + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + contents.update_note_title() + + self.ndb.sync_worker_go() + + elif key == self.config.get_keybind('note_category'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_titles.ViewTitles: + if not contents.body.positions(): + return None + note = contents.note_list[contents.focus_position].note + else: # self.gui_body_get().__class__ == view_note.ViewNote: + note = contents.note + + self._gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + 'Category: ', + note['category'], + self._gui_category_input, + None + ), + 'user_input_bar' + ) + ) + self._gui_footer_focus_input() + self.master_frame.keypress = \ + self._gui_footer_input_get().keypress + + elif key == self.config.get_keybind('search_gstyle') or \ + key == self.config.get_keybind('search_regex') or \ + key == self.config.get_keybind('search_prev_gstyle') or \ + key == self.config.get_keybind('search_prev_regex'): + if self.gui_body_get().__class__ != view_titles.ViewTitles and \ + self.gui_body_get().__class__ != view_note.ViewNote: + return key + + if self.gui_body_get().__class__ == view_note.ViewNote: + if key == self.config.get_keybind('search_prev_gstyle') or \ + key == self.config.get_keybind('search_prev_regex'): + self.view_note.search_direction = 'backward' + else: + self.view_note.search_direction = 'forward' + + options = [ + 'gstyle' if key == self.config.get_keybind('search_gstyle') + or key == self.config.get_keybind('search_prev_gstyle') + else 'regex', + 'backward' if key == + self.config.get_keybind('search_prev_gstyle') + or key == self.config.get_keybind('search_prev_regex') + else 'forward' + ] + + caption = '{}{}'.format('(regex) ' + if options[0] == 'regex' + else '', + '/' if options[1] == 'forward' + else '?') + + self._gui_footer_input_set( + urwid.AttrMap( + user_input.UserInput( + self.config, + caption, + '', + self._gui_search_input, + options + ), + 'user_input_bar' + ) + ) + self._gui_footer_focus_input() + self.master_frame.keypress = \ + self._gui_footer_input_get().keypress + + elif key == self.config.get_keybind('search_next'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + self.view_note.search_note_view_next() + + elif key == self.config.get_keybind('search_prev'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + self.view_note.search_note_view_prev() + + elif key == self.config.get_keybind('clear_search'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.view_titles.update_note_list( + None, + sort_mode=self.config.state.current_sort_mode + ) + self._gui_body_set(self.view_titles) + + elif key == self.config.get_keybind('sort_date'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.config.state.current_sort_mode = 'date' + self.view_titles.sort_note_list('date') + + elif key == self.config.get_keybind('sort_alpha'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.config.state.current_sort_mode = 'alpha' + self.view_titles.sort_note_list('alpha') + + elif key == self.config.get_keybind('sort_categories'): + if self.gui_body_get().__class__ != view_titles.ViewTitles: + return key + + self.config.state.current_sort_mode = 'categories' + self.view_titles.sort_note_list('categories') + + elif key == self.config.get_keybind('copy_note_text'): + if self.gui_body_get().__class__ != view_note.ViewNote: + return key + + self.view_note.copy_note_text() + + else: + return contents.keypress(size, key) + + self._gui_update_status_bar() + return None + + def _gui_init_view(self, loop, show_note): + """Initialize the GUI""" + self.master_frame.keypress = self._gui_frame_keypress + self._gui_body_set(self.view_titles) + + if show_note: + # note that title view set first to prime the view stack + self._gui_switch_frame_body(self.view_note) + + self.thread_sync.start() + + def _gui_clear(self): + """Clear the GUI""" + self.nncli_loop.widget = urwid.Filler(urwid.Text('')) + self.nncli_loop.draw_screen() + + def _gui_reset(self): + """Reset the GUI""" + self.nncli_loop.widget = self.master_frame + self.nncli_loop.draw_screen() + + def _gui_stop(self): + """Stop the GUI""" + # don't exit if there are any notes not yet saved to the disk + + # NOTE: this was originally causing hangs on exit with urllib2 + # should not be a problem now since using the requests library + # ref https://github.com/insanum/sncli/issues/18#issuecomment-105517773 + if self.ndb.verify_all_saved(): + # clear the screen and exit the urwid run loop + self._gui_clear() + raise urwid.ExitMainLoop() + else: + self.log('WARNING: Not all notes saved' + 'to disk (wait for sync worker)') + + def log(self, msg): + """Log as message, displaying to the user as appropriate""" + self.logger.log(msg) + + self.log_lock.acquire() + + self.log_alarms += 1 + self.logs.append(msg) + + if len(self.logs) > int(self.config.get_config('max_logs')): + self.log_alarms -= 1 + self.logs.pop(0) + + log_pile = [] + for log in self.logs: + log_pile.append(urwid.AttrMap(urwid.Text(log), 'log')) + + if self.config.state.verbose: + self._gui_footer_log_set(log_pile) + + self.nncli_loop.set_alarm_in( + int(self.config.get_config('log_timeout')), + self._log_timeout, None) + + self.log_lock.release() + + def _log_timeout(self, loop, arg): + """ + Run periodically to check for new log entries to append to + the GUI footer + """ + self.log_lock.acquire() + + self.log_alarms -= 1 + + if self.log_alarms == 0: + self._gui_footer_log_clear() + self.logs = [] + else: + # for some reason having problems with this being empty? + if not self.logs: + self.logs.pop(0) + + log_pile = [] + + for log in self.logs: + log_pile.append(urwid.AttrMap(urwid.Text(log), 'log')) + + if self.config.state.verbose: + self._gui_footer_log_set(log_pile) + + self.log_lock.release() diff --git a/nncli/log.py b/nncli/log.py new file mode 100644 index 0000000..b0d9a68 --- /dev/null +++ b/nncli/log.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""log module""" +import logging +from logging.handlers import RotatingFileHandler + +import os + +# pylint: disable=unused-argument, too-few-public-methods +class Logger: + """Handles logging for the application""" + def __init__(self, config): + self.config = config + self.logfile = os.path.join( + config.get_config('db_path'), + 'nncli.log' + ) + self.loghandler = RotatingFileHandler( + self.logfile, + maxBytes=100000, + backupCount=1 + ) + self.loghandler.setLevel(logging.DEBUG) + self.loghandler.setFormatter( + logging.Formatter( + fmt='%(asctime)s [%(levelname)s] %(message)s' + ) + ) + self.logger = logging.getLogger() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.loghandler) + + logging.debug('nncli logging initialized') + + def log(self, msg): + """Log as message, displaying to the user as appropriate""" + logging.debug(msg) + + if not self.config.state.do_gui: + if self.config.state.verbose: + print(msg) diff --git a/nncli/nextcloud_note.py b/nncli/nextcloud_note.py index d74f465..d4c5b6c 100644 --- a/nncli/nextcloud_note.py +++ b/nncli/nextcloud_note.py @@ -1,22 +1,13 @@ # -*- coding: utf-8 -*- - -from requests.exceptions import RequestException, ConnectionError -import time -import datetime +"""nextcloud_note module""" import logging -import requests +import time import traceback -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - # For Google AppEngine - from django.utils import simplejson as json +import requests +from requests.exceptions import RequestException -class NextcloudNote(object): +class NextcloudNote: """ Class for interacting with the NextCloud Notes web service """ def __init__(self, username, password, host): @@ -49,14 +40,14 @@ class NextcloudNote(object): res.raise_for_status() note = res.json() self.status = 'online' - except ConnectionError as e: + except ConnectionError as ex: self.status = 'offline, connection error' - return e, -1 - except RequestException as e: + return ex, -1 + except RequestException as ex: # logging.debug('RESPONSE ERROR: ' + str(e)) - return e, -1 - except ValueError as e: - return e, -1 + return ex, -1 + except ValueError as ex: + return ex, -1 # # use UTF-8 encoding # note["content"] = note["content"].encode('utf-8') @@ -96,27 +87,32 @@ class NextcloudNote(object): #logging.debug('REQUEST: ' + url + ' - ' + str(note)) try: - logging.debug('NOTE: ' + str(note)) + logging.debug('NOTE: %s', note) if url != self.url: - res = requests.put(url, auth=(self.username, - self.password), json=note) + res = requests.put( + url, + auth=(self.username, self.password), + json=note + ) else: - res = requests.post(url, auth=(self.username, - self.password), json=note) + res = requests.post( + url, auth=(self.username, self.password), + json=note + ) note = res.json() res.raise_for_status() - logging.debug('NOTE (from response): ' + str(res.json())) + logging.debug('NOTE (from response): %s', res.json()) self.status = 'online' - except ConnectionError as e: + except ConnectionError as ex: self.status = 'offline, connection error' - return e, -1 - except RequestException as e: - logging.debug('RESPONSE ERROR: ' + str(e)) + raise ex + except RequestException as ex: + logging.debug('RESPONSE ERROR: %s', ex) logging.debug(traceback.print_exc()) self.status = 'error updating note, check log' - return e, -1 - except ValueError as e: - return e, -1 + raise ex + except ValueError as ex: + raise ex #logging.debug('RESPONSE OK: ' + str(note)) return note, 0 @@ -149,22 +145,25 @@ class NextcloudNote(object): # perform initial HTTP request try: - logging.debug('REQUEST: ' + self.url + \ - '?exclude=content') - res = requests.get(self.url, auth=(self.username, self.password), params=params) + logging.debug('REQUEST: %s', self.url + '?exclude=content') + res = requests.get( + self.url, + auth=(self.username, self.password), + params=params + ) res.raise_for_status() #logging.debug('RESPONSE OK: ' + str(res)) note_list = res.json() self.status = 'online' - except ConnectionError as e: + except ConnectionError: logging.exception('connection error') self.status = 'offline, connection error' status = -1 - except RequestException as e: + except RequestException: # if problem with network request/response logging.exception('request error') status = -1 - except ValueError as e: + except ValueError: # if invalid json data status = -1 logging.exception('request returned bad JSON data') @@ -194,13 +193,13 @@ class NextcloudNote(object): url = '{}/{}'.format(self.url, str(note['id'])) try: - logging.debug('REQUEST DELETE: ' + url) + logging.debug('REQUEST DELETE: %s', url) res = requests.delete(url, auth=(self.username, self.password)) res.raise_for_status() self.status = 'online' - except ConnectionError as e: + except ConnectionError as ex: self.status = 'offline, connection error' - return e, -1 - except RequestException as e: - return e, -1 + raise ex + except RequestException as ex: + raise ex return {}, 0 diff --git a/nncli/nncli.py b/nncli/nncli.py index 83fa714..03c5cfd 100644 --- a/nncli/nncli.py +++ b/nncli/nncli.py @@ -1,980 +1,167 @@ # -*- coding: utf-8 -*- - -import os, sys, getopt, re, signal, time, datetime, shlex, hashlib -import subprocess, threading, logging -import copy, json, urwid, datetime -from . import view_titles, view_note, view_help, view_log, user_input -from . import utils, temp, __version__ +"""nncli module""" +import hashlib +import json +import os +import signal +import sys +import time + +from . import utils, __version__ from .config import Config -from .nextcloud_note import NextcloudNote +from .gui import NncliGui +from .log import Logger from .notes_db import NotesDB, ReadError, WriteError -from logging.handlers import RotatingFileHandler +from .utils import exec_cmd_on_note +# pylint: disable=unused-argument class Nncli: - + """Nncli class. Responsible for most of the application logic""" def __init__(self, do_server_sync, verbose=False, config_file=None): - self.config = Config(config_file) - self.do_server_sync = do_server_sync - self.verbose = verbose - self.do_gui = False - force_full_sync = False - self.current_sort_mode = self.config.get_config('sort_mode') - - self.tempdir = self.config.get_config('tempdir') - if self.tempdir == '': - self.tempdir = None + self.config = Config(config_file) + self.config.state.do_server_sync = do_server_sync + self.config.state.verbose = verbose + force_full_sync = False if not os.path.exists(self.config.get_config('db_path')): os.mkdir(self.config.get_config('db_path')) force_full_sync = True - # configure the logging module - self.logfile = os.path.join(self.config.get_config('db_path'), 'nncli.log') - self.loghandler = RotatingFileHandler(self.logfile, maxBytes=100000, backupCount=1) - self.loghandler.setLevel(logging.DEBUG) - self.loghandler.setFormatter(logging.Formatter(fmt='%(asctime)s [%(levelname)s] %(message)s')) - self.logger = logging.getLogger() - self.logger.setLevel(logging.DEBUG) - self.logger.addHandler(self.loghandler) - self.config.logfile = self.logfile - - logging.debug('nncli logging initialized') - - self.logs = [] + self.logger = Logger(self.config) try: - self.ndb = NotesDB(self.config, self.log, self.gui_update_view) - except Exception as e: - self.log(str(e)) + self.ndb = NotesDB( + self.config, + self.logger.log + ) + except (ReadError, WriteError) as ex: + self.logger.log(str(ex)) sys.exit(1) + self.nncli_gui = NncliGui(self.config, self.logger, self.ndb) + self.ndb.set_update_view(self.nncli_gui.gui_update_view) + if force_full_sync: # The note database doesn't exist so force a full sync. It is # important to do this outside of the gui because an account # with hundreds of notes will cause a recursion panic under # urwid. This simple workaround gets the job done. :-) - self.verbose = True - self.log('nncli database doesn\'t exist, forcing full sync...') - self.sync_notes() - self.verbose = verbose - - def sync_notes(self): - self.ndb.sync_now(self.do_server_sync) - - def get_editor(self): - editor = self.config.get_config('editor') - if not editor: - self.log('No editor configured!') - return None - return editor - - def get_pager(self): - pager = self.config.get_config('pager') - if not pager: - self.log('No pager configured!') - return None - return pager - - def exec_cmd_on_note(self, note, cmd=None, raw=False): - - if not cmd: - cmd = self.get_editor() - if not cmd: - return None - - tf = temp.tempfile_create(note if note else None, raw=raw, tempdir=self.tempdir) - fname = temp.tempfile_name(tf) - - focus_position = 0 - try: - focus_position = self.gui_body_get().focus_position - except IndexError: - # focus position will fail if no notes available (listbox empty) - # TODO: find a neater way to check than try/except - pass - except AttributeError: - # we're running in CLI mode - pass - - subs = { - 'fname': fname, - 'line': focus_position + 1, - } - cmd_list = [c.format(**subs) for c in shlex.split(cmd)] - - # if the filename wasn't able to be subbed, append it - # this makes it fully backwards compatible with previous configs - if '{fname}' not in cmd: - cmd_list.append(fname) - - self.log("EXECUTING: {}".format(cmd_list)) - - try: - subprocess.check_call(cmd_list) - except Exception as e: - self.log('Command error: ' + str(e)) - temp.tempfile_delete(tf) - return None - - content = None - if not raw: - content = temp.tempfile_content(tf) - if not content or content == '\n': - content = None - - temp.tempfile_delete(tf) - - if self.do_gui: - self.nncli_loop.screen.clear() - self.nncli_loop.draw_screen() - - return content - - def gui_header_clear(self): - self.master_frame.contents['header'] = ( None, None ) - self.nncli_loop.draw_screen() - - def gui_header_set(self, w): - self.master_frame.contents['header'] = ( w, None ) - self.nncli_loop.draw_screen() - - def gui_footer_log_clear(self): - ui = self.gui_footer_input_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([]), urwid.Pile([ui]) ]), None) - self.nncli_loop.draw_screen() - - def gui_footer_log_set(self, pl): - ui = self.gui_footer_input_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile(pl), urwid.Pile([ui]) ]), None) - self.nncli_loop.draw_screen() - - def gui_footer_log_get(self): - return self.master_frame.contents['footer'][0].contents[0][0] - - def gui_footer_input_clear(self): - pl = self.gui_footer_log_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([]) ]), None) - self.nncli_loop.draw_screen() - - def gui_footer_input_set(self, ui): - pl = self.gui_footer_log_get() - self.master_frame.contents['footer'] = \ - (urwid.Pile([ urwid.Pile([pl]), urwid.Pile([ui]) ]), None) - self.nncli_loop.draw_screen() - - def gui_footer_input_get(self): - return self.master_frame.contents['footer'][0].contents[1][0] - - def gui_footer_focus_input(self): - self.master_frame.focus_position = 'footer' - self.master_frame.contents['footer'][0].focus_position = 1 - - def gui_body_set(self, w): - self.master_frame.contents['body'] = ( w, None ) - self.gui_update_status_bar() - self.nncli_loop.draw_screen() - - def gui_body_get(self): - return self.master_frame.contents['body'][0] - - def gui_body_focus(self): - self.master_frame.focus_position = 'body' - - def log_timeout(self, loop, arg): - self.log_lock.acquire() - - self.log_alarms -= 1 - - if self.log_alarms == 0: - self.gui_footer_log_clear() - self.logs = [] - else: - # for some reason having problems with this being empty? - if len(self.logs) > 0: - self.logs.pop(0) - - log_pile = [] - - for l in self.logs: - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) - - if self.verbose: - self.gui_footer_log_set(log_pile) - - self.log_lock.release() - - def log(self, msg): - logging.debug(msg) - - if not self.do_gui: - if self.verbose: - print(msg) - return - - self.log_lock.acquire() - - self.log_alarms += 1 - self.logs.append(msg) - - if len(self.logs) > int(self.config.get_config('max_logs')): - self.log_alarms -= 1 - self.logs.pop(0) - - log_pile = [] - for l in self.logs: - log_pile.append(urwid.AttrMap(urwid.Text(l), 'log')) - - if self.verbose: - self.gui_footer_log_set(log_pile) - - self.nncli_loop.set_alarm_in( - int(self.config.get_config('log_timeout')), - self.log_timeout, None) - - self.log_lock.release() - - def gui_update_view(self): - if not self.do_gui: - return - - try: - cur_key = self.view_titles.note_list[self.view_titles.focus_position].note['localkey'] - except IndexError as e: - cur_key = None - pass - - self.view_titles.update_note_list(self.view_titles.search_string, sort_mode=self.current_sort_mode) - self.view_titles.focus_note(cur_key) - - if self.gui_body_get().__class__ == view_note.ViewNote: - self.view_note.update_note_view() - - self.gui_update_status_bar() - - def gui_update_status_bar(self): - if self.status_bar != 'yes': - self.gui_header_clear() - else: - self.gui_header_set(self.gui_body_get().get_status_bar()) - - def gui_switch_frame_body(self, new_view, save_current_view=True): - if new_view == None: - if len(self.last_view) == 0: - # XXX verify all notes saved... - self.gui_stop() - else: - self.gui_body_set(self.last_view.pop()) - else: - if self.gui_body_get().__class__ != new_view.__class__: - if save_current_view: - self.last_view.append(self.gui_body_get()) - self.gui_body_set(new_view) - - def delete_note_callback(self, key, delete): - if not delete: - return - note = self.ndb.get_note(key) - self.ndb.set_note_deleted(key, True) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - self.view_titles.update_note_title() - - self.gui_update_status_bar() - self.ndb.sync_worker_go() - - def gui_yes_no_input(self, args, yes_no): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - args[0](args[1], - True if yes_no in [ 'YES', 'Yes', 'yes', 'Y', 'y' ] - else False) - - def gui_search_input(self, args, search_string): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if search_string: - if (self.gui_body_get() == self.view_note): - self.search_direction = args[1] - self.view_note.search_note_view_next(search_string=search_string, search_mode=args[0]) - else: - self.view_titles.update_note_list(search_string, args[0], sort_mode=self.current_sort_mode) - self.gui_body_set(self.view_titles) - - def gui_category_input(self, args, category): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if category != None: - if self.gui_body_get().__class__ == view_titles.ViewTitles: - note = self.view_titles.note_list[self.view_titles.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = self.view_note.note - - self.ndb.set_note_category(note['localkey'], category) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - self.view_titles.update_note_title() - else: # self.gui_body_get().__class__ == view_note.ViewNote: - self.view_note.update_note_view() - - self.gui_update_status_bar() - self.ndb.sync_worker_go() - - def gui_pipe_input(self, args, cmd): - self.gui_footer_input_clear() - self.gui_body_focus() - self.master_frame.keypress = self.gui_frame_keypress - if cmd != None: - if self.gui_body_get().__class__ == view_titles.ViewTitles: - note = self.view_titles.note_list[self.view_titles.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = self.view_note.old_note if self.view_note.old_note \ - else self.view_note.note - args = shlex.split(cmd) - try: - self.gui_clear() - pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) - pipe.communicate(note['content'].encode('utf-8')) - pipe.stdin.close() - pipe.wait() - except OSError as e: - self.log('Pipe error: ' + str(e)) - finally: - self.gui_reset() - - def gui_frame_keypress(self, size, key): - # convert space character into name - if key == ' ': - key = 'space' - - lb = self.gui_body_get() - - if key == self.config.get_keybind('quit'): - self.gui_switch_frame_body(None) - - elif key == self.config.get_keybind('help'): - self.gui_switch_frame_body(self.view_help) - - elif key == self.config.get_keybind('sync'): - self.ndb.last_sync = 0 - self.ndb.sync_worker_go() - - elif key == self.config.get_keybind('view_log'): - self.view_log.update_log() - self.gui_switch_frame_body(self.view_log) - - elif key == self.config.get_keybind('down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - if lb.focus_position == (last - 1): - return None - lb.focus_position += 1 - lb.render(size) - - elif key == self.config.get_keybind('up'): - if len(lb.body.positions()) <= 0: - return None - if lb.focus_position == 0: - return None - lb.focus_position -= 1 - lb.render(size) - - elif key == self.config.get_keybind('page_down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - next_focus = lb.focus_position + size[1] - if next_focus >= last: - next_focus = last - 1 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('page_up'): - if len(lb.body.positions()) <= 0: - return None - if 'bottom' in lb.ends_visible(size): - last = len(lb.body.positions()) - next_focus = last - size[1] - size[1] - else: - next_focus = lb.focus_position - size[1] - if next_focus < 0: - next_focus = 0 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('half_page_down'): - if len(lb.body.positions()) <= 0: - return None - last = len(lb.body.positions()) - next_focus = lb.focus_position + (size[1] // 2) - if next_focus >= last: - next_focus = last - 1 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('half_page_up'): - if len(lb.body.positions()) <= 0: - return None - if 'bottom' in lb.ends_visible(size): - last = len(lb.body.positions()) - next_focus = last - size[1] - (size[1] // 2) - else: - next_focus = lb.focus_position - (size[1] // 2) - if next_focus < 0: - next_focus = 0 - lb.change_focus(size, next_focus, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('bottom'): - if len(lb.body.positions()) <= 0: - return None - lb.change_focus(size, (len(lb.body.positions()) - 1), - offset_inset=0, - coming_from='above') - - elif key == self.config.get_keybind('top'): - if len(lb.body.positions()) <= 0: - return None - lb.change_focus(size, 0, - offset_inset=0, - coming_from='below') - - elif key == self.config.get_keybind('view_next_note'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if len(self.view_titles.body.positions()) <= 0: - return None - last = len(self.view_titles.body.positions()) - if self.view_titles.focus_position == (last - 1): - return None - self.view_titles.focus_position += 1 - lb.update_note_view( - self.view_titles.note_list[self.view_titles.focus_position].note['localkey']) - self.gui_switch_frame_body(self.view_note) - - elif key == self.config.get_keybind('view_prev_note'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if len(self.view_titles.body.positions()) <= 0: - return None - if self.view_titles.focus_position == 0: - return None - self.view_titles.focus_position -= 1 - lb.update_note_view( - self.view_titles.note_list[self.view_titles.focus_position].note['localkey']) - self.gui_switch_frame_body(self.view_note) - - elif key == self.config.get_keybind('status'): - if self.status_bar == 'yes': - self.status_bar = 'no' - else: - self.status_bar = self.config.get_config('status_bar') - - elif key == self.config.get_keybind('create_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.gui_clear() - content = self.exec_cmd_on_note(None) - self.gui_reset() - - if content: - self.log('New note created') - self.ndb.create_note(content) - self.gui_update_view() - self.ndb.sync_worker_go() - - elif key == self.config.get_keybind('edit_note') or \ - key == self.config.get_keybind('view_note_ext') or \ - key == self.config.get_keybind('view_note_json'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - if key == self.config.get_keybind('edit_note'): - note = lb.note - else: - note = lb.old_note if lb.old_note else lb.note - - self.gui_clear() - if key == self.config.get_keybind('edit_note'): - content = self.exec_cmd_on_note(note) - elif key == self.config.get_keybind('view_note_ext'): - content = self.exec_cmd_on_note(note, cmd=self.get_pager()) - else: # key == self.config.get_keybind('view_note_json') - content = self.exec_cmd_on_note(note, cmd=self.get_pager(), raw=True) - - self.gui_reset() - - if not content: - return None - - md5_old = hashlib.md5(note['content'].encode('utf-8')).digest() - md5_new = hashlib.md5(content.encode('utf-8')).digest() - - if md5_old != md5_new: - self.log('Note updated') - self.ndb.set_note_content(note['localkey'], content) - if self.gui_body_get().__class__ == view_titles.ViewTitles: - lb.update_note_title() - else: # self.gui_body_get().__class__ == view_note.ViewNote: - lb.update_note_view() - self.ndb.sync_worker_go() - else: - self.log('Note unchanged') - - elif key == self.config.get_keybind('view_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - if len(lb.body.positions()) <= 0: - return None - self.view_note.update_note_view( - lb.note_list[lb.focus_position].note['localkey']) - self.gui_switch_frame_body(self.view_note) - - elif key == self.config.get_keybind('pipe_note'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.old_note if lb.old_note else lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - key, - '', - self.gui_pipe_input, - None), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('note_delete'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - 'Delete (y/n): ', - '', - self.gui_yes_no_input, - [ self.delete_note_callback, note['localkey'] ]), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('note_favorite'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - favorite = not note['favorite'] - - self.ndb.set_note_favorite(note['localkey'], favorite) - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - lb.update_note_title() - - self.ndb.sync_worker_go() - - elif key == self.config.get_keybind('note_category'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_titles.ViewTitles: - if len(lb.body.positions()) <= 0: - return None - note = lb.note_list[lb.focus_position].note - else: # self.gui_body_get().__class__ == view_note.ViewNote: - note = lb.note - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - 'Category: ', - note['category'], - self.gui_category_input, - None), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('search_gstyle') or \ - key == self.config.get_keybind('search_regex') or \ - key == self.config.get_keybind('search_prev_gstyle') or \ - key == self.config.get_keybind('search_prev_regex'): - if self.gui_body_get().__class__ != view_titles.ViewTitles and \ - self.gui_body_get().__class__ != view_note.ViewNote: - return key - - if self.gui_body_get().__class__ == view_note.ViewNote: - if key == self.config.get_keybind('search_prev_gstyle') or \ - key == self.config.get_keybind('search_prev_regex'): - self.view_note.search_direction = 'backward' - else: - self.view_note.search_direction = 'forward' - - options = [ - 'gstyle' if key == self.config.get_keybind('search_gstyle') - or key == self.config.get_keybind('search_prev_gstyle') - else 'regex', - 'backward' if key == self.config.get_keybind('search_prev_gstyle') - or key == self.config.get_keybind('search_prev_regex') - else 'forward' - ] - - caption = '{}{}'.format('(regex) ' if options[0] == 'regex' else '', '/' if options[1] == 'forward' else '?') - - self.gui_footer_input_set( - urwid.AttrMap( - user_input.UserInput( - self.config, - caption, - '', - self.gui_search_input, - options), - 'user_input_bar')) - self.gui_footer_focus_input() - self.master_frame.keypress = self.gui_footer_input_get().keypress - - elif key == self.config.get_keybind('search_next'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - self.view_note.search_note_view_next() - - elif key == self.config.get_keybind('search_prev'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - self.view_note.search_note_view_prev() - - elif key == self.config.get_keybind('clear_search'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.view_titles.update_note_list(None, sort_mode=self.current_sort_mode) - self.gui_body_set(self.view_titles) - - elif key == self.config.get_keybind('sort_date'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.current_sort_mode = 'date' - self.view_titles.sort_note_list('date') - - elif key == self.config.get_keybind('sort_alpha'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.current_sort_mode = 'alpha' - self.view_titles.sort_note_list('alpha') - - elif key == self.config.get_keybind('sort_categories'): - if self.gui_body_get().__class__ != view_titles.ViewTitles: - return key - - self.current_sort_mode = 'categories' - self.view_titles.sort_note_list('categories') - - elif key == self.config.get_keybind('copy_note_text'): - if self.gui_body_get().__class__ != view_note.ViewNote: - return key - - self.view_note.copy_note_text() - - else: - return lb.keypress(size, key) - - self.gui_update_status_bar() - return None - - def gui_init_view(self, loop, view_note): - self.master_frame.keypress = self.gui_frame_keypress - self.gui_body_set(self.view_titles) - - if view_note: - # note that title view set first to prime the view stack - self.gui_switch_frame_body(self.view_note) - - self.thread_sync.start() - - def gui_clear(self): - self.nncli_loop.widget = urwid.Filler(urwid.Text('')) - self.nncli_loop.draw_screen() - - def gui_reset(self): - self.nncli_loop.widget = self.master_frame - self.nncli_loop.draw_screen() - - def gui_stop(self): - # don't exit if there are any notes not yet saved to the disk - - # NOTE: this was originally causing hangs on exit with urllib2 - # should not be a problem now since using the requests library - # ref https://github.com/insanum/sncli/issues/18#issuecomment-105517773 - if self.ndb.verify_all_saved(): - # clear the screen and exit the urwid run loop - self.gui_clear() - raise urwid.ExitMainLoop() - else: - self.log(u'WARNING: Not all notes saved to disk (wait for sync worker)') + self.config.state.verbose = True + self.logger.log('nncli database doesn\'t exist,' + ' forcing full sync...') + self.ndb.sync_now() + self.config.state.verbose = verbose def gui(self, key): - - self.do_gui = True - - self.last_view = [] - self.status_bar = self.config.get_config('status_bar') - - self.log_alarms = 0 - self.log_lock = threading.Lock() - - self.thread_sync = threading.Thread(target=self.ndb.sync_worker, - args=[self.do_server_sync]) - self.thread_sync.setDaemon(True) - - self.view_titles = \ - view_titles.ViewTitles(self.config, - { - 'ndb' : self.ndb, - 'search_string' : None, - 'log' : self.log - }) - self.view_note = \ - view_note.ViewNote(self.config, - { - 'ndb' : self.ndb, - 'id' : key, # initial key to view or None - 'log' : self.log - }) - - self.view_log = view_log.ViewLog(self.config) - self.view_help = view_help.ViewHelp(self.config) - - palette = \ - [ - ('default', - self.config.get_color('default_fg'), - self.config.get_color('default_bg') ), - ('status_bar', - self.config.get_color('status_bar_fg'), - self.config.get_color('status_bar_bg') ), - ('log', - self.config.get_color('log_fg'), - self.config.get_color('log_bg') ), - ('user_input_bar', - self.config.get_color('user_input_bar_fg'), - self.config.get_color('user_input_bar_bg') ), - ('note_focus', - self.config.get_color('note_focus_fg'), - self.config.get_color('note_focus_bg') ), - ('note_title_day', - self.config.get_color('note_title_day_fg'), - self.config.get_color('note_title_day_bg') ), - ('note_title_week', - self.config.get_color('note_title_week_fg'), - self.config.get_color('note_title_week_bg') ), - ('note_title_month', - self.config.get_color('note_title_month_fg'), - self.config.get_color('note_title_month_bg') ), - ('note_title_year', - self.config.get_color('note_title_year_fg'), - self.config.get_color('note_title_year_bg') ), - ('note_title_ancient', - self.config.get_color('note_title_ancient_fg'), - self.config.get_color('note_title_ancient_bg') ), - ('note_date', - self.config.get_color('note_date_fg'), - self.config.get_color('note_date_bg') ), - ('note_flags', - self.config.get_color('note_flags_fg'), - self.config.get_color('note_flags_bg') ), - ('note_category', - self.config.get_color('note_category_fg'), - self.config.get_color('note_category_bg') ), - ('note_content', - self.config.get_color('note_content_fg'), - self.config.get_color('note_content_bg') ), - ('note_content_focus', - self.config.get_color('note_content_focus_fg'), - self.config.get_color('note_content_focus_bg') ), - ('note_content_old', - self.config.get_color('note_content_old_fg'), - self.config.get_color('note_content_old_bg') ), - ('note_content_old_focus', - self.config.get_color('note_content_old_focus_fg'), - self.config.get_color('note_content_old_focus_bg') ), - ('help_focus', - self.config.get_color('help_focus_fg'), - self.config.get_color('help_focus_bg') ), - ('help_header', - self.config.get_color('help_header_fg'), - self.config.get_color('help_header_bg') ), - ('help_config', - self.config.get_color('help_config_fg'), - self.config.get_color('help_config_bg') ), - ('help_value', - self.config.get_color('help_value_fg'), - self.config.get_color('help_value_bg') ), - ('help_descr', - self.config.get_color('help_descr_fg'), - self.config.get_color('help_descr_bg') ) - ] - - self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text('')), - header=None, - footer=urwid.Pile([ urwid.Pile([]), - urwid.Pile([]) ]), - focus_part='body') - - self.nncli_loop = urwid.MainLoop(self.master_frame, - palette, - handle_mouse=False) - - self.nncli_loop.set_alarm_in(0, self.gui_init_view, - True if key else False) - - self.nncli_loop.run() + """Method to initialize and display the GUI""" + self.config.state.do_gui = True + self.ndb.log = self.nncli_gui.log + self.nncli_gui.run() def cli_list_notes(self, regex, search_string): - - note_list, match_regex, all_notes_cnt = \ + """List the notes on the command line""" + note_list, _, _ = \ self.ndb.filter_notes( search_string, search_mode='regex' if regex else 'gstyle', sort_mode=self.config.get_config('sort_mode')) - for n in note_list: - flags = utils.get_note_flags(n.note) - print((str(n.key) + \ + for nnote in note_list: + flags = utils.get_note_flags(nnote.note) + print((str(nnote.key) + \ ' [' + flags + '] ' + \ - utils.get_note_title(n.note))) + utils.get_note_title(nnote.note))) def cli_note_dump(self, key): - + """Dump a note to the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') + self.logger.log('ERROR: Key does not exist') return - w = 60 - sep = '+' + '-'*(w+2) + '+' - t = time.localtime(float(note['modified'])) - mod_time = time.strftime('%a, %d %b %Y %H:%M:%S', t) + width = 60 + sep = '+' + '-' * (width + 2) + '+' + localtime = time.localtime(float(note['modified'])) + mod_time = time.strftime('%a, %d %b %Y %H:%M:%S', localtime) title = utils.get_note_title(note) flags = utils.get_note_flags(note) - category = utils.get_note_category(note) + category = utils.get_note_category(note) print(sep) - print(('| {:<' + str(w) + '} |').format((' Title: ' + title)[:w])) - print(('| {:<' + str(w) + '} |').format((' Key: ' + str(note.get('id', 'Localkey: {}'.format(note.get('localkey'))))[:w]))) - print(('| {:<' + str(w) + '} |').format((' Date: ' + mod_time)[:w])) - print(('| {:<' + str(w) + '} |').format((' Category: ' + category)[:w])) - print(('| {:<' + str(w) + '} |').format((' Flags: [' + flags + ']')[:w])) + print(('| {:<' + str(width) + '} |').format( + (' Title: ' + title)[:width])) + print(('| {:<' + str(width) + '} |').format( + (' Key: ' + + str(note.get( + 'id', + 'Localkey: {}'.format(note.get('localkey')) + ) + )[:width] + ))) + print(('| {:<' + str(width) + '} |').format( + (' Date: ' + mod_time)[:width])) + print(('| {:<' + str(width) + '} |').format( + (' Category: ' + category)[:width])) + print(('| {:<' + str(width) + '} |').format( + (' Flags: [' + flags + ']')[:width])) print(sep) print((note['content'])) def cli_dump_notes(self, regex, search_string): - - note_list, match_regex, all_notes_cnt = \ + """Dump multiple notes to the command line""" + note_list, _, _ = \ self.ndb.filter_notes( search_string, search_mode='regex' if regex else 'gstyle', sort_mode=self.config.get_config('sort_mode')) - for n in note_list: - self.cli_note_dump(n.key) + for note in note_list: + self.cli_note_dump(note.key) def cli_note_create(self, from_stdin, title): - + """Create a new note from the command line""" if from_stdin: content = ''.join(sys.stdin) else: - content = self.exec_cmd_on_note(None) + content = exec_cmd_on_note(None, self.config, self.nncli_gui, + self.logger) if title: content = title + '\n\n' + content if content else '' if content: - self.log('New note created') + self.logger.log('New note created') self.ndb.create_note(content) - self.sync_notes() + self.ndb.sync_now() def cli_note_import(self, from_stdin): - + """Import a note from the command line""" if from_stdin: raw = ''.join(sys.stdin) else: - raw = self.exec_cmd_on_note(None) + raw = exec_cmd_on_note(None, self.config, self.nncli_gui, + self.logger) if raw: try: note = json.loads(raw) - self.log('New note created') + self.logger.log('New note created') self.ndb.import_note(note) - self.sync_notes() - except json.decoder.JSONDecodeError as e: - self.log('(IMPORT) Decoding JSON has failed: {}'.format(e)) - sys.exit(1) - except ValueError as e: - self.log('(IMPORT) ValueError: {}'.format(e)) + self.ndb.sync_now() + except ValueError as ex: + self.logger.log('(IMPORT) ValueError: {}'.format(ex)) sys.exit(1) def cli_note_export(self, key): - + """Export a note to the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') + self.logger.log('ERROR: Key does not exist') return print(json.dumps(note, indent=2)) def cli_export_notes(self, regex, search_string): - - note_list, match_regex, all_notes_cnt = \ + """Export multiple notes to the command line""" + note_list, _, _ = \ self.ndb.filter_notes( search_string, search_mode='regex' if regex else 'gstyle', @@ -984,13 +171,14 @@ class Nncli: print(json.dumps(notes_data, indent=2)) def cli_note_edit(self, key): - + """Edit a note from the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') + self.logger.log('ERROR: Key does not exist') return - content = self.exec_cmd_on_note(note) + content = exec_cmd_on_note(note, self.config, self.nncli_gui, + self.logger) if not content: return @@ -998,65 +186,66 @@ class Nncli: md5_new = hashlib.md5(content.encode('utf-8')).digest() if md5_old != md5_new: - self.log('Note updated') + self.logger.log('Note updated') self.ndb.set_note_content(note['localkey'], content) - self.sync_notes() + self.ndb.sync_now() else: - self.log('Note unchanged') + self.logger.log('Note unchanged') def cli_note_delete(self, key, delete): - + """Delete a note from the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') + self.logger.log('ERROR: Key does not exist') return self.ndb.set_note_deleted(key, delete) - self.sync_notes() + self.ndb.sync_now() def cli_note_favorite(self, key, favorite): - + """Favorite a note from the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') + self.logger.log('ERROR: Key does not exist') return self.ndb.set_note_favorite(key, favorite) - self.sync_notes() + self.ndb.sync_now() def cli_note_category_get(self, key): - + """Get a note category from the command line""" note = self.ndb.get_note(key) if not note: - self.log('ERROR: Key does not exist') - return + self.logger.log('ERROR: Key does not exist') + return '' category = utils.get_note_category(note) return category def cli_note_category_set(self, key, category): - + """Set a note category from the command line""" note = self.ndb.get_note(key) if not note: - self.log('Error: Key does not exist') + self.logger.log('Error: Key does not exist') return self.ndb.set_note_category(key, category.lower()) - self.sync_notes() + self.ndb.sync_now() def cli_note_category_rm(self, key): - + """Remove a note category from the command line""" note = self.ndb.get_note(key) if not note: - self.log('Error: Key does not exist') + self.logger.log('Error: Key does not exist') return old_category = self.cli_note_category_get(key) if old_category: self.cli_note_category_set(key, '') -def SIGINT_handler(signum, frame): +def sigint_handler(signum, frame): + """Handle sigint""" print('\nSignal caught, bye!') sys.exit(1) -signal.signal(signal.SIGINT, SIGINT_handler) +signal.signal(signal.SIGINT, sigint_handler) diff --git a/nncli/notes_db.py b/nncli/notes_db.py index 99aa058..0cc2a30 100644 --- a/nncli/notes_db.py +++ b/nncli/notes_db.py @@ -1,27 +1,40 @@ # -*- coding: utf-8 -*- +"""notes_db module""" +import copy +import glob +import json +import os +import re +import threading +import time +from requests.exceptions import RequestException -import os, time, re, glob, json, copy, threading from . import utils from .nextcloud_note import NextcloudNote -import logging +# pylint: disable=too-many-instance-attributes, too-many-locals +# pylint: disable=too-many-branches, too-many-statements class ReadError(RuntimeError): + """Exception thrown on a read error""" pass class WriteError(RuntimeError): + """Exception thrown on a write error""" pass class NotesDB(): - """NotesDB will take care of the local notes database and syncing with SN. """ - def __init__(self, config, log, update_view): - self.config = config - self.log = log + NotesDB will take care of the local notes database and syncing with + NextCloud Notes + """ + def __init__(self, config, log, update_view=None): + self.config = config + self.log = log self.update_view = update_view self.last_sync = 0 # set to zero to trigger a full sync self.sync_lock = threading.Lock() - self.go_cond = threading.Condition() + self.go_cond = threading.Condition() # create db dir if it does not exist if not os.path.exists(self.config.get_config('db_path')): @@ -29,32 +42,35 @@ class NotesDB(): now = int(time.time()) # now read all .json files from disk - fnlist = glob.glob(self.helper_key_to_fname('*')) + fnlist = glob.glob(self._helper_key_to_fname('*')) self.notes = {} - for fn in fnlist: + for func in fnlist: try: - n = json.load(open(fn, 'r')) - except IOError as e: - raise ReadError ('Error opening {0}: {1}'.format(fn, str(e))) - except ValueError as e: - raise ReadError ('Error reading {0}: {1}'.format(fn, str(e))) + note = json.load(open(func, 'r')) + except IOError as ex: + raise ReadError('Error opening {0}: {1}'.format(func, str(ex))) + except ValueError as ex: + raise ReadError('Error reading {0}: {1}'.format(func, str(ex))) else: # we always have a localkey, also when we don't have a # note['id'] yet (no sync) - localkey = n.get('localkey', os.path.splitext(os.path.basename(fn))[0]) + localkey = note.get( + 'localkey', + os.path.splitext(os.path.basename(func))[0] + ) # we maintain in memory a timestamp of the last save # these notes have just been read, so at this moment # they're in sync with the disc. - n['savedate'] = now + note['savedate'] = now # set a localkey to each note in memory # Note: 'id' is used only for syncing with server - 'localkey' # is used for everything else in nncli - n['localkey'] = localkey + note['localkey'] = localkey # add the note to our database - self.notes[localkey] = n + self.notes[localkey] = note # initialise the NextCloud instance we're going to use # this does not yet need network access @@ -62,12 +78,20 @@ class NotesDB(): self.config.get_config('nn_password'), self.config.get_config('nn_host')) - def filtered_notes_sort(self, filtered_notes, sort_mode='date'): + def set_update_view(self, update_view): + """Set the update_view method""" + self.update_view = update_view + + def _filtered_notes_sort(self, filtered_notes, sort_mode='date'): + """Sort filtered note set""" if sort_mode == 'date': if self.config.get_config('favorite_ontop') == 'yes': - filtered_notes.sort(key=utils.sort_by_modify_date_favorite, reverse=True) + filtered_notes.sort(key=utils.sort_by_modify_date_favorite, + reverse=True) else: - filtered_notes.sort(key=lambda o: -float(o.note.get('modified', 0))) + filtered_notes.sort( + key=lambda o: -float(o.note.get('modified', 0)) + ) elif sort_mode == 'alpha': if self.config.get_config('favorite_ontop') == 'yes': filtered_notes.sort(key=utils.sort_by_title_favorite) @@ -78,7 +102,8 @@ class NotesDB(): utils.sort_notes_by_categories(filtered_notes, \ favorite_ontop=favorite) - def filter_notes(self, search_string=None, search_mode='gstyle', sort_mode='date'): + def filter_notes(self, search_string=None, search_mode='gstyle', + sort_mode='date'): """Return list of notes filtered with search string. Based on the search mode that has been selected in self.config, @@ -93,21 +118,24 @@ class NotesDB(): if search_mode == 'gstyle': filtered_notes, match_regexp, active_notes = \ - self.filter_notes_gstyle(search_string) + self._filter_notes_gstyle(search_string) else: filtered_notes, match_regexp, active_notes = \ - self.filter_notes_regex(search_string) + self._filter_notes_regex(search_string) - self.filtered_notes_sort(filtered_notes, sort_mode) + self._filtered_notes_sort(filtered_notes, sort_mode) return filtered_notes, match_regexp, active_notes - def _helper_gstyle_categorymatch(self, cat_pats, note): + @staticmethod + def _helper_gstyle_categorymatch(cat_pats, note): + """Match categories using a Google-style search string""" # Returns: # 2 = match - no category patterns specified # 1 = match - all category patterns match a category on this # note - # 0 = no match - note has no category or not all category patterns match + # 0 = no match - note has no category or not all category + # patterns match if not cat_pats: # match because no category patterns were specified @@ -116,16 +144,17 @@ class NotesDB(): note_category = note.get('category') if not note_category: - # category patterns specified but note has no categories, so no match + # category patterns specified but note has no categories, + # so no match return 0 # for each cat_pat, we have to find a matching category # .lower() used for case-insensitive search cat_pats_matched = 0 - for tp in cat_pats: - tp = tp.lower() - for t in note_category: - if tp in t.lower(): + for cat_pat in cat_pats: + cat_pat = cat_pat.lower() + for pat in note_category: + if cat_pat in pat.lower(): cat_pats_matched += 1 break @@ -136,32 +165,36 @@ class NotesDB(): # note doesn't match return 0 - def _helper_gstyle_wordmatch(self, word_pats, content): + @staticmethod + def _helper_gstyle_wordmatch(word_pats, content): + """Match note contents based no a Google-style search string""" if not word_pats: return True word_pats_matched = 0 lowercase_content = content.lower() # case insensitive search - for wp in word_pats: - wp = wp.lower() # case insensitive search - if wp in lowercase_content: + for word_pat in word_pats: + word_pat = word_pat.lower() # case insensitive search + if word_pat in lowercase_content: word_pats_matched += 1 if word_pats_matched == len(word_pats): - return True; + return True return False - def filter_notes_gstyle(self, search_string=None): - + def _filter_notes_gstyle(self, search_string=None): + """Filter the notes based of a Google-style search string""" filtered_notes = [] active_notes = 0 if not search_string: - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) return filtered_notes, [], active_notes @@ -169,41 +202,48 @@ class NotesDB(): # group1: multiple words in quotes # group2: single words - # example result for: 'category:category1 category:category2 word1 "word2 word3" category:category3' + # example result for: 'category:category1 category:category2 + # word1 "word2 word3" category:category3' # [ ('category1', '', ''), # ('category2', '', ''), # ('', '', 'word1'), # ('', 'word2 word3', ''), # ('category3', '', '') ] - groups = re.findall('category:([^\s]+)|"([^"]+)"|([^\s]+)', search_string) + groups = re.findall( + r'category:([^\s]+)|"([^"]+)"|([^\s]+)', search_string + ) all_pats = [[] for _ in range(3)] # we end up with [[cat_pats],[multi_word_pats],[single_word_pats]] - for g in groups: + for group in groups: for i in range(3): - if g[i]: all_pats[i].append(g[i]) + if group[i]: + all_pats[i].append(group[i]) - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 - catmatch = self._helper_gstyle_categorymatch(all_pats[0], n) + catmatch = self._helper_gstyle_categorymatch(all_pats[0], + note) word_pats = all_pats[1] + all_pats[2] if catmatch and \ - self._helper_gstyle_wordmatch(word_pats, n.get('content')): + self._helper_gstyle_wordmatch(word_pats, note.get('content')): # we have a note that can go through! filtered_notes.append( - utils.KeyValueObject(key=k, - note=n, - catfound=1 if catmatch == 1 else 0)) + utils.KeyValueObject(key=key, + note=note, + catfound=1 \ + if catmatch == 1 \ + else 0)) return filtered_notes, '|'.join(all_pats[1] + all_pats[2]), active_notes - def filter_notes_regex(self, search_string=None): + def _filter_notes_regex(self, search_string=None): """ Return a list of notes filtered using the regex search_string. Each element in the list is a tuple (local_key, note). @@ -213,32 +253,40 @@ class NotesDB(): filtered_notes = [] active_notes = 0 # total number of notes, including deleted ones - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 if not sspat: - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) continue if self.config.get_config('search_categories') == 'yes': cat_matched = False - for t in n.get('category'): - if sspat.search(t): + for cat in note.get('category'): + if sspat.search(cat): cat_matched = True - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=1)) + filtered_notes.append( + utils.KeyValueObject(key=key, + note=note, catfound=1) + ) break if cat_matched: continue - if sspat.search(n.get('content')): - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + if sspat.search(note.get('content')): + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) match_regexp = search_string if sspat else '' return filtered_notes, match_regexp, active_notes def import_note(self, note): + """Import a note into the database""" # need to get a key unique to this database. not really important # what it is, as long as it's unique. new_key = note['id'] if note.get('id') else utils.generate_random_key() @@ -250,21 +298,23 @@ class NotesDB(): try: modified = float(note.get('modified', timestamp)) except ValueError: - raise ValueError('date fields must be numbers or string representations of numbers') + raise ValueError('date fields must be numbers or string' + 'representations of numbers') # note has no internal key yet. - new_note = { - 'content' : note.get('content', ''), - 'modified' : modified, - 'title' : note.get('title'), - 'category' : note.get('category') \ - if note.get('category') is not None \ - else '', - 'savedate' : 0, # never been written to disc - 'syncdate' : 0, # never been synced with server - 'favorite' : False, - 'deleted' : False - } + new_note = \ + { + 'content' : note.get('content', ''), + 'modified' : modified, + 'title' : note.get('title'), + 'category' : note.get('category') \ + if note.get('category') is not None \ + else '', + 'savedate' : 0, # never been written to disc + 'syncdate' : 0, # never been synced with server + 'favorite' : False, + 'deleted' : False + } # sanity check all note values if not isinstance(new_note['content'], str): @@ -285,6 +335,7 @@ class NotesDB(): return new_key def create_note(self, content): + """Create a new note in the database""" # need to get a key unique to this database. not really important # what it is, as long as it's unique. new_key = utils.generate_random_key() @@ -295,84 +346,91 @@ class NotesDB(): title = content.split('\n')[0] # note has no internal key yet. - new_note = { - 'localkey' : new_key, - 'content' : content, - 'modified' : timestamp, - 'category' : '', - 'savedate' : 0, # never been written to disc - 'syncdate' : 0, # never been synced with server - 'favorite' : False, - 'deleted' : False, - 'title' : title - } + new_note = \ + { + 'localkey' : new_key, + 'content' : content, + 'modified' : timestamp, + 'category' : '', + 'savedate' : 0, # never been written to disc + 'syncdate' : 0, # never been synced with server + 'favorite' : False, + 'deleted' : False, + 'title' : title + } self.notes[new_key] = new_note return new_key def get_note(self, key): + """Get a note from the database""" return self.notes[key] - def get_note_category(self, key): - return self.notes[key].get('category') - - def flag_what_changed(self, note, what_changed): + @staticmethod + def _flag_what_changed(note, what_changed): + """Flag a note field as changed""" if 'what_changed' not in note: note['what_changed'] = [] if what_changed not in note['what_changed']: note['what_changed'].append(what_changed) def set_note_deleted(self, key, deleted): - n = self.notes[key] - old_deleted = n['deleted'] if 'deleted' in n else 0 + """Mark a note for deletion""" + note = self.notes[key] + old_deleted = note['deleted'] if 'deleted' in note else 0 if old_deleted != deleted: - n['deleted'] = deleted - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'deleted') + note['deleted'] = deleted + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'deleted') self.log('Note marked for deletion (key={0})'.format(key)) def set_note_content(self, key, content): - n = self.notes[key] - old_content = n.get('content') + """Set the content of a note in the database""" + note = self.notes[key] + old_content = note.get('content') if content != old_content: - n['content'] = content - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'content') + note['content'] = content + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'content') self.log('Note content updated (key={0})'.format(key)) def set_note_category(self, key, category): - n = self.notes[key] - old_category = n.get('category') + """Set the category of a note in the database""" + note = self.notes[key] + old_category = note.get('category') if category != old_category: - n['category'] = category - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'category') + note['category'] = category + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'category') self.log('Note category updated (key={0})'.format(key)) def set_note_favorite(self, key, favorite): - n = self.notes[key] - old_favorite = utils.note_favorite(n) + """Mark a note in the database as a favorite""" + note = self.notes[key] + old_favorite = utils.note_favorite(note) if favorite != old_favorite: - n['favorite'] = favorite - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'favorite') + note['favorite'] = favorite + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'favorite') self.log('Note {0} (key={1})'. \ format('favorite' if favorite else \ 'unfavorited', key)) - def helper_key_to_fname(self, k): + def _helper_key_to_fname(self, k): + """Convert a note key into a file name""" return os.path.join(self.config.get_config('db_path'), str(k)) + '.json' - def helper_save_note(self, k, note): + def _helper_save_note(self, k, note): + """Save a note to the file system""" # Save a single note to disc. - fn = self.helper_key_to_fname(k) - json.dump(note, open(fn, 'w'), indent=2) + func = self._helper_key_to_fname(k) + json.dump(note, open(func, 'w'), indent=2) # record that we saved this to disc. note['savedate'] = int(time.time()) - def sync_notes(self, server_sync=True, full_sync=True): + def _sync_notes(self, server_sync=True, full_sync=True): """Perform a full bi-directional sync with server. Psuedo-code algorithm for syncing: @@ -407,15 +465,15 @@ class NotesDB(): # 1. for any note changed locally, including new notes: # save note to server, update note with response - for note_index, local_key in enumerate(self.notes.keys()): - n = self.notes[local_key] + for _, local_key in enumerate(self.notes.keys()): + note = self.notes[local_key] - if not n.get('id') or \ - float(n.get('modified')) > float(n.get('syncdate')): + if not note.get('id') or \ + float(note.get('modified')) > float(note.get('syncdate')): - savedate = float(n.get('savedate')) - if float(n.get('modified')) > savedate or \ - float(n.get('syncdate')) > savedate: + savedate = float(note.get('savedate')) + if float(note.get('modified')) > savedate or \ + float(note.get('syncdate')) > savedate: # this will trigger a save to disk after sync algorithm # we want this note saved even if offline or sync fails local_updates[local_key] = True @@ -426,77 +484,81 @@ class NotesDB(): continue # only send required fields - cn = copy.deepcopy(n) - if 'what_changed' in n: - del n['what_changed'] - - if 'localkey' in cn: - del cn['localkey'] - - if 'minversion' in cn: - del cn['minversion'] - del cn['syncdate'] - del cn['savedate'] - del cn['deleted'] - if 'etag' in cn: - del cn['etag'] - if 'title' in cn: - del cn['title'] - - if 'what_changed' in cn: - if 'content' not in cn['what_changed'] \ - and 'category' not in cn['what_changed']: - del cn['content'] - if 'category' not in cn['what_changed']: - del cn['category'] - if 'favorite' not in cn['what_changed']: - del cn['favorite'] - del cn['what_changed'] - - if n['deleted']: - uret = self.note.delete_note(cn) - else: - uret = self.note.update_note(cn) + cnote = copy.deepcopy(note) + if 'what_changed' in note: + del note['what_changed'] + + if 'localkey' in cnote: + del cnote['localkey'] + + if 'minversion' in cnote: + del cnote['minversion'] + del cnote['syncdate'] + del cnote['savedate'] + del cnote['deleted'] + if 'etag' in cnote: + del cnote['etag'] + if 'title' in cnote: + del cnote['title'] + + if 'what_changed' in cnote: + if 'content' not in cnote['what_changed'] \ + and 'category' not in cnote['what_changed']: + del cnote['content'] + if 'category' not in cnote['what_changed']: + del cnote['category'] + if 'favorite' not in cnote['what_changed']: + del cnote['favorite'] + del cnote['what_changed'] + + try: + if note['deleted']: + uret = self.note.delete_note(cnote) + else: + uret = self.note.update_note(cnote) - if uret[1] == 0: # success # if this is a new note our local key is not valid anymore # merge the note we got back (content could be empty) # record syncdate and save the note at the assigned key del self.notes[local_key] - k = uret[0].get('id') - t = uret[0].get('title') - c = uret[0].get('category') - c = c if c is not None else '' - n.update(uret[0]) - n['syncdate'] = now - n['localkey'] = k - n['category'] = c - self.notes[k] = n - - local_updates[k] = True - if local_key != k: + key = uret[0].get('id') + category = uret[0].get('category') + category = category if category is not None else '' + note.update(uret[0]) + note['syncdate'] = now + note['localkey'] = key + note['category'] = category + self.notes[key] = note + + local_updates[key] = True + if local_key != key: # if local_key was a different key it should be deleted local_deletes[local_key] = True if local_key in local_updates: del local_updates[local_key] - self.log('Synced note to server (key={0})'.format(local_key)) - else: - self.log('ERROR: Failed to sync note to server (key={0})'.format(local_key)) + self.log( + 'Synced note to server (key={0})'.format(local_key) + ) + except (ConnectionError, RequestException, ValueError): + self.log( + 'ERROR: Failed to sync note to server (key={0})'. + format(local_key) + ) sync_errors += 1 # 2. get the note index if not server_sync: - nl = [] + note_list = [] else: - nl = self.note.get_note_list() + note_list = self.note.get_note_list() - if nl[1] == 0: # success - nl = nl[0] + if note_list[1] == 0: # success + note_list = note_list[0] else: self.log('ERROR: Failed to get note list from server') sync_errors += 1 - nl = [] + note_list = [] skip_remote_syncing = True # 3. for each remote note @@ -504,44 +566,58 @@ class NotesDB(): # a new note and key is not in local store # retrieve note, update note with response if not skip_remote_syncing: - for note_index, n in enumerate(nl): - k = n.get('id') - c = n.get('category') if n.get('category') is not None \ + for _, note in enumerate(note_list): + key = note.get('id') + category = note.get('category') \ + if note.get('category') is not None \ else '' - server_keys[k] = True + server_keys[key] = True # this works because in the prior step we rewrite local keys to # server keys when we get an updated note back from the server - if k in self.notes: + if key in self.notes: # we already have this note # if the server note has a newer syncnum we need to get it - if int(n.get('modified')) > int(self.notes[k].get('modified')): - gret = self.note.get_note(k) + if int(note.get('modified')) > \ + int(self.notes[key].get('modified')): + gret = self.note.get_note(key) if gret[1] == 0: - self.notes[k].update(gret[0]) - local_updates[k] = True - self.notes[k]['syncdate'] = now - self.notes[k]['localkey'] = k - self.notes[k]['category'] = c - self.notes[k]['deleted'] = False - - self.log('Synced newer note from server (key={0})'.format(k)) + self.notes[key].update(gret[0]) + local_updates[key] = True + self.notes[key]['syncdate'] = now + self.notes[key]['localkey'] = key + self.notes[key]['category'] = category + self.notes[key]['deleted'] = False + + self.log( + 'Synced newer note from server (key={0})'. + format(key) + ) else: - self.log('ERROR: Failed to sync newer note from server (key={0})'.format(k)) + self.log( + 'ERROR: Failed to sync newer note ' + 'from server (key={0})'.format(key) + ) sync_errors += 1 else: # this is a new note - gret = self.note.get_note(k) + gret = self.note.get_note(key) if gret[1] == 0: - self.notes[k] = gret[0] - local_updates[k] = True - self.notes[k]['syncdate'] = now - self.notes[k]['localkey'] = k - self.notes[k]['category'] = c - self.notes[k]['deleted'] = False - - self.log('Synced new note from server (key={0})'.format(k)) + self.notes[key] = gret[0] + local_updates[key] = True + self.notes[key]['syncdate'] = now + self.notes[key]['localkey'] = key + self.notes[key]['category'] = category + self.notes[key]['deleted'] = False + + self.log( + 'Synced new note from server (key={0})'. + format(key) + ) else: - self.log('ERROR: Failed syncing new note from server (key={0})'.format(k)) + self.log( + 'ERROR: Failed syncing new note from' + 'server (key={0})'.format(key) + ) sync_errors += 1 # 4. for each local note not in the index @@ -557,22 +633,22 @@ class NotesDB(): for k in list(local_updates.keys()): try: - self.helper_save_note(k, self.notes[k]) - except WriteError as e: - raise WriteError (str(e)) - self.log("Saved note to disk (key={0})".format(k)) + self._helper_save_note(k, self.notes[k]) + except WriteError as ex: + raise WriteError(str(ex)) + self.log("Saved note to disk (key={0})".format(key)) for k in list(local_deletes.keys()): - fn = self.helper_key_to_fname(k) - if os.path.exists(fn): - os.unlink(fn) - self.log("Deleted note from disk (key={0})".format(k)) + fnote = self._helper_key_to_fname(k) + if os.path.exists(fnote): + os.unlink(fnote) + self.log("Deleted note from disk (key={0})".format(key)) if not sync_errors: self.last_sync = sync_start_time # if there were any changes then update the current view - if len(local_updates) > 0 or len(local_deletes) > 0: + if local_updates or local_deletes: self.update_view() if server_sync and full_sync: @@ -580,35 +656,41 @@ class NotesDB(): return sync_errors - def get_note_status(self, key): - n = self.notes[key] - o = utils.KeyValueObject(saved=False, synced=False, modified=False) - modified = float(n['modified']) - savedate = float(n['savedate']) + def _get_note_status(self, key): + """Get the note status""" + note = self.notes[key] + obj = utils.KeyValueObject(saved=False, synced=False, modified=False) + modified = float(note['modified']) + savedate = float(note['savedate']) if savedate > modified: - o.saved = True - return o + obj.saved = True + return obj def verify_all_saved(self): + """ + Verify all notes in the local database are saved to the + server + """ all_saved = True self.sync_lock.acquire() for k in list(self.notes.keys()): - o = self.get_note_status(k) - if not o.saved: + obj = self._get_note_status(k) + if not obj.saved: all_saved = False break self.sync_lock.release() return all_saved def sync_now(self, do_server_sync=True): + """Sync the notes to the server""" self.sync_lock.acquire() - self.sync_notes(server_sync=do_server_sync, - full_sync=True if not self.last_sync else False) + self._sync_notes(server_sync=do_server_sync, + full_sync=True if not self.last_sync else False) self.sync_lock.release() - # sync worker thread... def sync_worker(self, do_server_sync): + """The sync worker thread""" time.sleep(1) # give some time to wait for GUI initialization self.log('Sync worker: started') self.sync_now(do_server_sync) @@ -619,6 +701,7 @@ class NotesDB(): self.go_cond.release() def sync_worker_go(self): + """Start the sync worker""" self.go_cond.acquire() self.go_cond.notify() self.go_cond.release() diff --git a/nncli/temp.py b/nncli/temp.py index 0bb3177..a6f53bc 100644 --- a/nncli/temp.py +++ b/nncli/temp.py @@ -1,42 +1,50 @@ # -*- coding: utf-8 -*- - -import os, json, tempfile +"""temp module""" +import json +import os +import tempfile def tempfile_create(note, raw=False, tempdir=None): + """create a temp file""" if raw: # dump the raw json of the note - tf = tempfile.NamedTemporaryFile(suffix='.json', delete=False, dir=tempdir) + tfile = tempfile.NamedTemporaryFile(suffix='.json', + delete=False, dir=tempdir) contents = json.dumps(note, indent=2) - tf.write(contents.encode('utf-8')) - tf.flush() + tfile.write(contents.encode('utf-8')) + tfile.flush() else: ext = '.mkd' - tf = tempfile.NamedTemporaryFile(suffix=ext, delete=False, dir=tempdir) + tfile = tempfile.NamedTemporaryFile(suffix=ext, delete=False, + dir=tempdir) if note: contents = note['content'] - tf.write(contents.encode('utf-8')) - tf.flush() - return tf + tfile.write(contents.encode('utf-8')) + tfile.flush() + return tfile -def tempfile_delete(tf): - if tf: - tf.close() - os.unlink(tf.name) +def tempfile_delete(tfile): + """delete a temp file""" + if tfile: + tfile.close() + os.unlink(tfile.name) -def tempfile_name(tf): - if tf: - return tf.name +def tempfile_name(tfile): + """get the name of a temp file""" + if tfile: + return tfile.name return '' -def tempfile_content(tf): +def tempfile_content(tfile): + """read the contents of the temp file""" # This 'hack' is needed because some editors use an intermediate temporary # file, and rename it to that of the correct file, overwriting it. This # means that the tf file handle won't be updated with the new contents, and # the tempfile must be re-opened and read - if not tf: + if not tfile: return None - with open(tf.name, 'rb') as f: - updated_tf_contents = f.read() + with open(tfile.name, 'rb') as temp: + updated_tf_contents = temp.read() return updated_tf_contents.decode('utf-8') diff --git a/nncli/user_input.py b/nncli/user_input.py index 958cf8e..0e83ad9 100644 --- a/nncli/user_input.py +++ b/nncli/user_input.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- - +"""user_input module""" import urwid +# pylint: disable=too-many-arguments class UserInput(urwid.Edit): - + """UserInput class""" def __init__(self, config, caption, edit_text, callback_func, args): self.config = config - self.callback_func = callback_func + self.callback_func = callback_func self.callback_func_args = args super(UserInput, self).__init__(caption=caption, edit_text=edit_text, diff --git a/nncli/utils.py b/nncli/utils.py index 3635c33..e043622 100644 --- a/nncli/utils.py +++ b/nncli/utils.py @@ -1,6 +1,83 @@ # -*- coding: utf-8 -*- +"""utils module""" +import random +import re +import shlex + +import subprocess +from subprocess import CalledProcessError + +from . import temp + +# pylint: disable=too-many-arguments,too-few-public-methods +def get_editor(config, logger): + """Get the editor""" + editor = config.get_config('editor') + if not editor: + logger.log('No editor configured!') + return None + return editor + +def get_pager(config, logger): + """Get the pager""" + pager = config.get_config('pager') + if not pager: + logger.log('No pager configured!') + return None + return pager + +def exec_cmd_on_note(note, config, gui, logger, cmd=None, raw=False): + """Execute an external command to operate on the note""" + + if not cmd: + cmd = get_editor(config, logger) + if not cmd: + return None + + tfile = temp.tempfile_create( + note if note else None, + raw=raw, + tempdir=config.get_config('tempdir') + ) + fname = temp.tempfile_name(tfile) + + if config.state.do_gui: + focus_position = 0 + try: + focus_position = gui.gui_body_get().focus_position + except IndexError: + pass + + subs = {'fname': fname, 'line': focus_position + 1} + cmd_list = [c.format(**subs) for c in shlex.split(cmd)] + + # if the filename wasn't able to be subbed, append it + # this makes it fully backwards compatible with previous configs + if '{fname}' not in cmd: + cmd_list.append(fname) + + logger.log("EXECUTING: {}".format(cmd_list)) + + try: + subprocess.check_call(cmd_list) + except CalledProcessError as ex: + logger.log('Command error: %s' % ex) + temp.tempfile_delete(tfile) + return None -import datetime, random, re + content = None + if not raw: + content = temp.tempfile_content(tfile) + if not content or content == '\n': + content = None + + temp.tempfile_delete(tfile) + + if config.state.do_gui: + gui.nncli_loop.screen.clear() + gui.nncli_loop.draw_screen() + + return content def generate_random_key(): """Generate random 30 digit (15 byte) hex string. @@ -10,16 +87,21 @@ def generate_random_key(): return '%030x' % (random.randrange(256**15),) def get_note_category(note): + """get a note category""" if 'category' in note: category = note['category'] if note['category'] is not None else '' else: category = '' return category -# Returns a fixed length string: -# 'X' - needs sync -# '*' - favorite def get_note_flags(note): + """ + get the note flags + + Returns a fixed length string: + 'X' - needs sync + '*' - favorite + """ flags = '' flags += 'X' if float(note['modified']) > float(note['syncdate']) else ' ' if 'favorite' in note: @@ -29,30 +111,40 @@ def get_note_flags(note): return flags def get_note_title(note): + """get the note title""" if 'title' in note: return note['title'] - else: - return '' + return '' -def note_favorite(n): - if 'favorite' in n: - return n['favorite'] - else: - return False +def note_favorite(note): + """ + get the status of the note as a favorite + + returns True if the note is marked as a favorite + False otherwise + """ + if 'favorite' in note: + return note['favorite'] + return False -def sort_by_title_favorite(a): - return (not note_favorite(a.note), get_note_title(a.note)) +def sort_by_title_favorite(left): + """sort notes by title, favorites on top""" + return (not note_favorite(left.note), get_note_title(left.note)) def sort_notes_by_categories(notes, favorite_ontop=False): + """ + sort notes by category, optionally pushing favorites to the + top + """ notes.sort(key=lambda i: (favorite_ontop and not note_favorite(i.note), i.note.get('category'), get_note_title(i.note))) -def sort_by_modify_date_favorite(a): - if note_favorite(a.note): - return 100.0 * float(a.note.get('modified', 0)) - else: - return float(a.note.get('modified', 0)) +def sort_by_modify_date_favorite(left): + """sort notest by modify date, favorites on top""" + if note_favorite(left.note): + return 100.0 * float(left.note.get('modified', 0)) + return float(left.note.get('modified', 0)) class KeyValueObject: """Store key=value pairs in this object and retrieve with o.key. @@ -78,7 +170,8 @@ def build_regex_search(search_string): } if search_string: try: - search_string, flag_letters = re.match(r'^(.+?)(?:/([a-z]+))?$', search_string).groups() + search_string, flag_letters = \ + re.match(r'^(.+?)(?:/([a-z]+))?$', search_string).groups() flags = 0 # if flags are given, OR together all the valid flags # see https://docs.python.org/3/library/re.html#re.compile diff --git a/nncli/view_help.py b/nncli/view_help.py index cf9635f..dbe3a18 100644 --- a/nncli/view_help.py +++ b/nncli/view_help.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- - -import re, urwid +"""view_help module""" +import re +import urwid class ViewHelp(urwid.ListBox): - + """ViewHelp class""" def __init__(self, config): self.config = config - self.descr_width = 26 + self.descr_width = 26 self.config_width = 29 lines = [] lines.extend(self.create_kb_help_lines('Keybinds Common', 'common')) lines.extend(self.create_kb_help_lines('Keybinds Note List', 'titles')) - lines.extend(self.create_kb_help_lines('Keybinds Note Content', 'notes')) + lines.extend( + self.create_kb_help_lines('Keybinds Note Content', 'notes') + ) lines.extend(self.create_config_help_lines()) lines.extend(self.create_color_help_lines()) lines.append(urwid.Text(('help_header', ''))) @@ -21,10 +24,11 @@ class ViewHelp(urwid.ListBox): super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) def get_status_bar(self): - cur = -1 + """get the status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) status_title = \ @@ -38,87 +42,123 @@ class ViewHelp(urwid.ListBox): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def create_kb_help_lines(self, header, use): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help page for the keybindings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' ' + header), 'help_header', 'help_focus')) - for c in self.config.keybinds: - if use not in self.config.get_keybind_use(c): + for config in self.config.keybinds: + if use not in self.config.get_keybind_use(config): continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + keybinds_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_keybind_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('kb_' + c)), - ('help_value', "'" + self.config.get_keybind(c) + "'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) + ( + 'help_descr', + ( + '{:>' + str(self.descr_width) + '} ' + ).format( + self.config.get_keybind_descr( + config + ) + ) + ), + ( + 'help_config', + ( + '{:>' + str(self.config_width) \ + + '} ' + ).format('kb_' + config) + ), + ( + 'help_value', + "'" + + self.config.get_keybind(config) + "'" + ) + ]) + lines.append( + urwid.AttrMap( + urwid.AttrMap( + keybinds_text, + attr_map=None, + focus_map= \ + { + 'help_value': 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + }), + 'default', 'help_focus')) return lines def create_config_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help lines for the general config settings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Configuration'), 'help_header', 'help_focus')) - for c in self.config.configs: - if c in [ 'sn_username', 'sn_password' ]: continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + for config in self.config.configs: + if config in ['nn_username', 'nn_password']: + continue + config_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_config_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('cfg_' + c)), - ('help_value', "'" + self.config.get_config(c) + "'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) + ('help_descr', + ('{:>' + str(self.descr_width) + '} '). + format(self.config.get_config_descr(config))), + ('help_config', + ('{:>' + str(self.config_width) + '} '). + format('cfg_' + config)), + ('help_value', + "'" + + str(self.config.get_config(config)) + "'") + ]) + lines.append( + urwid.AttrMap(urwid.AttrMap( + config_text, + attr_map=None, + focus_map={ + 'help_value' : 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + } + ), 'default', 'help_focus')) return lines def create_color_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help lines for the color settings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Colors'), 'help_header', 'help_focus')) fmap = {} - for c in self.config.colors: - fmap[re.search('^(.*)(_fg|_bg)$', c).group(1)] = 'help_focus' - for c in self.config.colors: - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + for config in self.config.colors: + fmap[re.search('^(.*)(_fg|_bg)$', config).group(1)] = 'help_focus' + for color in self.config.colors: + colors_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_color_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('clr_' + c)), - (re.search('^(.*)(_fg|_bg)$', c).group(1), "'" + self.config.get_color(c) + "'") - ] - ), - attr_map = None, - focus_map = fmap - ), 'default', 'help_focus')) + ('help_descr', + ('{:>' + str(self.descr_width) + '} '). + format(self.config.get_color_descr(color))), + ('help_config', + ('{:>' + str(self.config_width) + '} '). + format('clr_' + color)), + (re.search('^(.*)(_fg|_bg)$', color).group(1), + "'" + self.config.get_color(color) + "'") + ]) + lines.append( + urwid.AttrMap(urwid.AttrMap( + colors_text, + attr_map=None, + focus_map=fmap + ), 'default', 'help_focus')) return lines def keypress(self, size, key): diff --git a/nncli/view_log.py b/nncli/view_log.py index 924f557..9b1839e 100644 --- a/nncli/view_log.py +++ b/nncli/view_log.py @@ -1,32 +1,38 @@ # -*- coding: utf-8 -*- - +"""view_log module""" import urwid class ViewLog(urwid.ListBox): + """ + ViewLog class + This class defines the urwid view class for the log viewer + """ def __init__(self, config): self.config = config super(ViewLog, self).__init__(urwid.SimpleFocusListWalker([])) def update_log(self): + """update the log""" lines = [] - f = open(self.config.logfile) - for line in f: - lines.append( - urwid.AttrMap(urwid.Text(line.rstrip()), - 'note_content', - 'note_content_focus')) - f.close() + with open(self.config.logfile) as logfile: + for line in logfile: + lines.append( + urwid.AttrMap(urwid.Text(line.rstrip()), + 'note_content', + 'note_content_focus') + ) if self.config.get_config('log_reversed') == 'yes': lines.reverse() self.body[:] = urwid.SimpleFocusListWalker(lines) self.focus_position = 0 def get_status_bar(self): - cur = -1 + """get the log view status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) status_title = \ @@ -40,7 +46,7 @@ class ViewLog(urwid.ListBox): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def keypress(self, size, key): diff --git a/nncli/view_note.py b/nncli/view_note.py index ad90e85..48aa543 100644 --- a/nncli/view_note.py +++ b/nncli/view_note.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- - -import time, urwid +"""view_note module""" +import time +import urwid from . import utils -import re from .clipboard import Clipboard -import logging +# pylint: disable=too-many-instance-attributes class ViewNote(urwid.ListBox): + """ + ViewNote class + This class defines the urwid class responsible for displaying an + individual note in an internal pager + """ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] @@ -21,31 +26,35 @@ class ViewNote(urwid.ListBox): self.tabstop = int(self.config.get_config('tabstop')) self.clipboard = Clipboard() super(ViewNote, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_content_as_list())) + urwid.SimpleFocusListWalker(self.get_note_content_as_list())) def get_note_content_as_list(self): + """return the contents of a note as a list of strings""" lines = [] if not self.key: return lines if self.old_note: - for l in self.old_note['content'].split('\n'): + for line in self.old_note['content'].split('\n'): lines.append( - urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), - 'note_content_old', - 'note_content_old_focus')) + urwid.AttrMap(urwid.Text( + line.replace('\t', ' ' * self.tabstop)), + 'note_content_old', + 'note_content_old_focus')) else: - for l in self.note['content'].split('\n'): + for line in self.note['content'].split('\n'): lines.append( - urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), - 'note_content', - 'note_content_focus')) + urwid.AttrMap(urwid.Text( + line.replace('\t', ' ' * self.tabstop)), + 'note_content', + 'note_content_focus')) lines.append(urwid.AttrMap(urwid.Divider('-'), 'default')) return lines - def update_note_view(self, key=None, version=None): + def update_note_view(self, key=None): + """update the view""" if key: # setting a new note - self.key = key - self.note = self.ndb.get_note(self.key) + self.key = key + self.note = self.ndb.get_note(self.key) self.old_note = None self.body[:] = \ @@ -54,63 +63,80 @@ class ViewNote(urwid.ListBox): self.focus_position = 0 def lines_after_current_position(self): - lines_after_current_position = list(range(self.focus_position + 1, len(self.body.positions()) - 1)) + """ + return the number of lines after the currently-focused + line + """ + lines_after_current_position = \ + list(range(self.focus_position + 1, + len(self.body.positions()) - 1)) return lines_after_current_position def lines_before_current_position(self): + """ + return the number of lines before the currently-focused line + """ lines_before_current_position = list(range(0, self.focus_position)) lines_before_current_position.reverse() return lines_before_current_position def search_note_view_next(self, search_string=None, search_mode=None): + """move to the next match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode - note_range = self.lines_after_current_position() if self.search_direction == 'forward' else self.lines_before_current_position() + note_range = self.lines_after_current_position() \ + if self.search_direction == 'forward' \ + else self.lines_before_current_position() self.search_note_range(note_range) def search_note_view_prev(self, search_string=None, search_mode=None): + """move to the previous match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode - note_range = self.lines_after_current_position() if self.search_direction == 'backward' else self.lines_before_current_position() + note_range = self.lines_after_current_position() \ + if self.search_direction == 'backward' \ + else self.lines_before_current_position() self.search_note_range(note_range) def search_note_range(self, note_range): + """search within a range of lines""" for line in note_range: line_content = self.note['content'].split('\n')[line] - if (self.is_match(self.search_string, line_content)): + if self.is_match(self.search_string, line_content): self.focus_position = line break self.update_note_view() def is_match(self, term, full_text): + """returns True if there is a match, False otherwise""" if self.search_mode == 'gstyle': return term in full_text - else: - sspat = utils.build_regex_search(term) - return sspat and sspat.search(full_text) + sspat = utils.build_regex_search(term) + return sspat and sspat.search(full_text) def get_status_bar(self): + """get the note view status bar""" if not self.key: return \ urwid.AttrMap(urwid.Text('No note...'), 'status_bar') - cur = -1 + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) - t = time.localtime(float(self.note['modified'])) - title = utils.get_note_title(self.note) - flags = utils.get_note_flags(self.note) + localtime = time.localtime(float(self.note['modified'])) + title = utils.get_note_title(self.note) + flags = utils.get_note_flags(self.note) category = utils.get_note_category(self.note) - mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', t) + mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', localtime) status_title = \ urwid.AttrMap(urwid.Text('Title: ' + @@ -140,14 +166,15 @@ class ViewNote(urwid.ListBox): ']'), 'status_bar')) - pile_top = urwid.Columns([ status_title, status_key_index ]) - pile_bottom = urwid.Columns([ status_date, status_category_flags ]) + pile_top = urwid.Columns([status_title, status_key_index]) + pile_bottom = urwid.Columns([status_date, status_category_flags]) return \ - urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom ]), + urwid.AttrMap(urwid.Pile([pile_top, pile_bottom]), 'status_bar') def copy_note_text(self): + """copy the text of the note to the system clipboard""" line_content = self.note['content'].split('\n')[self.focus_position] self.clipboard.copy(line_content) diff --git a/nncli/view_titles.py b/nncli/view_titles.py index 5d924a0..edcf9fe 100644 --- a/nncli/view_titles.py +++ b/nncli/view_titles.py @@ -1,32 +1,48 @@ # -*- coding: utf-8 -*- - -import re, time, datetime, urwid, subprocess -from . import utils, view_note - +"""view_titles module""" +import re +import time +import datetime +import urwid +from . import utils + +# pylint: disable=too-many-instance-attributes, too-many-statements class ViewTitles(urwid.ListBox): + """ + ViewTitles class + Implements the urwid class for the view_titles view + """ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] self.search_string = args['search_string'] self.log = args['log'] self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string, sort_mode=self.config.get_config('sort_mode')) + self.ndb.filter_notes( + self.search_string, + sort_mode=self.config.get_config('sort_mode') + ) super(ViewTitles, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_titles())) + urwid.SimpleFocusListWalker(self.get_note_titles())) - def update_note_list(self, search_string, search_mode='gstyle', sort_mode='date'): + def update_note_list(self, search_string, + search_mode='gstyle', sort_mode='date'): + """update the note list""" self.search_string = search_string self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string, search_mode, sort_mode=sort_mode) + self.ndb.filter_notes( + self.search_string, search_mode, sort_mode=sort_mode + ) self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) - if len(self.note_list) == 0: + if not self.note_list: self.log('No notes found!') else: self.focus_position = 0 def sort_note_list(self, sort_mode): + """sort the note list""" self.ndb.filtered_notes_sort(self.note_list, sort_mode) self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) @@ -44,21 +60,23 @@ class ViewTitles(urwid.ListBox): %N -- note title """ - t = time.localtime(float(note['modified'])) - mod_time = time.strftime(self.config.get_config('format_strftime'), t) + localtime = time.localtime(float(note['modified'])) + mod_time = \ + time.strftime(self.config.get_config('format_strftime'), + localtime) title = utils.get_note_title(note) flags = utils.get_note_flags(note) - category = utils.get_note_category(note) + category = utils.get_note_category(note) # get the age of the note - dt = datetime.datetime.fromtimestamp(time.mktime(t)) - if dt > datetime.datetime.now() - datetime.timedelta(days=1): + dtime = datetime.datetime.fromtimestamp(time.mktime(localtime)) + if dtime > datetime.datetime.now() - datetime.timedelta(days=1): note_age = 'd' # less than a day old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=1): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=1): note_age = 'w' # less than a week old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=4): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=4): note_age = 'm' # less than a month old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=52): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=52): note_age = 'y' # less than a year old else: note_age = 'a' # ancient @@ -66,86 +84,101 @@ class ViewTitles(urwid.ListBox): def recursive_format(title_format): if not title_format: return None - fmt = re.search("^(.*)%([-]*)([0-9]*)([FDTN])(.*)$", title_format) + fmt = re.search(r'^(.*)%([-]*)([0-9]*)([FDTN])(.*)$', title_format) if not fmt: - m = ('pack', urwid.AttrMap(urwid.Text(title_format), - 'default')) + attr_map = ('pack', urwid.AttrMap(urwid.Text(title_format), + 'default')) l_fmt = None r_fmt = None else: - l = fmt.group(1) if fmt.group(1) else None - m = None - r = fmt.group(5) if fmt.group(5) else None + left = fmt.group(1) if fmt.group(1) else None + attr_map = None + right = fmt.group(5) if fmt.group(5) else None align = 'left' if fmt.group(2) == '-' else 'right' width = int(fmt.group(3)) if fmt.group(3) else 'pack' if fmt.group(4) == 'F': - m = (width, urwid.AttrMap(urwid.Text(flags, - align=align, - wrap='clip'), - 'note_flags')) + attr_map = (width, urwid.AttrMap(urwid.Text(flags, + align=align, + wrap='clip'), + 'note_flags')) elif fmt.group(4) == 'D': - m = (width, urwid.AttrMap(urwid.Text(mod_time, - align=align, - wrap='clip'), - 'note_date')) + attr_map = (width, urwid.AttrMap(urwid.Text(mod_time, + align=align, + wrap='clip'), + 'note_date')) elif fmt.group(4) == 'T': - m = (width, urwid.AttrMap(urwid.Text(category, - align=align, - wrap='clip'), - 'note_category')) + attr_map = (width, urwid.AttrMap(urwid.Text(category, + align=align, + wrap='clip'), + 'note_category')) elif fmt.group(4) == 'N': - if note_age == 'd': attr = 'note_title_day' - elif note_age == 'w': attr = 'note_title_week' - elif note_age == 'm': attr = 'note_title_month' - elif note_age == 'y': attr = 'note_title_year' - elif note_age == 'a': attr = 'note_title_ancient' + if note_age == 'd': + attr = 'note_title_day' + elif note_age == 'w': + attr = 'note_title_week' + elif note_age == 'm': + attr = 'note_title_month' + elif note_age == 'y': + attr = 'note_title_year' + elif note_age == 'a': + attr = 'note_title_ancient' if width != 'pack': - m = (width, urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr)) + attr_map = (width, urwid.AttrMap( + urwid.Text( + title, + align=align, + wrap='clip' + ), + attr)) else: - m = urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr) - l_fmt = recursive_format(l) - r_fmt = recursive_format(r) + attr_map = urwid.AttrMap(urwid.Text(title, + align=align, + wrap='clip'), + attr) + l_fmt = recursive_format(left) + r_fmt = recursive_format(right) tmp = [] - if l_fmt: tmp.extend(l_fmt) - tmp.append(m) - if r_fmt: tmp.extend(r_fmt) + if l_fmt: + tmp.extend(l_fmt) + tmp.append(attr_map) + if r_fmt: + tmp.extend(r_fmt) return tmp # convert the format string into the actual note title line - title_line = recursive_format(self.config.get_config('format_note_title')) + title_line = recursive_format( + self.config.get_config('format_note_title') + ) return urwid.Columns(title_line) def get_note_title(self, note): + """get the title of a note""" return urwid.AttrMap(self.format_title(note), 'default', - { 'default' : 'note_focus', - 'note_title_day' : 'note_focus', - 'note_title_week' : 'note_focus', - 'note_title_month' : 'note_focus', - 'note_title_year' : 'note_focus', - 'note_title_ancient' : 'note_focus', - 'note_date' : 'note_focus', - 'note_flags' : 'note_focus', - 'note_categories' : 'note_focus' }) + {'default' : 'note_focus', + 'note_title_day' : 'note_focus', + 'note_title_week' : 'note_focus', + 'note_title_month' : 'note_focus', + 'note_title_year' : 'note_focus', + 'note_title_ancient' : 'note_focus', + 'note_date' : 'note_focus', + 'note_flags' : 'note_focus', + 'note_categories' : 'note_focus'}) def get_note_titles(self): + """get the titles of all of the notes""" lines = [] - for n in self.note_list: - lines.append(self.get_note_title(n.note)) + for note in self.note_list: + lines.append(self.get_note_title(note.note)) return lines def get_status_bar(self): - cur = -1 + """get the status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) hdr = 'NextCloud Notes' @@ -153,7 +186,7 @@ class ViewTitles(urwid.ListBox): # include connection status in header hdr += ' (' + self.ndb.note.status + ')' - if self.search_string != None: + if self.search_string is not None: hdr += ' - Search: ' + self.search_string status_title = \ @@ -167,10 +200,11 @@ class ViewTitles(urwid.ListBox): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def update_note_title(self, key=None): + """update a note title""" if not key: self.body[self.focus_position] = \ self.get_note_title(self.note_list[self.focus_position].note) @@ -180,6 +214,7 @@ class ViewTitles(urwid.ListBox): self.body[i] = self.get_note_title(self.note_list[i].note) def focus_note(self, key): + """set the focus on a given note""" for i in range(len(self.note_list)): if 'localkey' in self.note_list[i].note and \ self.note_list[i].note['localkey'] == key: diff --git a/pyproject.toml b/pyproject.toml index 9fe6282..36b2e01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ Documentation = "https://nncli.readthedocs.io/en/latest" [tool.flit.metadata.requires-extra] dev = ["pipenv"] +doc = ["sphinx"] [tool.flit.scripts] nncli = "nncli.cli:main" diff --git a/tests/test_config.py b/tests/test_config.py index c6f77a2..7fd3aee 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,79 +1,106 @@ # -*- coding: utf-8 -*- import os +import subprocess import sys from nncli.config import Config from pytest import raises +def mock_config_file(mocker, file_contents): + """mock the file and configparser 'enumerate' iterator""" + mock_cfg = mocker.mock_open( + read_data='\n'.join(file_contents) + ) + mocker.patch('configparser.open', mock_cfg) + mocker.patch('configparser.enumerate', new=mocker.Mock( + return_value=enumerate(file_contents, start=1) + )) + return mock_cfg + def test_init(mocker): - mocker.patch('subprocess.check_output') + """test nominal initialization""" + mock_cfg = mock_config_file( + mocker, + [ + '[nncli]', + 'cfg_nn_username=user', + 'cfg_nn_password_eval=password_cmd', + 'cfg_nn_host=nextcloud.example.org' + ]) + mocker.patch('subprocess.check_output', + new=mocker.Mock(return_value='yes\n')) + config = Config() - if sys.platform == 'linux': - assert config.config_home == os.path.join(os.path.expanduser('~'), \ - '.config', 'nncli') - assert config.cache_home == os.path.join(os.path.expanduser('~'), \ - '.cache', 'nncli') - if sys.platform == 'darwin': - assert config.config_home == os.path.join(os.path.expanduser('~'), \ - 'Library', 'Preferences', 'nncli') - assert config.cache_home == os.path.join(os.path.expanduser('~'), \ - 'Library', 'Caches', 'nncli') - - -def test_custom_file(): - with open('test_cfg', 'w') as config_file: - config_file.write('[nncli]\n') - config_file.write('cfg_nn_username=user\n') - config_file.write('cfg_nn_password=password\n') - config_file.write('cfg_nn_host=nextcloud.example.org\n') + mock_cfg.assert_called_once() + subprocess.check_output.assert_called_once() + assert config.get_config('nn_password') == 'yes' - config = Config('test_cfg') - os.remove('test_cfg') +def test_custom_file(mocker): + """test with a supplied (custom) config file""" + mock_cfg = mock_config_file(mocker, + [ + '[nncli]', + 'cfg_nn_username=user', + 'cfg_nn_password=password', + 'cfg_nn_host=nextcloud.example.org' + ]) -def test_bad_password_eval(): - with open('test_cfg', 'w') as config_file: - config_file.write('[nncli]\n') - config_file.write('cfg_nn_username=user\n') - config_file.write('cfg_nn_password_eval=password\n') - config_file.write('cfg_nn_host=nextcloud.example.org\n') + config = Config('test_cfg') + mock_cfg.assert_called_once_with('test_cfg', encoding=None) + assert config.get_config('nn_username') == 'user' + assert config.get_config('nn_password') == 'password' + +def test_bad_password_eval(mocker): + """test failed call to password eval""" + mock_cfg = mock_config_file(mocker, + [ + '[nncli]', + 'cfg_nn_username=user', + 'cfg_nn_password_eval=password', + 'cfg_nn_host=nextcloud.example.org' + ]) with raises(SystemExit): config = Config('test_cfg') - os.remove('test_cfg') -def test_empty_config(): - with open('test_cfg', 'w') as config_file: - config_file.write('\n') +def test_empty_config(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') - os.remove('test_cfg') -def test_get_config(): +def test_get_config(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_config('sort_mode') == 'date' -def test_get_config_descr(): +def test_get_config_descr(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_config_descr('sort_mode') == 'Sort mode' -def test_get_keybind(): +def test_get_keybind(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_keybind('help') == 'h' -def test_get_keybind_use(): +def test_get_keybind_use(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_keybind_use('help') == [ 'common' ] -def test_get_keybind_descr(): +def test_get_keybind_descr(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_keybind_descr('help') == 'Help' -def test_get_color(): +def test_get_color(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_color('default_fg') == 'default' -def test_get_color_descr(): +def test_get_color_descr(mocker): + mock_cfg = mock_config_file(mocker, []) config = Config('test_cfg') assert config.get_color_descr('default_fg') == 'Default fg' diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..bbae73a --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +"""tests for gui module""" +import pytest + +import nncli.gui + +@pytest.mark.skip +def test_gui_header_clear(): + pass + +@pytest.mark.skip +def test_gui_header_set(): + pass + +@pytest.mark.skip +def test_gui_header_get(): + pass + +@pytest.mark.skip +def test_gui_header_focus(): + pass + +@pytest.mark.skip +def test_gui_footer_log_clear(): + pass + +@pytest.mark.skip +def test_gui_footer_log_set(): + pass + +@pytest.mark.skip +def test_gui_footer_log_get(): + pass + +@pytest.mark.skip +def test_gui_footer_input_clear(): + pass + +@pytest.mark.skip +def test_gui_footer_input_set(): + pass + +@pytest.mark.skip +def test_gui_footer_input_get(): + pass + +@pytest.mark.skip +def test_gui_footer_focus_input(): + pass + +@pytest.mark.skip +def test_gui_body_clear(): + pass + +@pytest.mark.skip +def test_gui_body_set(): + pass + +@pytest.mark.skip +def test_gui_body_get(): + pass + +@pytest.mark.skip +def test_gui_body_focus(): + pass + +@pytest.mark.skip +def test_log_timeout(): + pass + +@pytest.mark.skip +def test_log(): + pass + +@pytest.mark.skip +def test_gui_update_view(): + pass + +@pytest.mark.skip +def test_gui_update_status_bar(): + pass + +@pytest.mark.skip +def test_gui_switch_frame_body(): + pass + +@pytest.mark.skip +def test_delete_note_callback(): + pass + +@pytest.mark.skip +def test_gui_yes_no_input(): + pass + +@pytest.mark.skip +def test_gui_search_input(): + pass + +@pytest.mark.skip +def test_gui_category_input(): + pass + +@pytest.mark.skip +def test_gui_pipe_input(): + pass + +@pytest.mark.skip +def test_gui_frame_keypress(): + pass + +@pytest.mark.skip +def test_gui_init_view(): + pass + +@pytest.mark.skip +def test_gui_clear(): + pass + +@pytest.mark.skip +def test_gui_reset(): + pass + +@pytest.mark.skip +def test_gui_stop(): + pass + +@pytest.mark.skip +def test_gui(): + pass diff --git a/tests/test_nncli.py b/tests/test_nncli.py index f9c67d2..d3790ae 100644 --- a/tests/test_nncli.py +++ b/tests/test_nncli.py @@ -1,234 +1,255 @@ # -*- coding: utf-8 -*- - +"""tests for nncli module""" +from io import StringIO import logging import os import pytest import shutil -from logging.handlers import RotatingFileHandler import nncli.nncli +from nncli.notes_db import ReadError +import nncli.utils @pytest.fixture def mock_nncli(mocker): - mocker.patch('logging.getLogger') + """mock the major interfaces for the Nncli class""" mocker.patch('nncli.nncli.NotesDB') + mocker.patch('nncli.nncli.NncliGui') + mocker.patch('nncli.nncli.Config') + mocker.patch('nncli.nncli.Logger') mocker.patch('os.mkdir') - mocker.patch.object(RotatingFileHandler, '_open') mocker.patch('subprocess.check_output') - -def mock_get_config(mocker, return_list): - mocker.patch.object( - nncli.nncli.Config, - 'get_config', - new=mocker.MagicMock(side_effect=return_list) - ) - -def assert_initialized(): - assert logging.getLogger.call_count == 2 - RotatingFileHandler._open.assert_called_once() + mocker.patch('os.path.exists', + new=mocker.MagicMock(return_value=True)) + +def test_init_no_local_db(mocker, mock_nncli): + """test initialization when there is no local notes database""" + mocker.patch('os.path.exists', + new=mocker.MagicMock(return_value=False)) + nn_obj = nncli.nncli.Nncli(False) + assert nn_obj.config.get_config.call_count == 2 + nn_obj.ndb.set_update_view.assert_called_once() os.mkdir.assert_called_once() - -def test_init_no_tempdir(mocker, mock_nncli): - mock_get_config(mocker, ['what', '', 'duh', 'duh', 'duh']) - nn = nncli.nncli.Nncli(False) - assert_initialized() - assert nn.tempdir == None - os.mkdir.assert_called_with('duh') + nn_obj.ndb.sync_now.assert_called_once() def test_init(mocker, mock_nncli): - mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh']) - nn = nncli.nncli.Nncli(False) - assert_initialized() - assert nn.tempdir == 'blah' + """test nominal initialization""" + nn_obj = nncli.nncli.Nncli(False) + nn_obj.config.get_config.assert_called_once() + nn_obj.ndb.set_update_view.assert_called_once() + assert os.mkdir.call_count == 0 def test_init_notesdb_fail(mocker, mock_nncli): - mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh']) + """test init when there is a notes database failure""" + mocker.patch('os.path.exists', + new=mocker.MagicMock(return_value=True)) mocker.patch('nncli.nncli.NotesDB', - new=mocker.MagicMock(side_effect=SystemExit) - ) + new=mocker.MagicMock(side_effect=ReadError) + ) with pytest.raises(SystemExit): nn = nncli.nncli.Nncli(False) - -def test_get_editor(mocker, mock_nncli): - mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'vim', '']) - nn = nncli.nncli.Nncli(False) - assert_initialized() - assert nn.get_editor() == 'vim' - assert nn.get_editor() == None - -def test_get_pager(mocker, mock_nncli): - mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'less', '']) - nn = nncli.nncli.Nncli(False) - assert_initialized() - assert nn.get_editor() == 'less' - assert nn.get_editor() == None - -def test_get_diff(mocker, mock_nncli): - mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'diff', '']) - nn = nncli.nncli.Nncli(False) - assert_initialized() - assert nn.get_editor() == 'diff' - assert nn.get_editor() == None - -@pytest.mark.skip -def test_exec_cmd_on_note(mocker, mock_nncli): - mocker.patch.object( - 'nncli.nncli.Nncli', - get_editor, - new=mocker.MagicMock(return_value='vim')) - mocker.patch('nncli.temp.tempfile_create') - -@pytest.mark.skip -def test_exec_diff_on_note(): - pass - -@pytest.mark.skip -def test_gui_header_clear(): - pass - -@pytest.mark.skip -def test_gui_header_set(): - pass - -@pytest.mark.skip -def test_gui_header_get(): - pass - -@pytest.mark.skip -def test_gui_header_focus(): - pass - -@pytest.mark.skip -def test_gui_footer_log_clear(): - pass - -@pytest.mark.skip -def test_gui_footer_log_set(): - pass - -@pytest.mark.skip -def test_gui_footer_log_get(): - pass - -@pytest.mark.skip -def test_gui_footer_input_clear(): - pass - -@pytest.mark.skip -def test_gui_footer_input_set(): - pass - -@pytest.mark.skip -def test_gui_footer_input_get(): - pass - -@pytest.mark.skip -def test_gui_footer_focus_input(): - pass - -@pytest.mark.skip -def test_gui_body_clear(): - pass - -@pytest.mark.skip -def test_gui_body_set(): - pass - -@pytest.mark.skip -def test_gui_body_get(): - pass - -@pytest.mark.skip -def test_gui_body_focus(): - pass - -@pytest.mark.skip -def test_log_timeout(): - pass - -@pytest.mark.skip -def test_log(): - pass - -@pytest.mark.skip -def test_gui_update_view(): - pass - -@pytest.mark.skip -def test_gui_update_status_bar(): - pass - -@pytest.mark.skip -def test_gui_switch_frame_body(): - pass - -@pytest.mark.skip -def test_delete_note_callback(): - pass - -@pytest.mark.skip -def test_gui_yes_no_input(): - pass - -@pytest.mark.skip -def test_gui_search_input(): - pass - -@pytest.mark.skip -def test_gui_category_input(): - pass - -@pytest.mark.skip -def test_gui_pipe_input(): - pass - -@pytest.mark.skip -def test_gui_frame_keypress(): - pass - -@pytest.mark.skip -def test_gui_init_view(): - pass - -@pytest.mark.skip -def test_gui_clear(): - pass - -@pytest.mark.skip -def test_gui_reset(): - pass - -@pytest.mark.skip -def test_gui_stop(): - pass - -@pytest.mark.skip -def test_gui(): - pass - -@pytest.mark.skip -def test_cli_list_notes(): - pass - -@pytest.mark.skip -def test_cli_note_dump(): - pass - -@pytest.mark.skip -def test_cli_dump_notes(): - pass - -@pytest.mark.skip -def test_cli_note_create(): - pass - -@pytest.mark.skip -def test_cli_note_import(): - pass - -@pytest.mark.skip -def test_cli_note_export(): - pass + os.path.exists.assert_called_once() + +def test_gui(mocker, mock_nncli): + """test starting the gui""" + nn_obj = nncli.nncli.Nncli(False) + nn_obj.gui(0) + assert nn_obj.config.state.do_gui == True + assert nn_obj.ndb.log == nn_obj.nncli_gui.log + nn_obj.nncli_gui.run.assert_called_once() + +def test_cli_list_notes(mocker, mock_nncli): + """test listing notes from the command line""" + test_note = ( + [nncli.utils.KeyValueObject(key='test_key', + note='test_note')], + [], + [] + ) + mocker.patch('nncli.utils.get_note_flags', + new=mocker.Mock(return_value='flg')) + mocker.patch('nncli.utils.get_note_title', + new=mocker.Mock(return_value='test_title')) + mocker.patch('nncli.nncli.print') + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'filter_notes', + new=mocker.Mock(return_value=test_note)) + nn_obj.cli_list_notes(False, 'test_search_string') + nncli.nncli.print.assert_called_once_with('test_key [flg] test_title') + nncli.utils.get_note_flags.assert_called_once() + nncli.utils.get_note_title.assert_called_once() + +def test_cli_note_dump(mocker, mock_nncli): + """test dumping a note to the command line""" + test_note = {'modified': 12345, + 'id': 1, + 'localkey': 1, + 'content': 'test_content'} + mocker.patch('nncli.utils.get_note_flags', + new=mocker.Mock(return_value='flg')) + mocker.patch('nncli.utils.get_note_title', + new=mocker.Mock(return_value='test_title')) + mocker.patch('nncli.utils.get_note_category', + new=mocker.Mock(return_value='test_category')) + mocker.patch('nncli.nncli.print') + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'get_note', + new=mocker.Mock(return_value=test_note)) + nn_obj.cli_note_dump(1) + assert nncli.nncli.print.call_count == 8 + nn_obj.ndb.get_note.assert_called_once_with(1) + nncli.utils.get_note_flags.assert_called_once_with(test_note) + nncli.utils.get_note_title.assert_called_once_with(test_note) + nncli.utils.get_note_category.assert_called_once_with(test_note) + +def test_failed_cli_note_dump(mocker, mock_nncli): + """test failed note dump to the command line""" + mocker.patch('nncli.utils.get_note_flags', + new=mocker.Mock(return_value='flg')) + mocker.patch('nncli.utils.get_note_title', + new=mocker.Mock(return_value='test_title')) + mocker.patch('nncli.utils.get_note_category', + new=mocker.Mock(return_value='test_category')) + mocker.patch('nncli.nncli.print') + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'get_note', + new=mocker.Mock(return_value = None)) + nn_obj.cli_note_dump(1) + nncli.nncli.print.assert_not_called() + nn_obj.ndb.get_note.assert_called_once_with(1) + nncli.utils.get_note_flags.assert_not_called() + nncli.utils.get_note_title.assert_not_called() + nncli.utils.get_note_category.assert_not_called() + +def test_cli_dump_notes(mocker, mock_nncli): + """test cli_dump_notes""" + test_notes = ( + [nncli.utils.KeyValueObject(key=1, + note={'key': 1})], + [], + [] + ) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'filter_notes', + new=mocker.Mock(return_value=test_notes)) + mocker.patch.object(nn_obj, 'cli_note_dump') + nn_obj.cli_dump_notes(False, 'test_search_string') + nn_obj.cli_note_dump.assert_called_once_with(1) + +def test_cli_note_create(mocker, mock_nncli): + """test cli_note_create""" + mocker.patch('nncli.nncli.exec_cmd_on_note', + new=mocker.Mock(return_value='test content')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'create_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_create(False, 'test title') + nncli.nncli.exec_cmd_on_note.assert_called_once() + nn_obj.ndb.create_note.assert_called_once_with('test title\n\ntest content') + nn_obj.ndb.sync_now.assert_called_once() + +def test_cli_note_create_from_stdin(mocker, mock_nncli): + """test cli_note_create reading from stdin""" + mocker.patch('sys.stdin', new=StringIO('test content')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'create_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_create(True, 'test title') + nn_obj.ndb.create_note.assert_called_once_with('test title\n\ntest content') + nn_obj.ndb.sync_now.assert_called_once() + +def test_cli_note_create_no_title(mocker, mock_nncli): + """test cli_note_create without a title""" + mocker.patch('sys.stdin', new=StringIO('test content')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'create_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_create(True, None) + nn_obj.ndb.create_note.assert_called_once_with('test content') + nn_obj.ndb.sync_now.assert_called_once() + +def test_cli_note_create_no_content(mocker, mock_nncli): + """test failed cli_note_create without content""" + mocker.patch('sys.stdin', new=StringIO(None)) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'create_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_create(True, None) + nn_obj.ndb.create_note.assert_not_called() + nn_obj.ndb.sync_now.assert_not_called() + +def test_cli_note_import(mocker, mock_nncli): + """test cli_note_import""" + mocker.patch('nncli.nncli.exec_cmd_on_note', + new=mocker.Mock(return_value='{"content": "test"}')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'import_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_import(False) + nncli.nncli.exec_cmd_on_note.assert_called_once() + nn_obj.ndb.import_note.assert_called_once_with({'content': 'test'}) + nn_obj.ndb.sync_now.assert_called_once() + +def test_cli_note_import_from_stdin(mocker, mock_nncli): + """test cli_note_import""" + mocker.patch('sys.stdin', + new=StringIO('{"content": "test"}')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'import_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + nn_obj.cli_note_import(True) + nn_obj.ndb.import_note.assert_called_once_with({'content': 'test'}) + nn_obj.ndb.sync_now.assert_called_once() + +def test_cli_note_import_json_error(mocker, mock_nncli): + """test cli_note_import failure at json decode""" + mocker.patch('nncli.nncli.exec_cmd_on_note', + new=mocker.Mock(return_value='{"content", "test"}')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'import_note') + mocker.patch.object(nn_obj.ndb, 'sync_now') + with pytest.raises(SystemExit): + nn_obj.cli_note_import(False) + nncli.nncli.exec_cmd_on_note.assert_called_once() + nn_obj.logger.log.assert_called_once() + nn_obj.ndb.import_note.assert_not_called() + nn_obj.ndb.sync_now.assert_not_called() + +def test_cli_note_import_value_error(mocker, mock_nncli): + """test cli_note_import failure""" + mocker.patch('nncli.nncli.exec_cmd_on_note', + new=mocker.Mock(return_value='{"content": "test"}')) + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'import_note', + new=mocker.Mock(side_effect=ValueError)) + mocker.patch.object(nn_obj.ndb, 'sync_now') + with pytest.raises(SystemExit): + nn_obj.cli_note_import(False) + nncli.nncli.exec_cmd_on_note.assert_called_once() + assert nn_obj.logger.log.call_count == 2 + nn_obj.ndb.import_note.assert_called_once() + nn_obj.ndb.sync_now.assert_not_called() + +def test_cli_note_export(mocker, mock_nncli): + """test exporting a note as raw JSON""" + mocker.patch('nncli.nncli.print') + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'get_note', + new=mocker.Mock(return_value={'content': 'test'})) + nn_obj.cli_note_export(1) + nn_obj.ndb.get_note.assert_called_once_with(1) + nncli.nncli.print.assert_called_once() + +def test_cli_note_export_no_note(mocker, mock_nncli): + """test failed note export (key not in DB)""" + mocker.patch('nncli.nncli.print') + nn_obj = nncli.nncli.Nncli(False) + mocker.patch.object(nn_obj.ndb, 'get_note', + new=mocker.Mock(return_value=None)) + nn_obj.cli_note_export(1) + nn_obj.ndb.get_note.assert_called_once_with(1) + nn_obj.logger.log.assert_called_once() + nncli.nncli.print.assert_not_called() @pytest.mark.skip def test_cli_export_notes(): @@ -261,19 +282,3 @@ def test_cli_note_category_rm(): @pytest.mark.skip def test_SIGINT_handler(): pass - -@pytest.mark.skip -def test_usage(): - pass - -@pytest.mark.skip -def test_version(): - pass - -@pytest.mark.skip -def test_main(): - pass - -@pytest.mark.skip -def test_nncli_start(): - pass @@ -4,8 +4,13 @@ skipsdist = True [testenv:pylint] deps = pylint +setenv = + PIPENV_NO_INHERIT = 1 + PIPENV_HIDE_EMOJIS = 1 whitelist_externals = make -commands = make lint +commands = + make test-install + make lint [testenv:coverage] deps = pipenv |