Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support save query results into file #108

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
23 changes: 16 additions & 7 deletions src/pyfx/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
from pyfx.view import View
from pyfx.view.components import AutoCompletePopUp
from pyfx.view.components import HelpPopUp
from pyfx.view.components import SavingBar
from pyfx.view.components import JSONBrowser
from pyfx.view.components import QueryBar
from pyfx.view.components import WarningBar
from pyfx.view.json_lib.json_node_factory import JSONNodeFactory
from pyfx.view.keys import KeyMapper
from pyfx.view.keyboards import KeyMapper
from pyfx.view.themes import Theme
from pyfx.view.view_frame import ViewFrame
from pyfx.view.view_mediator import ViewMediator
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self, data, config=None, debug_mode=False):
self._dispatcher = Dispatcher()
# model
self._model = Model(self._data)
self._dispatcher.register("save", self._model.save)
self._dispatcher.register("query", self._model.query)
self._dispatcher.register("complete", self._model.complete)

Expand All @@ -68,19 +70,24 @@ def __init__(self, data, config=None, debug_mode=False):
self._screen = self.__create_screen()
self._mediator = ViewMediator()

# view_frame bodies
# view_frame buffers
self._node_factory = JSONNodeFactory()
self._json_browser = JSONBrowser(self._node_factory, self._mediator,
self._keymapper.json_browser)
self._mediator.register("json_browser", "refresh",
self._json_browser.refresh_view)

# view_frame footers
# view_frame mini buffers
# save bar
self._save_bar = SavingBar(self._keymapper.saving_bar, self._client,
self._mediator)
# warning bar
self._warning_bar = WarningBar()
self._mediator.register("warning_bar", "update",
self._warning_bar.update)
self._mediator.register("warning_bar", "clear",
self._warning_bar.clear)
# query bar
self._query_bar = QueryBar(self._mediator, self._client,
self._keymapper.query_bar)
self._mediator.register("query_bar", "select_complete_option",
Expand All @@ -92,16 +99,17 @@ def __init__(self, data, config=None, debug_mode=False):
def autocomplete_factory(*args, **kwargs):
def get_autocomplete_popup_params(original_widget, pop_up_widget,
size):
cur_col, _ = original_widget.get_cursor_coords(size)
popup_max_col, popup_max_row = pop_up_widget.pack(size)
max_col, max_row = size
popup_max_col, popup_max_row = pop_up_widget.pack(size)
# The current focus must be an edit widget (e.g. query bar)
cur_col, _ = original_widget.focus.get_cursor_coords((max_col,))
# FIXME: The following call closely couple to query bar
# we should investigate ways to merge query bar and
# auto_complete directly.
footer_rows = original_widget.mini_buffer.rows((max_col,), True)
focus_rows = original_widget.focus.rows((max_col,), True)
return {
'left': cur_col,
'top': max_row - popup_max_row - footer_rows,
'top': max_row - popup_max_row - focus_rows,
'overlay_width': popup_max_col,
'overlay_height': popup_max_row
}
Expand Down Expand Up @@ -141,6 +149,7 @@ def get_help_popup_params(original_widget, pop_up_widget, size):
{"json_browser": self._json_browser},
# footers
{
"save_bar": self._save_bar,
"query_bar": self._query_bar,
"warning_bar": self._warning_bar
},
Expand Down
6 changes: 6 additions & 0 deletions src/pyfx/config/yaml/keymaps/basic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ json_browser:
collapse_all: "c"
toggle_expansion: "enter"
open_query_bar: "."
open_save_bar: "ctrl s"
open_help_page: "?"
exit: "q"

# Key Mappings for Save Bar
saving_bar:
save: "enter"
cancel: "esc"

# Key Mappings for Query Bar
query_bar:
query: "enter"
Expand Down
17 changes: 17 additions & 0 deletions src/pyfx/model/model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json
import pathlib

from jsonpath_ng import parse
from loguru import logger

Expand All @@ -17,6 +20,20 @@ def __init__(self, data):
self._data = data
self._current = data

# noinspection PyBroadException
def save(self, file_path):
try:
path = pathlib.Path(file_path)
# Create the file if not exists
path.touch()
with path.open('w') as f:
json.dump(self._current, f)
return True
except Exception as e:
logger.opt(exception=True) \
.error("Failed to save the current data into {}", file_path, e)
return False

def query(self, text):
if self._data is None:
logger.debug("Data is None.")
Expand Down
3 changes: 2 additions & 1 deletion src/pyfx/view/components/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""UI components (such as `query bar`) used in Pyfx's TUI."""
"""UI components (such as `query bar`) used in Pyfx."""
from pyfx.view.components.autocomplete_popup import AutoCompletePopUp
from pyfx.view.components.help_popup import HelpPopUp
from pyfx.view.components.saving_bar import SavingBar
from pyfx.view.components.json_browser import JSONBrowser
from pyfx.view.components.query_bar import QueryBar
from pyfx.view.components.warning_bar import WarningBar
2 changes: 1 addition & 1 deletion src/pyfx/view/components/autocomplete_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import urwid
from overrides import overrides

from pyfx.view.common import SelectableText
from pyfx.view.components.abstract_component_keys import BaseComponentKeyMapper
from pyfx.view.components.abstract_component_keys import KeyDefinition
from pyfx.view.widgets import SelectableText


class AutoCompletePopUpKeys(KeyDefinition, Enum):
Expand Down
2 changes: 1 addition & 1 deletion src/pyfx/view/components/help_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import urwid
from overrides import overrides

from pyfx.view.common import SelectableText
from pyfx.view.components.abstract_component_keys import BaseComponentKeyMapper
from pyfx.view.components.abstract_component_keys import KeyDefinition
from pyfx.view.widgets import SelectableText


class HelpPopUpKeys(KeyDefinition, Enum):
Expand Down
11 changes: 10 additions & 1 deletion src/pyfx/view/components/json_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class JSONBrowserKeys(KeyDefinition, Enum):
"Toggle to expand/collapse the current JSON node."

# keys for switching window
OPEN_SAVE_BAR = "ctrl s", "Open the info bar to type the path to save."
OPEN_QUERY_BAR = ".", "Open the query bar to type JSONPath."
OPEN_HELP_PAGE = "?", "Open help page."

Expand All @@ -34,6 +35,7 @@ class JSONBrowserKeyMapper(BaseComponentKeyMapper):
exit: str = "q"

open_help_page: str = "?"
open_save_bar: str = "ctrl s"
open_query_bar: str = "."

cursor_up: str = "up"
Expand All @@ -51,6 +53,7 @@ def mapped_key(self):
self.toggle_expansion: JSONBrowserKeys.TOGGLE_EXPANSION,
self.expand_all: JSONBrowserKeys.EXPAND_ALL,
self.collapse_all: JSONBrowserKeys.COLLAPSE_ALL,
self.open_save_bar: JSONBrowserKeys.OPEN_SAVE_BAR,
self.open_query_bar: JSONBrowserKeys.OPEN_QUERY_BAR,
self.open_help_page: JSONBrowserKeys.OPEN_HELP_PAGE,
self.exit: JSONBrowserKeys.EXIT
Expand All @@ -63,6 +66,7 @@ def short_help(self):
f"DOWN: {self.cursor_down}",
f"TOGGLE: {self.toggle_expansion}",
f"QUERY: {self.open_query_bar}",
f"SAVE: {self.open_save_bar}",
f"HELP: {self.open_help_page}",
f"QUIT: {self.exit}"]

Expand All @@ -73,7 +77,7 @@ def detailed_help(self):
self.exit,
self.cursor_up, self.cursor_down, self.toggle_expansion,
self.expand_all, self.collapse_all,
self.open_query_bar, self.open_help_page
self.open_query_bar, self.open_save_bar, self.open_help_page
]
descriptions = {key: self.mapped_key[key].description for key in keys}
return {
Expand Down Expand Up @@ -115,6 +119,11 @@ def keypress(self, size, key):
pop_up_type="help")
return None

if key == JSONBrowserKeys.OPEN_SAVE_BAR.key:
self._mediator.notify("json_browser", "show", "view_frame",
"save_bar", True)
return None

self._mediator.notify("json_browser", "update", "warning_bar",
f"Unknown key `{key}`. Press `?` for all "
f"supported keys.")
Expand Down
25 changes: 8 additions & 17 deletions src/pyfx/view/components/query_bar.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Module of JsonPath query related classes."""
import asyncio
from dataclasses import dataclass
from enum import Enum
Expand Down Expand Up @@ -106,29 +107,13 @@ def insert_text(self, text, is_partial_complete):
def pass_keypress(self, key):
max_col, max_row = self._mediator.notify("query_bar", "size",
"view_frame", "query_bar")
self.handle_keypress((max_col,), key)
self.keypress((max_col,), key)

def help_message(self):
return self._keymapper.short_help

@overrides
def keypress(self, size, key):
# FIXME: A very hacky way to deal with two cases of key press handling
# in query bar
key = self.handle_keypress(size, key)

if key is None:
return None

self._mediator.notify("query_bar", "update", "warning_bar",
f"Unknown key `{key}`. Press any keys to "
f"continue.")
self._mediator.notify("query_bar", "show", "view_frame", "warning_bar",
False)

return key

def handle_keypress(self, size, key):
key = self._keymapper.key(key)
key = super().keypress(size, key)

Expand All @@ -149,4 +134,10 @@ def handle_keypress(self, size, key):
"json_browser", True)
return None

self._mediator.notify("query_bar", "update", "warning_bar",
f"Unknown key `{key}`. Press any keys to "
f"continue.")
self._mediator.notify("query_bar", "show", "view_frame", "warning_bar",
False)

return key
101 changes: 101 additions & 0 deletions src/pyfx/view/components/saving_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Module of general text input classes, such as the file name to save."""
from dataclasses import dataclass
from enum import Enum

import urwid
from overrides import overrides

from pyfx.view.components.abstract_component_keys import BaseComponentKeyMapper
from pyfx.view.components.abstract_component_keys import KeyDefinition


class SavingBarKeys(KeyDefinition, Enum):
"""Enums for all the available keys defined in InputBar."""

SAVE = "enter", "Finish the current input."
CANCEL = "esc", "Cancel input."


@dataclass(frozen=True)
class SavingBarKeyMapper(BaseComponentKeyMapper):
save: str = "enter"
cancel: str = "esc"

@property
@overrides
def mapped_key(self):
return {
self.save: SavingBarKeys.SAVE,
self.cancel: SavingBarKeys.CANCEL
}

@property
@overrides
def short_help(self):
return [f"CONFIRM: {self.save}",
f"CANCEL: {self.cancel}"]

@property
@overrides
def detailed_help(self):
keys = [self.save, self.cancel]
descriptions = {key: self.mapped_key[key].description for key in keys}
return {
"section": "Saving Bar",
"description": descriptions
}


class SavingBar(urwid.WidgetWrap):
PREFIX = "Save the current view into file: "

def __init__(self, keymapper, client, mediator):
self._keymapper = keymapper
self._client = client
self._mediator = mediator
super().__init__(urwid.Edit(caption=SavingBar.PREFIX))

def help_message(self):
return self._keymapper.short_help

@overrides
def keypress(self, size, original_key):
key = self._keymapper.key(original_key)
key = super().keypress(size, key)

if key is None:
return None

if key == SavingBarKeys.SAVE.key:
file_path = self._w.text[len(SavingBar.PREFIX):]
save_result = self._client.invoke("save", file_path)

if not save_result:
self._mediator.notify("saving_bar", "update", "warning_bar",
"Failed to save the current json data "
f"into file {file_path}.")
self._mediator.notify("saving_bar", "show", "view_frame",
"warning_bar", False)
else:
self._mediator.notify("saving_bar", "update", "warning_bar",
"Saved the current data successfully.")
self._mediator.notify("saving_bar", "show", "view_frame",
"warning_bar", False)
self._mediator.notify("input_bar", "show", "view_frame",
"json_browser", True)
return None

if key == SavingBarKeys.CANCEL.key:
# Hide the saving bar and then reset the text in saving bar to
# its original prefix
self._mediator.notify("saving_bar", "show", "view_frame",
"query_bar", False)
self._mediator.notify("saving_bar", "show", "view_frame",
"json_browser", True)
self.__clear_text()
return None

return original_key

def __clear_text(self):
self._w.set_edit_text("")
2 changes: 1 addition & 1 deletion src/pyfx/view/json_lib/json_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import urwid
from overrides import overrides

from ..common import SelectableText
from pyfx.view.widgets import SelectableText


class JSONWidget(urwid.WidgetWrap):
Expand Down
2 changes: 2 additions & 0 deletions src/pyfx/view/keys.py → src/pyfx/view/keyboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from pyfx.view.components.autocomplete_popup import AutoCompletePopUpKeyMapper
from pyfx.view.components.help_popup import HelpPopUpKeyMapper
from pyfx.view.components.saving_bar import SavingBarKeyMapper
from pyfx.view.components.json_browser import JSONBrowserKeyMapper
from pyfx.view.components.query_bar import QueryBarKeyMapper

Expand Down Expand Up @@ -72,6 +73,7 @@ class KeyMapper:
input_filter: InputFilter = field(init=False)

json_browser: JSONBrowserKeyMapper = JSONBrowserKeyMapper()
saving_bar: SavingBarKeyMapper = SavingBarKeyMapper()
query_bar: QueryBarKeyMapper = QueryBarKeyMapper()
autocomplete_popup: AutoCompletePopUpKeyMapper = \
AutoCompletePopUpKeyMapper()
Expand Down
Loading