diff --git a/CHANGELOG.md b/CHANGELOG.md
index 705e44f..8a89f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.1.0-beta] - 2020-11-15
+
+### Added
+
+- Support configuration
+- Add modes "emacs" and "vim" as predefined key mappings
+
+### Fixed
+
+- Focus line jumping around when moving up / below
## [0.1.0-alpha.3] - 2020-10-18
@@ -35,7 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial commit with pyfx
-[unreleased]: https://github.com/cielong/pyfx/compare/v0.1.0-alpha.3...HEAD
+[unreleased]: https://github.com/cielong/pyfx/compare/v0.1.0-beta...HEAD
+[0.1.0-beta]: https://github.com/cielong/pyfx/compare/v0.1.0-alpha.3...v0.1.0-beta
[0.1.0-alpha.3]: https://github.com/cielong/pyfx/compare/v0.1.0-alpha.2...v0.1.0-alpha.3
[0.1.0-alpha.2]: https://github.com/cielong/pyfx/compare/v0.1.0-alpha.1...v0.1.0-alpha.2
[0.1.0-alpha.1]: https://github.com/cielong/pyfx/compare/v0.1.0-alpha...v0.1.0-alpha.1
diff --git a/MANIFEST.in b/MANIFEST.in
index dadbeb1..19df4fc 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -21,3 +21,7 @@ include tox.ini
# include Trave CI config
include .travis.yml
+
+# include pyfx config
+include src/pyfx/config/*.yml
+include src/pyfx/view/keymapper/modes/*.yml
diff --git a/Pipfile b/Pipfile
index db494b1..6bee708 100644
--- a/Pipfile
+++ b/Pipfile
@@ -19,6 +19,10 @@ sphinx-click = "*"
pytest-cov = "*"
loguru = "*"
jsonpath-ng = "*"
+yamale = "*"
+dacite = "*"
+dataclasses = "*"
+first = "*"
[requires]
python_version = "3.8"
diff --git a/Pipfile.lock b/Pipfile.lock
index 20fc37e..90707d4 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "26e7f45540799eeabec65f5ec5100eedccd1dc9023337185b45d410a7acab643"
+ "sha256": "349db7f961b5e0d26a38f6646e97a937c403a4054563a073537e9ac4f7004d2d"
},
"pipfile-spec": 6,
"requires": {
@@ -32,34 +32,34 @@
},
"asciimatics": {
"hashes": [
- "sha256:1d0871133c95fa15c603d471ebb77e39b3389877e2ff2ad5ab3bc906d81b5e8c",
- "sha256:6c7ad130a0663ecb1b3a731b82c51d6662f1d2c4760de2b8ffc9c1faba775360"
+ "sha256:4120461a3fb345638dee4fe0f8a3d3f9b6d2d2e003f95c5f914523f94463158d",
+ "sha256:83c8ead386cdcc5fd4cebb4289b0d6da61164e44dfdfff3776b66e6bcfef2e3d"
],
"index": "pypi",
- "version": "==1.11.0"
+ "version": "==1.12.0"
},
"attrs": {
"hashes": [
- "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
- "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
+ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+ "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==20.2.0"
+ "version": "==20.3.0"
},
"babel": {
"hashes": [
- "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
- "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
+ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5",
+ "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.0"
+ "version": "==2.9.0"
},
"certifi": {
"hashes": [
- "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
- "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
+ "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
+ "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
],
- "version": "==2020.6.20"
+ "version": "==2020.11.8"
},
"chardet": {
"hashes": [
@@ -116,6 +116,22 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.3"
},
+ "dacite": {
+ "hashes": [
+ "sha256:764c96e0304cb189628686689a163a6a3a8ce7bf3465f0a2d882a8b42f88108f",
+ "sha256:f7f269647ede90f8702728eb7dcb972051511c81b853a93c962fbd31f1753b9f"
+ ],
+ "index": "pypi",
+ "version": "==1.5.1"
+ },
+ "dataclasses": {
+ "hashes": [
+ "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f",
+ "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"
+ ],
+ "index": "pypi",
+ "version": "==0.6"
+ },
"decorator": {
"hashes": [
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
@@ -145,10 +161,18 @@
],
"version": "==3.0.12"
},
+ "first": {
+ "hashes": [
+ "sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86",
+ "sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"
+ ],
+ "index": "pypi",
+ "version": "==2.0.2"
+ },
"future": {
"hashes": [
- "sha256:0962b60de4ca66f0b0dbe01cbcb83fe5606b1bbf329f4bdb5bccc122cc555386",
- "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
+ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d",
+ "sha256:0962b60de4ca66f0b0dbe01cbcb83fe5606b1bbf329f4bdb5bccc122cc555386"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2"
@@ -373,13 +397,29 @@
],
"version": "==2020.4"
},
+ "pyyaml": {
+ "hashes": [
+ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
+ "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
+ "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
+ "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
+ "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
+ "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
+ "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
+ "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
+ "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
+ "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
+ "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
+ ],
+ "version": "==5.3.1"
+ },
"requests": {
"hashes": [
- "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
- "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
+ "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
+ "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
- "version": "==2.24.0"
+ "version": "==2.25.0"
},
"six": {
"hashes": [
@@ -398,11 +438,11 @@
},
"sphinx": {
"hashes": [
- "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8",
- "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"
+ "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300",
+ "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"
],
"index": "pypi",
- "version": "==3.2.1"
+ "version": "==3.3.1"
},
"sphinx-click": {
"hashes": [
@@ -478,11 +518,11 @@
},
"urllib3": {
"hashes": [
- "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
- "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
+ "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
+ "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
- "version": "==1.25.11"
+ "version": "==1.26.2"
},
"urwid": {
"hashes": [
@@ -505,6 +545,13 @@
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
+ },
+ "yamale": {
+ "hashes": [
+ "sha256:bec53aa08e29c26a9a47c0f5d5cb5632e7ac8aff619a4727acfe490042c83a5f"
+ ],
+ "index": "pypi",
+ "version": "==3.0.4"
}
},
"develop": {}
diff --git a/README.md b/README.md
index 324201e..1c9b457 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ from pyfx import Controller
Controller().run_with_data(data)
```
#### Import *pyfx*'s Native JSON Library and Integrate with Your Own TUI
-You can also import *pyfx* native JSON lib to integrate it into your own urwid TUI, e.g. [view_window.py](https://github.com/cielong/pyfx/blob/master/src/pyfx/view/components/view_window.py).
+You can also import *pyfx* native JSON lib to integrate it into your own urwid TUI, e.g. [json_browser.py](https://github.com/cielong/pyfx/blob/master/src/pyfx/view/components/json_browser/json_browser.py).
```python
from pyfx.view.json_lib import JSONListBox, JSONListWalker, NodeFactory
@@ -55,20 +55,71 @@ listbox = JSONListBox(JSONListWalker(top_node))
# use listbox in your own TUI
...
```
+## Configuration
+*pyfx* can be configured using YAML, the config file is either passed directly through CLI option or automatically
+loaded in predefined config folderq.
-### Key Mappings
+If *pyfx* is invoked without `-c / --config` option, it will search config file in with the following order:
+1. ~/.config/pyfx/config.yml
+2. PYTHON_DIR/site-packages/pyfx/config/config.yml
+
+### Predefined Key Mappings
+Key mapping is configured with the following configuration schema
+```
+keymap:
+ mode: string, accepted_options = ["basic" (The default) | "emacs" | "vim"]
+```
+#### Basic Mode
| Key | Function |
|------------------|---------------------------------------------------|
-| **Main Window** |
-| q | exit pyfx |
-| **View Window** |
+| q | exit pyfx (except in Query Bar) |
+| **JSON Browser** |
+| up | move cursor up one line |
+| down | move cursor down one line |
| enter | toggle expansion |
-| up/ctrl p | move cursor up one line |
-| down/ctrl n | move cursor down one line |
-| **Query Window** |
| . | enter query window (used to input JSONPath query) |
-| enter | apply JSONPath query and switch to View Window |
-| esc | apply JSONPath query and exit Query Window |
+| **Query Bar** |
+| enter | apply JSONPath query and switch to JSON Browser |
+| esc | cancel query and restore to state before query |
+
+#### Emacs Mode
+To enable, add the following configuration in your config file:
+```yaml
+keymap:
+ mode: "emacs"
+```
+##### Mapped Keys
+| Key | Function |
+|------------------|---------------------------------------------------|
+| q | exit pyfx (except in Query Bar) |
+| **JSON Browser** |
+| up / ctrl p | move cursor up one line |
+| down / ctrl n | move cursor down one line |
+| enter | toggle expansion |
+| . / meta x | enter query window (used to input JSONPath query) |
+| **Query Bar** |
+| enter | apply JSONPath query and switch to JSON Browser |
+| ctrl g | cancel query and restore to state before query |
+
+#### Vim Mode
+To enable, add the following configuration in your config file:
+```yaml
+keymap:
+ mode: "vim"
+```
+##### Mapped Keys
+| Key | Function |
+|------------------|---------------------------------------------------|
+| q | exit pyfx (except in Query Bar) |
+| **JSON Browser** |
+| up / k | move cursor up one line |
+| down / j | move cursor down one line |
+| enter | toggle expansion |
+| . / : | enter query window (used to input JSONPath query) |
+| **Query Bar** |
+| enter | apply JSONPath query and switch to JSON Browser |
+| esc | cancel query and restore to state before query |
+
## Full Documentation
Please visit [Documentation](https://python-fx.readthedocs.io/en/latest/)
diff --git a/VERSION b/VERSION
index d594bc9..a67658d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0-alpha.3
+0.1.0-beta
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 4664bc9..8136220 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -11,3 +11,7 @@ sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
urwid==2.1.2
loguru==0.5.3
+yamale==3.0.4
+dataclasses==0.6
+dacite==1.5.1
+first==2.0.2
diff --git a/setup.py b/setup.py
index d2679c3..5c3e989 100644
--- a/setup.py
+++ b/setup.py
@@ -8,10 +8,10 @@
here = pathlib.Path(__file__).parent.resolve()
-# Get the version from the VERSION file
+# get the version from the VERSION file
version = (here / 'VERSION').read_text(encoding='utf-8')
-# Get the long description from the README file
+# get the long description from the README file
long_description = (here / 'README.md').read_text(encoding='utf-8')
setup(
@@ -27,13 +27,19 @@
keywords="fx, pyfx, json viewer, tui",
packages=find_packages('src'),
package_dir={'': 'src'},
+ package_data={'pyfx': ['config/*.yml', 'view/keymapper/modes/*.yml']},
+ include_package_data=True,
py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')],
install_requires=[
'click',
'urwid',
'overrides',
'jsonpath-ng',
- 'loguru'
+ 'loguru',
+ 'yamale',
+ 'dataclasses',
+ 'dacite',
+ 'first'
],
setup_requires=[
'pytest-runner',
@@ -42,7 +48,7 @@
"console_scripts": ["pyfx=pyfx.cli:main"]
},
classifiers=[
- "Development Status :: 3 - Alpha",
+ "Development Status :: 4 - Beta",
"Programming Language :: Python :: 3 :: Only",
"Environment :: Console",
"Operating System :: POSIX",
diff --git a/src/pyfx/cli.py b/src/pyfx/cli.py
index dc0f7a5..3663914 100644
--- a/src/pyfx/cli.py
+++ b/src/pyfx/cli.py
@@ -1,25 +1,29 @@
import click
+from .config.config_parser import parse
from .core import Controller
-from .logging import log_config
+from .logging import setup_logger
STDIN = 'stdin'
@click.command(name="pyfx")
+@click.option("-c", "--config-file", type=click.Path(exists=True))
@click.argument("file", type=click.Path(exists=True), nargs=-1)
-def main(file):
+def main(file, config_file):
"""
pyfx command line entry point.
It loads data from a JSON file FILE and opens pyfx UI for browsing.
"""
- log_config()
+ setup_logger()
+ config = parse(config_file)
if len(file) > 1:
- raise ValueError("pyfx does not support multi JSON files.")
+ raise click.BadArgumentUsage("pyfx does not support multi JSON files.")
+ controller = Controller(config)
if len(file) == 1:
- Controller().run_with_file(file[0])
+ controller.run_with_file(file[0])
else:
text_stream = click.get_text_stream(STDIN)
- Controller().run_with_text_stream(text_stream)
+ controller.run_with_text_stream(text_stream)
diff --git a/src/pyfx/cli_utils.py b/src/pyfx/cli_utils.py
new file mode 100644
index 0000000..6c0a3c9
--- /dev/null
+++ b/src/pyfx/cli_utils.py
@@ -0,0 +1,18 @@
+import functools
+
+import click
+
+
+def exit_on_exception(func):
+ """
+ A decorator which exit the current click application when there's unexpected error
+ and print the error message to the stderr.
+ """
+ # noinspection PyBroadException
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ raise click.ClickException(e)
+ return wrapper
diff --git a/src/pyfx/config/__init__.py b/src/pyfx/config/__init__.py
new file mode 100644
index 0000000..088b2c2
--- /dev/null
+++ b/src/pyfx/config/__init__.py
@@ -0,0 +1,2 @@
+from .config import Configuration
+from .config_parser import ConfigurationParser
diff --git a/src/pyfx/config/config.py b/src/pyfx/config/config.py
new file mode 100644
index 0000000..9acaa41
--- /dev/null
+++ b/src/pyfx/config/config.py
@@ -0,0 +1,8 @@
+from dataclasses import dataclass
+
+from ..view.keymapper import KeyMapperConfiguration
+
+
+@dataclass
+class Configuration:
+ keymap: KeyMapperConfiguration = KeyMapperConfiguration()
diff --git a/src/pyfx/config/config.yml b/src/pyfx/config/config.yml
new file mode 100644
index 0000000..7ef129b
--- /dev/null
+++ b/src/pyfx/config/config.yml
@@ -0,0 +1,2 @@
+keymap:
+ mode: "basic"
diff --git a/src/pyfx/config/config_parser.py b/src/pyfx/config/config_parser.py
new file mode 100644
index 0000000..1b3a76c
--- /dev/null
+++ b/src/pyfx/config/config_parser.py
@@ -0,0 +1,44 @@
+import pathlib
+
+import dacite
+import yamale
+from first import first
+
+from .config import Configuration
+from ..cli_utils import exit_on_exception
+
+
+@exit_on_exception
+def parse(config_file):
+ return ConfigurationParser().parse(config_file)
+
+
+class ConfigurationParser:
+
+ __CLASS_DIR = pathlib.Path(__file__).parent.resolve()
+ __SCHEMA_PATH = __CLASS_DIR / "schema.yml"
+
+ __CONFIG_PATHS = [
+ pathlib.Path.home() / ".config" / "pyfx" / "config.yml",
+ __CLASS_DIR / "config.yml"
+ ]
+
+ def __init__(self):
+ self._schema = self.__load_schema()
+
+ def parse(self, config_file=None):
+ config = self.__load_config(config_file)
+ yamale.validate(self._schema, config)
+ # config is composed as [(config, path)...]
+ return dacite.from_dict(data_class=Configuration, data=config[0][0])
+
+ @staticmethod
+ def __load_schema():
+ return yamale.make_schema(ConfigurationParser.__SCHEMA_PATH)
+
+ # noinspection PyBroadException
+ @staticmethod
+ def __load_config(config_file):
+ if config_file is None:
+ config_file = first(ConfigurationParser.__CONFIG_PATHS, key=lambda path: path.exists())
+ return yamale.make_data(config_file)
diff --git a/src/pyfx/config/schema.yml b/src/pyfx/config/schema.yml
new file mode 100644
index 0000000..c99b268
--- /dev/null
+++ b/src/pyfx/config/schema.yml
@@ -0,0 +1,2 @@
+keymap:
+ mode: str()
diff --git a/src/pyfx/core.py b/src/pyfx/core.py
index 4796505..bad5fcc 100644
--- a/src/pyfx/core.py
+++ b/src/pyfx/core.py
@@ -1,3 +1,4 @@
+from .config import Configuration
from .model import Model
from .view import View
@@ -7,9 +8,9 @@ class Controller:
*pyfx* controller, the main entry point of pyfx library.
"""
- def __init__(self, config_file: str = None):
- self._config = config_file
- self._view = View(self)
+ def __init__(self, config=Configuration()):
+ self._config = config
+ self._view = View(self, config)
self._model = Model(self)
def run_with_file(self, filename):
diff --git a/src/pyfx/logging.py b/src/pyfx/logging.py
index 0e49060..07149d6 100644
--- a/src/pyfx/logging.py
+++ b/src/pyfx/logging.py
@@ -4,7 +4,7 @@
from loguru import logger
-def log_config():
+def setup_logger():
logger.remove()
logger.add("/tmp/pyfx.log", level='DEBUG', rotation='5MB', retention="10 days",
format="{time} {module}.{function} {message}")
diff --git a/src/pyfx/view/components/__init__.py b/src/pyfx/view/components/__init__.py
index aad8fc0..246b7b7 100644
--- a/src/pyfx/view/components/__init__.py
+++ b/src/pyfx/view/components/__init__.py
@@ -4,6 +4,6 @@
Each component is a single unit rendered in *pyfx*.
"""
from .autocomplete_popup import AutoCompletePopUp
-from .help_window import HelpWindow
-from .query_window import QueryWindow
-from .view_window import ViewWindow
+from .help_bar import HelpBar
+from .query_bar import QueryBar
+from .json_browser import JSONBrowser
diff --git a/src/pyfx/view/components/autocomplete_popup/__init__.py b/src/pyfx/view/components/autocomplete_popup/__init__.py
new file mode 100644
index 0000000..a553dc4
--- /dev/null
+++ b/src/pyfx/view/components/autocomplete_popup/__init__.py
@@ -0,0 +1 @@
+from .autocomplete_popup import AutoCompletePopUp
diff --git a/src/pyfx/view/components/autocomplete_popup.py b/src/pyfx/view/components/autocomplete_popup/autocomplete_popup.py
similarity index 82%
rename from src/pyfx/view/components/autocomplete_popup.py
rename to src/pyfx/view/components/autocomplete_popup/autocomplete_popup.py
index 3632885..1402fc2 100644
--- a/src/pyfx/view/components/autocomplete_popup.py
+++ b/src/pyfx/view/components/autocomplete_popup/autocomplete_popup.py
@@ -1,7 +1,16 @@
+from enum import Enum
+
import urwid
from overrides import overrides
-from ..common import SelectableText
+from ...common import SelectableText
+
+
+class AutoCompletePopUpKeys(Enum):
+ CURSOR_UP = "up"
+ CURSOR_DOWN = "down"
+ SELECT = "enter"
+ CANCEL = "esc"
class AutoCompletePopUp(urwid.WidgetWrap):
@@ -12,10 +21,12 @@ class AutoCompletePopUp(urwid.WidgetWrap):
# predefined constants to constrain pop up window size
MAX_HEIGHT = 5
- def __init__(self, popup_launcher, controller, query_window, prefix, options):
+ def __init__(self, controller, keymapper, popup_launcher, query_window, prefix, options):
self._popup_launcher = popup_launcher
self._query_window = query_window
self._controller = controller
+ self._keymapper = keymapper
+
self._prefix = prefix
self._options = options
super().__init__(self._load_widget())
@@ -55,17 +66,22 @@ def _update_query(self):
@overrides
def keypress(self, size, key):
+ key = self._keymapper.key(key)
key = super().keypress(size, key)
- if key == 'enter':
+
+ if key == AutoCompletePopUpKeys.SELECT.value:
self._update_query()
self._popup_launcher.close_pop_up()
self._controller.query(self._query_window.get_text())
return None
- elif key in ('esc', 'ctrl g'):
+
+ elif key == AutoCompletePopUpKeys.CANCEL.value:
self._popup_launcher.close_pop_up()
return None
+
elif key is not None:
# forward key to the query window if not handled by auto-complete
self._popup_launcher.close_pop_up()
key = self._query_window.keypress_internal(key)
+
return key
diff --git a/src/pyfx/view/components/autocomplete_popup/autocomplete_popup_keymapper.py b/src/pyfx/view/components/autocomplete_popup/autocomplete_popup_keymapper.py
new file mode 100644
index 0000000..0230eb6
--- /dev/null
+++ b/src/pyfx/view/components/autocomplete_popup/autocomplete_popup_keymapper.py
@@ -0,0 +1,25 @@
+from dataclasses import dataclass
+
+from overrides import overrides
+
+from .autocomplete_popup import AutoCompletePopUpKeys
+from ...keymapper import AbstractComponentKeyMapper
+
+
+@dataclass(frozen=True)
+class AutoCompletePopUpKeyMapper(AbstractComponentKeyMapper):
+
+ cursor_up: str = "up"
+ cursor_down: str = "down"
+ select: str = "enter"
+ cancel: str = "esc"
+
+ @property
+ @overrides
+ def mapped_key(self):
+ return {
+ self.cursor_up: AutoCompletePopUpKeys.CURSOR_UP,
+ self.cursor_down: AutoCompletePopUpKeys.CURSOR_DOWN,
+ self.select: AutoCompletePopUpKeys.SELECT,
+ self.cancel: AutoCompletePopUpKeys.CANCEL
+ }
diff --git a/src/pyfx/view/components/help_bar/__init__.py b/src/pyfx/view/components/help_bar/__init__.py
new file mode 100644
index 0000000..85a6091
--- /dev/null
+++ b/src/pyfx/view/components/help_bar/__init__.py
@@ -0,0 +1 @@
+from .help_bar import HelpBar
diff --git a/src/pyfx/view/components/help_window.py b/src/pyfx/view/components/help_bar/help_bar.py
similarity index 71%
rename from src/pyfx/view/components/help_window.py
rename to src/pyfx/view/components/help_bar/help_bar.py
index e4cd8b0..9c0ff7a 100644
--- a/src/pyfx/view/components/help_window.py
+++ b/src/pyfx/view/components/help_bar/help_bar.py
@@ -1,12 +1,12 @@
import urwid
-class HelpWindow(urwid.WidgetWrap):
+class HelpBar(urwid.WidgetWrap):
HELP_TEXT = [
('title', "Pyfx"), " ", "UP, DOWN, ENTER, Q",
]
def __init__(self, manager):
self._manager = manager
- self._text_widget = urwid.Text(HelpWindow.HELP_TEXT)
+ self._text_widget = urwid.Text(HelpBar.HELP_TEXT)
super().__init__(urwid.AttrWrap(self._text_widget, "foot"))
diff --git a/src/pyfx/view/components/help_page/__init__.py b/src/pyfx/view/components/help_page/__init__.py
new file mode 100644
index 0000000..bb12a6e
--- /dev/null
+++ b/src/pyfx/view/components/help_page/__init__.py
@@ -0,0 +1 @@
+from .detailed_help_page import DetailedHelpPage
diff --git a/src/pyfx/view/components/help_details_window.py b/src/pyfx/view/components/help_page/detailed_help_page.py
similarity index 56%
rename from src/pyfx/view/components/help_details_window.py
rename to src/pyfx/view/components/help_page/detailed_help_page.py
index 1454937..b392c51 100644
--- a/src/pyfx/view/components/help_details_window.py
+++ b/src/pyfx/view/components/help_page/detailed_help_page.py
@@ -1,12 +1,12 @@
import urwid
-class HelpDetailsWindow(urwid.WidgetWrap):
+class DetailedHelpPage(urwid.WidgetWrap):
HELP_TEXT = ""
def __init__(self):
self._text_widget = urwid.AttrWrap(
- urwid.ListBox(urwid.SimpleListWalker([urwid.Text(HelpDetailsWindow.HELP_TEXT)])),
+ urwid.ListBox(urwid.SimpleListWalker([urwid.Text(DetailedHelpPage.HELP_TEXT)])),
"body"
)
super().__init__(self._text_widget)
diff --git a/src/pyfx/view/components/json_browser/__init__.py b/src/pyfx/view/components/json_browser/__init__.py
new file mode 100644
index 0000000..ef353f7
--- /dev/null
+++ b/src/pyfx/view/components/json_browser/__init__.py
@@ -0,0 +1 @@
+from .json_browser import JSONBrowser
diff --git a/src/pyfx/view/components/view_window.py b/src/pyfx/view/components/json_browser/json_browser.py
similarity index 55%
rename from src/pyfx/view/components/view_window.py
rename to src/pyfx/view/components/json_browser/json_browser.py
index b813eb9..3fb1cbc 100644
--- a/src/pyfx/view/components/view_window.py
+++ b/src/pyfx/view/components/json_browser/json_browser.py
@@ -1,24 +1,34 @@
+from enum import Enum
+
import urwid
from overrides import overrides
-from ..json_lib import JSONListBox
-from ..json_lib import JSONListWalker
-from ..json_lib import NodeFactory
+from ...json_lib import JSONListBox
+from ...json_lib import JSONListWalker
+from ...json_lib import NodeFactory
+
+
+class JSONBrowserKeys(Enum):
+ # keys for json lib
+ CURSOR_UP = "up"
+ CURSOR_DOWN = "down"
+ TOGGLE_EXPANSION = "enter"
+ # keys for switching window
+ OPEN_QUERY_BAR = "."
-class ViewWindow(urwid.WidgetWrap):
+class JSONBrowser(urwid.WidgetWrap):
"""
Window to display JSON contents.
"""
- def __init__(self, manager, data=""):
+ def __init__(self, manager, keymapper, data=""):
+ self._keymapper = keymapper
self._manager = manager
- data = ViewWindow._validate(data)
self._top_node = NodeFactory.create_node("", data, display_key=False)
super().__init__(self._load_widget())
def set_top_node(self, data):
- data = ViewWindow._validate(data)
self._top_node = NodeFactory.create_node("", data, display_key=False)
self._refresh()
@@ -29,18 +39,12 @@ def _load_widget(self):
def _refresh(self):
self._w = self._load_widget()
- @staticmethod
- def _validate(data):
- """
- Validates input data and reset it into empty string if it is None.
- :param data: JSON valid data, could be `dict`, `list`, `int`, `str`, `float` etc.
- :return: original data, or empty string if None
- """
- return data if data else ""
-
@overrides
def keypress(self, size, key):
+ key = self._keymapper.key(key)
key = super().keypress(size, key)
- if key == '.':
+
+ if key == JSONBrowserKeys.OPEN_QUERY_BAR.value:
self._manager.enter_query_window()
+
return key
diff --git a/src/pyfx/view/components/json_browser/json_browser_keymapper.py b/src/pyfx/view/components/json_browser/json_browser_keymapper.py
new file mode 100644
index 0000000..c53a22f
--- /dev/null
+++ b/src/pyfx/view/components/json_browser/json_browser_keymapper.py
@@ -0,0 +1,24 @@
+from dataclasses import dataclass
+
+from overrides import overrides
+
+from .json_browser import JSONBrowserKeys
+from ...keymapper import AbstractComponentKeyMapper
+
+
+@dataclass(frozen=True)
+class JSONBrowserKeyMapper(AbstractComponentKeyMapper):
+ cursor_up: str = "up"
+ cursor_down: str = "down"
+ toggle_expansion: str = "enter"
+ open_query_bar: str = "."
+
+ @property
+ @overrides
+ def mapped_key(self):
+ return {
+ self.cursor_up: JSONBrowserKeys.CURSOR_UP,
+ self.cursor_down: JSONBrowserKeys.CURSOR_DOWN,
+ self.toggle_expansion: JSONBrowserKeys.TOGGLE_EXPANSION,
+ self.open_query_bar: JSONBrowserKeys.OPEN_QUERY_BAR
+ }
diff --git a/src/pyfx/view/components/query_bar/__init__.py b/src/pyfx/view/components/query_bar/__init__.py
new file mode 100644
index 0000000..6960073
--- /dev/null
+++ b/src/pyfx/view/components/query_bar/__init__.py
@@ -0,0 +1 @@
+from .query_bar import QueryBar
diff --git a/src/pyfx/view/components/query_window.py b/src/pyfx/view/components/query_bar/query_bar.py
similarity index 72%
rename from src/pyfx/view/components/query_window.py
rename to src/pyfx/view/components/query_bar/query_bar.py
index 95fc3a9..23fd6d9 100644
--- a/src/pyfx/view/components/query_window.py
+++ b/src/pyfx/view/components/query_bar/query_bar.py
@@ -1,19 +1,27 @@
+from enum import Enum
+
import urwid
from overrides import overrides
-class QueryWindow(urwid.WidgetWrap):
+class QueryBarKeys(Enum):
+ QUERY = "enter"
+ CANCEL = "esc"
+
+
+class QueryBar(urwid.WidgetWrap):
"""
Query window for `pyfx` to input JSONPath query
"""
JSONPATH_START = "$"
- def __init__(self, manager, controller):
+ def __init__(self, manager, controller, keymapper):
self._manager = manager
self._controller = controller
+ self._keymapper = keymapper
self._edit_widget = urwid.Edit()
- self._edit_widget.insert_text(QueryWindow.JSONPATH_START)
+ self._edit_widget.insert_text(QueryBar.JSONPATH_START)
super().__init__(urwid.AttrWrap(self._edit_widget, None, "focus"))
def setup(self):
@@ -36,11 +44,15 @@ def keypress_internal(self, key):
@overrides
def keypress(self, size, key):
+ key = self._keymapper.key(key)
key = super().keypress(size, key)
- if key == 'enter':
+
+ if key == QueryBarKeys.QUERY.value:
self._controller.query(self.get_text())
self._manager.enter_view_window()
- elif key == 'esc':
+
+ if key == QueryBarKeys.CANCEL.value:
self._controller.query(self.get_text())
- self._manager.exit_query_window()
+ self._manager.enter_view_window()
+
return key
diff --git a/src/pyfx/view/components/query_bar/query_bar_keymapper.py b/src/pyfx/view/components/query_bar/query_bar_keymapper.py
new file mode 100644
index 0000000..e055703
--- /dev/null
+++ b/src/pyfx/view/components/query_bar/query_bar_keymapper.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+
+from overrides import overrides
+
+from .query_bar import QueryBarKeys
+from ...keymapper import AbstractComponentKeyMapper
+
+
+@dataclass(frozen=True)
+class QueryBarKeyMapper(AbstractComponentKeyMapper):
+
+ query: str = "enter"
+ cancel: str = "esc"
+
+ @property
+ @overrides
+ def mapped_key(self):
+ return {
+ self.query: QueryBarKeys.QUERY,
+ self.cancel: QueryBarKeys.CANCEL
+ }
diff --git a/src/pyfx/view/json_lib/__init__.py b/src/pyfx/view/json_lib/__init__.py
index b01d3ff..37ebb2b 100644
--- a/src/pyfx/view/json_lib/__init__.py
+++ b/src/pyfx/view/json_lib/__init__.py
@@ -41,5 +41,5 @@
For each leaf node, it implements :py:class:`.json_simple_node.JSONSimpleNode`.
"""
from .json_listbox import JSONListBox
-from .json_listbox import JSONListWalker
+from .json_listwalker import JSONListWalker
from .node_factory import NodeFactory
diff --git a/src/pyfx/view/json_lib/json_listbox.py b/src/pyfx/view/json_lib/json_listbox.py
index 717ac24..8f2471d 100644
--- a/src/pyfx/view/json_lib/json_listbox.py
+++ b/src/pyfx/view/json_lib/json_listbox.py
@@ -1,19 +1,13 @@
import urwid
from loguru import logger
from overrides import overrides
-from urwid import CURSOR_UP, CURSOR_DOWN, ACTIVATE
-
-from .json_listwalker import JSONListWalker
class JSONListBox(urwid.ListBox):
"""
a ListBox with special handling for navigation and collapsing of JSONWidgets
"""
-
- def __init__(self,
- walker: JSONListWalker
- ):
+ def __init__(self, walker):
# set body to JSONListWalker
super().__init__(walker)
@@ -35,13 +29,13 @@ def keypress(self, size, key):
self.make_cursor_visible((maxcol, maxrow))
return None
- if self._command_map[key] == CURSOR_UP:
+ if key == "up":
self.move_focus_to_prev_line(size)
- elif self._command_map[key] == CURSOR_DOWN:
+ elif key == "down":
self.move_focus_to_next_line(size)
- elif self._command_map[key] == ACTIVATE:
+ elif key == "enter":
self.toggle_collapse_on_focused_parent(size)
return key
@@ -55,6 +49,8 @@ def toggle_collapse_on_focused_parent(self, size):
if not widget.is_expandable():
return
+ position.toggle_expanded()
+
if position.is_end_node() and (not position.is_expanded()):
# switch to unexpanded widget when collapse on end widget
self.change_focus(size, position.get_start_node())
diff --git a/src/pyfx/view/json_lib/json_widget.py b/src/pyfx/view/json_lib/json_widget.py
index df3a994..8381acd 100644
--- a/src/pyfx/view/json_lib/json_widget.py
+++ b/src/pyfx/view/json_lib/json_widget.py
@@ -1,8 +1,5 @@
import urwid
from overrides import overrides
-from urwid import ACTIVATE
-
-from ..common import SelectableText
class JSONWidget(urwid.WidgetWrap):
@@ -168,17 +165,9 @@ def selectable(self):
@overrides
def keypress(self, size, key):
"""
- Handle expand & collapse requests (non-leaf nodes)
+ Delegate keypress into inner widget
"""
- if not self._expandable:
- return key
-
if self._w.selectable():
key = self._w.keypress(size, key)
- if key is None:
- return None
- if self._command_map[key] == ACTIVATE:
- # toggle expanded
- self._node.toggle_expanded()
return key
diff --git a/src/pyfx/view/keymapper/__init__.py b/src/pyfx/view/keymapper/__init__.py
new file mode 100644
index 0000000..1f81839
--- /dev/null
+++ b/src/pyfx/view/keymapper/__init__.py
@@ -0,0 +1,3 @@
+from .abstract_component_keymapper import AbstractComponentKeyMapper
+from .keymapper_config_parser import KeyMapperConfigurationParser
+from .keymapper_config import KeyMapperConfiguration
diff --git a/src/pyfx/view/keymapper/abstract_component_keymapper.py b/src/pyfx/view/keymapper/abstract_component_keymapper.py
new file mode 100644
index 0000000..c359071
--- /dev/null
+++ b/src/pyfx/view/keymapper/abstract_component_keymapper.py
@@ -0,0 +1,19 @@
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True)
+class AbstractComponentKeyMapper(ABC):
+ """
+ Base Key Mapping
+ """
+
+ @property
+ @abstractmethod
+ def mapped_key(self):
+ raise NotImplementedError(f"mapped_key is not implemented in {type(self)}")
+
+ def key(self, key):
+ if key in self.mapped_key:
+ return self.mapped_key[key].value
+ return key
diff --git a/src/pyfx/view/keymapper/keymapper.py b/src/pyfx/view/keymapper/keymapper.py
new file mode 100644
index 0000000..868dd41
--- /dev/null
+++ b/src/pyfx/view/keymapper/keymapper.py
@@ -0,0 +1,12 @@
+from dataclasses import dataclass
+
+from ..components.autocomplete_popup.autocomplete_popup_keymapper import AutoCompletePopUpKeyMapper
+from ..components.json_browser.json_browser_keymapper import JSONBrowserKeyMapper
+from ..components.query_bar.query_bar_keymapper import QueryBarKeyMapper
+
+
+@dataclass(frozen=True)
+class KeyMapper:
+ json_browser: JSONBrowserKeyMapper = JSONBrowserKeyMapper()
+ query_bar: QueryBarKeyMapper = QueryBarKeyMapper()
+ autocomplete_popup: AutoCompletePopUpKeyMapper = AutoCompletePopUpKeyMapper()
diff --git a/src/pyfx/view/keymapper/keymapper_config.py b/src/pyfx/view/keymapper/keymapper_config.py
new file mode 100644
index 0000000..ace1e36
--- /dev/null
+++ b/src/pyfx/view/keymapper/keymapper_config.py
@@ -0,0 +1,6 @@
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True)
+class KeyMapperConfiguration:
+ mode: str = "basic"
diff --git a/src/pyfx/view/keymapper/keymapper_config_parser.py b/src/pyfx/view/keymapper/keymapper_config_parser.py
new file mode 100644
index 0000000..186489f
--- /dev/null
+++ b/src/pyfx/view/keymapper/keymapper_config_parser.py
@@ -0,0 +1,38 @@
+import pathlib
+
+import dacite
+import yaml
+
+try:
+ from yaml import CLoader as Loader, CDumper as Dumper
+except ImportError:
+ from yaml import Loader, Dumper
+
+from .keymapper import KeyMapper
+
+
+HERE = pathlib.Path(__file__).parent.resolve()
+
+
+class KeyMapperConfigurationParser:
+ """ Configuration parser that create keymappers from configuration """
+ modes = {
+ "basic": None,
+ "emacs": HERE / "modes" / "emacs.yml",
+ "vim": HERE / "modes" / "vim.yml"
+ }
+
+ @staticmethod
+ def create_keymapper(config):
+ mode_config = KeyMapperConfigurationParser.modes[config.mode]
+ if mode_config is None:
+ return KeyMapper()
+
+ keymapper = KeyMapperConfigurationParser._load_yaml(mode_config)
+ return dacite.from_dict(data_class=KeyMapper, data=keymapper)
+
+ @staticmethod
+ def _load_yaml(mode_config):
+ with mode_config.open() as f:
+ return yaml.load(f, Loader=Loader)
+
diff --git a/src/pyfx/view/keymapper/modes/emacs.yml b/src/pyfx/view/keymapper/modes/emacs.yml
new file mode 100644
index 0000000..0394fee
--- /dev/null
+++ b/src/pyfx/view/keymapper/modes/emacs.yml
@@ -0,0 +1,15 @@
+autocomplete_popup:
+ cursor_down: "meta n"
+ cursor_up: "meta p"
+ select: "enter"
+ cancel: "ctrl g"
+
+json_browser:
+ cursor_down: "ctrl n"
+ cursor_up: "ctrl p"
+ toggle_expansion: "enter"
+ open_query_bar: "meta x"
+
+query_bar:
+ query: "enter"
+ cancel: "ctrl g"
diff --git a/src/pyfx/view/keymapper/modes/vim.yml b/src/pyfx/view/keymapper/modes/vim.yml
new file mode 100644
index 0000000..c18fa5a
--- /dev/null
+++ b/src/pyfx/view/keymapper/modes/vim.yml
@@ -0,0 +1,15 @@
+autocomplete_popup:
+ cursor_down: "j"
+ cursor_up: "k"
+ select: "enter"
+ cancel: "esc"
+
+json_browser:
+ cursor_down: "j"
+ cursor_up: "k"
+ toggle_expansion: "enter"
+ open_query_bar: ":"
+
+query_bar:
+ query: "enter"
+ cancel: "esc"
\ No newline at end of file
diff --git a/src/pyfx/view/view_frame.py b/src/pyfx/view/view_frame.py
index e509d1f..bf88ddd 100644
--- a/src/pyfx/view/view_frame.py
+++ b/src/pyfx/view/view_frame.py
@@ -5,7 +5,6 @@
from overrides import overrides
from .common import PopUpLauncher
-from .components import AutoCompletePopUp
class ViewFrame(PopUpLauncher):
@@ -13,8 +12,8 @@ class ViewFrame(PopUpLauncher):
A wrapper of the frame as the main UI of `pyfx`.
"""
- def __init__(self, controller, body, footer):
- self._controller = controller
+ def __init__(self, body, footer, popup_factory):
+ self.popup_factory = popup_factory
super().__init__(urwid.Frame(body, footer=footer))
def change_widget(self, widget, area):
@@ -31,7 +30,7 @@ def change_focus(self, area):
@overrides
def create_pop_up(self, widget, prefix, options):
- return AutoCompletePopUp(self, self._controller, widget, prefix, options)
+ return self.popup_factory(self, widget, prefix, options)
@overrides
def get_pop_up_parameters(self, size):
diff --git a/src/pyfx/view/view_manager.py b/src/pyfx/view/view_manager.py
index 00c98fc..9a64fdd 100644
--- a/src/pyfx/view/view_manager.py
+++ b/src/pyfx/view/view_manager.py
@@ -1,9 +1,11 @@
import urwid
from loguru import logger
-from .components import HelpWindow
-from .components import QueryWindow
-from .components import ViewWindow
+from .components import AutoCompletePopUp
+from .components import HelpBar
+from .components import JSONBrowser
+from .components import QueryBar
+from .keymapper import KeyMapperConfigurationParser
from .view_frame import FocusArea
from .view_frame import ViewFrame
@@ -28,21 +30,27 @@ class View:
('focus', 'light gray', 'dark blue', 'standout')
]
- def __init__(self, controller):
+ def __init__(self, controller, config):
"""
:param controller: The controller/presenter used in `pyfx` to initialize data change.
:type controller: :py:class:`pyfx.core.Controller`
"""
self._controller = controller
+ self._config = config
+
self._data = None
+ self._keymapper = KeyMapperConfigurationParser.create_keymapper(self._config.keymap)
# different window components
- self._view_window = ViewWindow(self, self._data)
- self._query_window = QueryWindow(self, controller)
- self._help_window = HelpWindow(self)
+ self._view_window = JSONBrowser(self, self._keymapper.json_browser, self._data)
+ self._query_window = QueryBar(self, controller, self._keymapper.query_bar)
+ self._help_window = HelpBar(self)
# view frame
- self._frame = ViewFrame(self._controller, self._view_window, self._help_window)
+ def popup_factory(popup_launcher, query_bar, prefix, options):
+ return AutoCompletePopUp(controller, self._keymapper.autocomplete_popup, popup_launcher, query_bar,
+ prefix, options)
+ self._frame = ViewFrame(self._view_window, self._help_window, popup_factory)
self._screen = None
self._loop = None
diff --git a/tests/view/component/test_view_window.py b/tests/view/component/test_json_browser.py
similarity index 70%
rename from tests/view/component/test_view_window.py
rename to tests/view/component/test_json_browser.py
index 8bd779e..c8e2554 100644
--- a/tests/view/component/test_view_window.py
+++ b/tests/view/component/test_json_browser.py
@@ -1,10 +1,11 @@
import unittest
-from pyfx import Controller
-from pyfx.view import View
-from pyfx.view.components import ViewWindow
from urwid.compat import B
+from pyfx import Controller
+from pyfx.config import ConfigurationParser
+from pyfx.view.components import JSONBrowser
+
class ViewWindowTest(unittest.TestCase):
"""
@@ -18,20 +19,23 @@ def test_view_window_refresh(self):
}
]
- controller = Controller()
- view_manager = View(controller)
- view_window = ViewWindow(view_manager, data)
+ config = ConfigurationParser().parse()
+
+ controller = Controller(config)
+ view_manager = controller._view
+ keymapper = view_manager._keymapper.json_browser
+ json_browser = JSONBrowser(view_manager, keymapper, data)
# expand the first line
- content = view_window.render((18, 3)).content()
+ content = json_browser.render((18, 3)).content()
texts_before_refresh = [[t[2] for t in row] for row in content]
# refresh view window
new_data = {
"key": "value"
}
- view_window.set_top_node(new_data)
- content = view_window.render((18, 3)).content()
+ json_browser.set_top_node(new_data)
+ content = json_browser.render((18, 3)).content()
texts_after_refresh = [[t[2] for t in row] for row in content]
# verify
diff --git a/tests/view/component/test_query_window.py b/tests/view/component/test_query_bar.py
similarity index 68%
rename from tests/view/component/test_query_window.py
rename to tests/view/component/test_query_bar.py
index 02e8581..a2124a0 100644
--- a/tests/view/component/test_query_window.py
+++ b/tests/view/component/test_query_bar.py
@@ -1,9 +1,9 @@
import unittest
from unittest.mock import MagicMock
+from pyfx.config import ConfigurationParser
from pyfx.core import Controller
-from pyfx.view import View
-from pyfx.view.components import QueryWindow
+from pyfx.view.components import QueryBar
class QueryWindowTest(unittest.TestCase):
@@ -15,12 +15,15 @@ def test_query_on_enter(self):
"""
test query window submit query to controller
"""
- controller = Controller()
+ config = ConfigurationParser().parse()
+
+ controller = Controller(config)
controller.query = MagicMock()
controller.complete = MagicMock(return_value=None)
- view_manager = View(controller)
- query_window = QueryWindow(view_manager, controller)
+ view_manager = controller._view
+ keymapper = view_manager._keymapper.query_bar
+ query_window = QueryBar(view_manager, controller, keymapper)
query_window.setup()
# act
@@ -36,12 +39,15 @@ def test_query_on_esc(self):
"""
test query window submit query to controller
"""
- controller = Controller()
+ config = ConfigurationParser().parse()
+
+ controller = Controller(config)
controller.query = MagicMock()
controller.complete = MagicMock(return_value=None)
- view_manager = View(controller)
- query_window = QueryWindow(view_manager, controller)
+ view_manager = controller._view
+ keymapper = view_manager._keymapper.query_bar
+ query_window = QueryBar(view_manager, controller, keymapper)
query_window.setup()
# act
diff --git a/tests/view/json-lib/array/test_array_node.py b/tests/view/json-lib/array/test_array_node.py
index 2586e85..05dc56a 100644
--- a/tests/view/json-lib/array/test_array_node.py
+++ b/tests/view/json-lib/array/test_array_node.py
@@ -21,7 +21,7 @@ def test_empty_list(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -66,7 +66,7 @@ def test_simple_array(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -106,7 +106,7 @@ def test_nested_array(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -148,7 +148,7 @@ def test_array_with_object_child(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
diff --git a/tests/view/json-lib/object/test_object_node.py b/tests/view/json-lib/object/test_object_node.py
index b579f67..5e02b5b 100644
--- a/tests/view/json-lib/object/test_object_node.py
+++ b/tests/view/json-lib/object/test_object_node.py
@@ -21,7 +21,7 @@ def test_empty_object(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -62,7 +62,7 @@ def test_simple_object(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -98,7 +98,7 @@ def test_nested_object(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()
@@ -135,7 +135,7 @@ def test_object_with_list_child(self):
while widget is not None:
node = widget.get_node()
if not node.is_expanded():
- widget.keypress((18,), "enter")
+ node.toggle_expanded()
widget = node.get_widget()
contents.append(widget.render((18,)).content())
widget = widget.next_inorder()