aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.pylintrc3
-rw-r--r--.travis.yml6
-rw-r--r--.vulture_whitelist.py31
-rw-r--r--Makefile6
-rw-r--r--Pipfile.lock151
-rw-r--r--nncli/__init__.py2
-rw-r--r--nncli/__main__.py1
-rw-r--r--nncli/clipboard.py28
-rw-r--r--nncli/config.py802
-rw-r--r--nncli/gui.py913
-rw-r--r--nncli/log.py40
-rw-r--r--nncli/nextcloud_note.py87
-rw-r--r--nncli/nncli.py1031
-rw-r--r--nncli/notes_db.py525
-rw-r--r--nncli/temp.py48
-rw-r--r--nncli/user_input.py7
-rw-r--r--nncli/utils.py131
-rw-r--r--nncli/view_help.py172
-rw-r--r--nncli/view_log.py30
-rw-r--r--nncli/view_note.py93
-rw-r--r--nncli/view_titles.py175
-rw-r--r--pyproject.toml1
-rw-r--r--tests/test_config.py105
-rw-r--r--tests/test_gui.py129
-rw-r--r--tests/test_nncli.py457
-rw-r--r--tox.ini7
27 files changed, 2978 insertions, 2004 deletions
diff --git a/.gitignore b/.gitignore
index c5e83b8..4fdfcc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ MANIFEST
.pytest_cache/
docs/build/
.tox
+htmlcov/
diff --git a/.pylintrc b/.pylintrc
index b2edc99..718c557 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -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)
diff --git a/Makefile b/Makefile
index 64e61a6..2f01dd7 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/tox.ini b/tox.ini
index 708e7a4..a6b847f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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