"""Pyfx app, the main entry point of pyfx library.""" import sys from concurrent.futures.thread import ThreadPoolExecutor import urwid from loguru import logger from pyfx.config import keymaps_path from pyfx.config import parse from pyfx.config import themes_path from pyfx.config.config_parser import load from pyfx.error import PyfxException from pyfx.model import Model from pyfx.service.client import Client from pyfx.service.dispatcher import Dispatcher from pyfx.view import View from pyfx.view.components import AutoCompletePopUp from pyfx.view.components import HelpPopUp 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.themes import Theme from pyfx.view.view_frame import ViewFrame from pyfx.view.view_mediator import ViewMediator class PyfxApp: """Pyfx app, the main entry point of pyfx library.""" def __init__(self, data, config=None, debug_mode=False): """PyfxApp Constructor. Args: data: The actual data to be visualized. While the data is supposed to be in the JSON format, this requirement is not enforced and validated here. config: The path of the configuration file. ``None`` asks Pyfx to search the config file in pre-defined locations. debug_mode: A flag to indicate whether debug logging is enabled or not. Returns: The PyfxApp instance that is ready to be started. """ self.__init_logger(debug_mode) self._config = self.__parse_config(config) self._data = data # backend part self._dispatcher = Dispatcher() # model self._model = Model(self._data) self._dispatcher.register("query", self._model.query) self._dispatcher.register("complete", self._model.complete) # UI part self._keymapper = self.__convert_keymap(self._config.ui.keymap) self._theme = self.__convert_theme(self._config.ui.theme) self._thread_pool_executor = ThreadPoolExecutor() self._client = Client(self._dispatcher, self._thread_pool_executor) self._screen = self.__create_screen() self._mediator = ViewMediator() # view_frame bodies 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 self._warning_bar = WarningBar() self._mediator.register("warning_bar", "update", self._warning_bar.update) self._mediator.register("warning_bar", "clear", self._warning_bar.clear) self._query_bar = QueryBar(self._mediator, self._client, self._keymapper.query_bar) self._mediator.register("query_bar", "select_complete_option", self._query_bar.insert_text) self._mediator.register("query_bar", "pass_keypress", self._query_bar.pass_keypress) # pop up factories 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 # 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) return { 'left': cur_col, 'top': max_row - popup_max_row - footer_rows, 'overlay_width': popup_max_col, 'overlay_height': popup_max_row } popup_widget = AutoCompletePopUp( self._mediator, self._keymapper.autocomplete_popup, *args, **kwargs) return popup_widget, get_autocomplete_popup_params self._autocomplete_popup_factory = autocomplete_factory def help_factory(*args, **kwargs): def get_help_popup_params(original_widget, pop_up_widget, size): popup_max_col, popup_max_row = pop_up_widget.pack(size) max_col, max_row = size return { 'left': int((max_col - popup_max_col) / 2), 'top': int((max_row - popup_max_row) / 2), 'overlay_width': popup_max_col + 2, 'overlay_height': popup_max_row + 2 } popup_widget = HelpPopUp( self._keymapper.detailed_help(), self._mediator, self._keymapper.help_popup) return popup_widget, get_help_popup_params self._help_popup_factory = help_factory # pyfx view frame, the UI for the whole screen self._view_frame = ViewFrame( self._screen, # bodies {"json_browser": self._json_browser}, # footers { "query_bar": self._query_bar, "warning_bar": self._warning_bar }, { "autocomplete": self._autocomplete_popup_factory, "help": self._help_popup_factory }, default_body="json_browser", default_footer="query_bar") self._mediator.register("view_frame", "show", self._view_frame.switch) self._mediator.register("view_frame", "size", self._view_frame.size) self._mediator.register("view_frame", "open_pop_up", self._view_frame.open_pop_up) self._mediator.register("view_frame", "close_pop_up", self._view_frame.close_pop_up) # Pyfx view manager, manages UI life cycle self._view = View(self._theme.palette(), self._keymapper.input_filter, self._screen, self._view_frame) def add_node_creator(self, node_creator): """Customizes rendering behavior in Pyfx of any Python types. Args: `node_creator`(JSONNodeCreator): An instance of :class:`~.view.json_lib.json_node_creator.JSONNodeCreator`. It creates a specific type of :class:`.JSONSimpleNode` based on the type of the value. .. note:: `node_creator` will take precedence than any predefined node creators in Pyfx, thus ``None`` is a valid input for it. """ self._node_factory.register(node_creator) def run(self): """Starts Pyfx.""" try: self.__init() self.__run() except PyfxException as e: # Identified exception, will gonna print to stderr raise e except Exception as e: # We gonna swallow unknown error here # so that pyfx exit quietly logger.opt(exception=True). \ error("Unknown exception encountered in app.run, " "exit with {}", e) finally: self._thread_pool_executor.shutdown(wait=True) self._screen.clear() def __init(self): """Post-initializes Pyfx, it must be called before `__run()`. .. note:: `__init__()`: Used to construct all Pyfx dependencies and load all the static configuration files. `__init()`: Used to initialize all the dependencies, such as processing data to construct essential widgets. """ logger.debug("Initializing Pyfx...") self._json_browser.refresh_view(self._data) def __run(self): """Starts the UI loop.""" logger.debug("Running Pyfx...") self._view.run() def __init_logger(self, is_debug_mode): logger.configure( handlers=[{ "sink": "/tmp/pyfx.log", "level": "DEBUG" if is_debug_mode else "INFO", "enqueue": True, "rotation": "5MB", "retention": "10 days", "format": "{time} {module}.{function} " "{message}" }]) def __parse_config(self, config_path): """Parses the provided configuration into dataclass and then dynamically generates the mapped value to be used by Pyfx. """ logger.debug("Loading Pyfx configuration...") return parse(config_path) def __convert_keymap(self, keymap_config): """Converts the configuration of keymaps into its actual mappings. The configuration of `keymaps` currently only supports certain keywords of predefined mapping (`basic`, `emacs`, etc.). """ keymap_config_file = keymaps_path / f"{keymap_config.mode}.yml" return load(keymap_config_file, KeyMapper) def __convert_theme(self, theme_config): """Converts the configuration of themes into its actual mappings The configuration of `themes` currently only supports certain keywords of predefined mapping (`basic`). """ theme_config_file = themes_path / f"{theme_config}.yml" return load(theme_config_file, Theme) def __create_screen(self): """Creates a `urwid.raw_display.Screen` and turn off control.""" # Specify the `input` to force Screen reload the value for sys.stdin # as sys.stdin may be redirected. E.g., when pyfx is using with pipe, # we replaced the sys.stdin at the CLI level screen = urwid.raw_display.Screen(input=sys.stdin) # noinspection PyBroadException try: # this is to turn off control for SIGTERM while in pyfx screen.tty_signal_keys('undefined', 'undefined', 'undefined', 'undefined', 'undefined') except Exception: # avoid potential error during e2e test pass return screen