Exemple #1
0
class ProgressBar(object):
    """
    Progress bar context manager.

    Usage ::

        with ProgressBar(...) as pb:
            for item in pb(data):
                ...

    :param title: Text to be displayed above the progress bars. This can be a
        callable or formatted text as well.
    :param formatters: List of :class:`.Formatter` instances.
    :param bottom_toolbar: Text to be displayed in the bottom toolbar. This
        can be a callable or formatted text.
    :param style: :class:`prompt_toolkit.styles.BaseStyle` instance.
    :param key_bindings: :class:`.KeyBindings` instance.
    :param file: The file object used for rendering, by default `sys.stderr` is used.

    :param color_depth: `prompt_toolkit` `ColorDepth` instance.
    :param output: :class:`~prompt_toolkit.output.Output` instance.
    :param input: :class:`~prompt_toolkit.input.Input` instance.
    """
    def __init__(self,
                 title=None,
                 formatters=None,
                 bottom_toolbar=None,
                 style=None,
                 key_bindings=None,
                 file=None,
                 color_depth=None,
                 output=None,
                 input=None):
        assert formatters is None or (isinstance(formatters, list) and all(
            isinstance(fo, Formatter) for fo in formatters))
        assert style is None or isinstance(style, BaseStyle)
        assert key_bindings is None or isinstance(key_bindings, KeyBindings)

        self.title = title
        self.formatters = formatters or create_default_formatters()
        self.bottom_toolbar = bottom_toolbar
        self.counters = []
        self.style = style
        self.key_bindings = key_bindings

        # Note that we use __stderr__ as default error output, because that
        # works best with `patch_stdout`.
        self.color_depth = color_depth
        self.output = output or create_output(stdout=file or sys.__stderr__)
        self.input = input or get_default_input()

        self._thread = None

        self._loop = get_event_loop()
        self._previous_winch_handler = None
        self._has_sigwinch = False

    def __enter__(self):
        # Create UI Application.
        title_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.title),
                   height=1,
                   style='class:progressbar,title'),
            filter=Condition(lambda: self.title is not None))

        bottom_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.bottom_toolbar,
                                        style='class:bottom-toolbar.text'),
                   style='class:bottom-toolbar',
                   height=1),
            filter=~is_done & renderer_height_is_known
            & Condition(lambda: self.bottom_toolbar is not None))

        def width_for_formatter(formatter):
            # Needs to be passed as callable (partial) to the 'width'
            # parameter, because we want to call it on every resize.
            return formatter.get_width(progress_bar=self)

        progress_controls = [
            Window(content=_ProgressControl(self, f),
                   width=functools.partial(width_for_formatter, f))
            for f in self.formatters
        ]

        self.app = Application(
            min_redraw_interval=.05,
            layout=Layout(
                HSplit([
                    title_toolbar,
                    VSplit(progress_controls,
                           height=lambda: D(preferred=len(self.counters),
                                            max=len(self.counters))),
                    Window(),
                    bottom_toolbar,
                ])),
            style=self.style,
            key_bindings=self.key_bindings,
            color_depth=self.color_depth,
            output=self.output,
            input=self.input)

        # Run application in different thread.
        def run():
            with _auto_refresh_context(self.app, .3):
                try:
                    self.app.run()
                except BaseException as e:
                    traceback.print_exc()
                    print(e)

        self._thread = threading.Thread(target=run)
        self._thread.start()

        # Attach WINCH signal handler in main thread.
        # (Interrupt that we receive during resize events.)
        self._has_sigwinch = hasattr(signal, 'SIGWINCH') and in_main_thread()
        if self._has_sigwinch:
            self._previous_winch_handler = self._loop.add_signal_handler(
                signal.SIGWINCH, self.app.invalidate)

        return self

    def __exit__(self, *a):
        # Quit UI application.
        if self.app.is_running:
            self.app.exit()

        # Remove WINCH handler.
        if self._has_sigwinch:
            self._loop.add_signal_handler(signal.SIGWINCH,
                                          self._previous_winch_handler)

        self._thread.join()

    def __call__(self,
                 data=None,
                 label='',
                 remove_when_done=False,
                 total=None):
        """
        Start a new counter.

        :param label: Title text or description for this progress. (This can be
            formatted text as well).
        :param remove_when_done: When `True`, hide this progress bar.
        :param total: Specify the maximum value if it can't be calculated by
            calling ``len``.
        """
        assert is_formatted_text(label)
        assert isinstance(remove_when_done, bool)

        counter = ProgressBarCounter(self,
                                     data,
                                     label=label,
                                     remove_when_done=remove_when_done,
                                     total=total)
        self.counters.append(counter)
        return counter

    def invalidate(self):
        self.app.invalidate()
Exemple #2
0
class HummingbotCLI:
    def __init__(self, input_handler: Callable, bindings: KeyBindings,
                 completer: Completer):
        use_asyncio_event_loop()
        self.input_field = create_input_field(completer=completer)
        self.output_field = create_output_field()
        self.log_field = create_log_field()
        self.layout = generate_layout(self.input_field, self.output_field,
                                      self.log_field)

        self.bindings = bindings
        self.input_handler = input_handler
        self.input_field.accept_handler = self.accept
        self.app = Application(layout=self.layout,
                               full_screen=True,
                               key_bindings=self.bindings,
                               style=load_style(),
                               mouse_support=True,
                               clipboard=PyperclipClipboard())
        self.log_lines: Deque[str] = deque()
        self.log(HEADER)

        # settings
        self.prompt_text = ">>> "
        self.pending_input = None
        self.input_event = None
        self.hide_input = False

    async def run(self):
        await self.app.run_async().to_asyncio_future()

    def accept(self, buff):
        self.pending_input = self.input_field.text.strip()

        if self.input_event:
            self.input_event.set()

        try:
            if self.hide_input:
                output = ''
            else:
                output = '\n>>>  {}'.format(self.input_field.text, )
                self.input_field.buffer.append_to_history()
        except BaseException as e:
            output = str(e)

        self.log(output)
        self.input_handler(self.input_field.text)

    def clear_input(self):
        self.pending_input = None

    def log(self, text: str):
        self.log_lines.extend(str(text).split('\n'))
        while len(self.log_lines) > MAXIMUM_OUTPUT_PANE_LINE_COUNT:
            self.log_lines.popleft()
        new_text: str = "\n".join(self.log_lines)
        self.output_field.buffer.document = Document(
            text=new_text, cursor_position=len(new_text))

    def change_prompt(self, prompt: str, is_password: bool = False):
        self.prompt_text = prompt
        processors = []
        if is_password:
            processors.append(PasswordProcessor())
        processors.append(BeforeInput(prompt))
        self.input_field.control.input_processors = processors

    async def prompt(self, prompt: str, is_password: bool = False) -> str:
        self.change_prompt(prompt, is_password)
        self.app.invalidate()
        self.input_event = asyncio.Event()
        await self.input_event.wait()

        temp = self.pending_input
        self.clear_input()
        self.input_event = None

        if is_password:
            masked_string = "*" * len(temp)
            self.log(f"{prompt}{masked_string}")
        else:
            self.log(f"{prompt}{temp}")
        return temp

    def set_text(self, new_text: str):
        self.input_field.document = Document(text=new_text,
                                             cursor_position=len(new_text))

    def toggle_hide_input(self):
        self.hide_input = not self.hide_input

    def exit(self):
        self.app.exit()
Exemple #3
0
class progress_bar(object):
    """
    Progress bar context manager.

    Usage ::

        with progress_bar(...) as pb:
            for item in pb(data):
                ...

    :param title: Text to be displayed above the progress bars. This can be a
        callable or formatted text as well.
    :param bottom_toolbar: Text to be displayed in the bottom toolbar.
        This can be a callable or formatted text.
    """
    def __init__(self,
                 title=None,
                 formatter=default_format,
                 bottom_toolbar=None,
                 style=None):
        self.title = title
        self.formatter = formatter
        self.bottom_toolbar = bottom_toolbar
        self.counters = []
        self.style = style
        self._thread = None

        self._loop = get_event_loop()
        self._previous_winch_handler = None
        self._has_sigwinch = False

    def __enter__(self):
        # Create UI Application.
        title_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.title), height=1),
            filter=Condition(lambda: self.title is not None))

        bottom_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.bottom_toolbar,
                                        style='class:bottom-toolbar.text'),
                   style='class:bottom-toolbar',
                   height=1),
            filter=~is_done & renderer_height_is_known
            & Condition(lambda: self.bottom_toolbar is not None))

        self.app = Application(min_redraw_interval=.05,
                               layout=Layout(
                                   HSplit([
                                       title_toolbar,
                                       Window(
                                           content=_ProgressControl(self),
                                           height=lambda: len(self.counters)),
                                       Window(),
                                       bottom_toolbar,
                                   ])),
                               style=self.style)

        # Run application in different thread.
        def run():
            try:
                self.app.run()
            except Exception as e:
                import traceback
                traceback.print_exc()
                print(e)

        self._thread = threading.Thread(target=run)
        self._thread.start()

        # Attach WINCH signal handler in main thread.
        # (Interrupt that we receive during resize events.)
        self._has_sigwinch = hasattr(signal, 'SIGWINCH') and in_main_thread()
        if self._has_sigwinch:
            self._previous_winch_handler = self._loop.add_signal_handler(
                signal.SIGWINCH, self.app.invalidate)

        return self

    def __exit__(self, *a):
        # Quit UI application.
        if self.app.is_running:
            self.app.set_return_value(None)

        # Remove WINCH handler.
        if self._has_sigwinch:
            self._loop.add_signal_handler(signal.SIGWINCH,
                                          self._previous_winch_handler)

        self._thread.join()

    def __call__(self,
                 data=None,
                 title='',
                 remove_when_done=False,
                 total=None):
        """
        Start a new counter.

        :param remove_when_done: When `True`, hide this progress bar.
        :param total: Specify the maximum value if it can't be calculated by
            calling ``len``.
        """
        counter = ProgressBarCounter(self,
                                     data,
                                     title=title,
                                     remove_when_done=remove_when_done,
                                     total=total)
        self.counters.append(counter)
        return counter

    def invalidate(self):
        self.app.invalidate()
class HummingbotCLI:
    def __init__(self, input_handler: Callable, bindings: KeyBindings,
                 completer: Completer):
        self.search_field = create_search_field()
        self.input_field = create_input_field(completer=completer)
        self.output_field = create_output_field()
        self.log_field = create_log_field(self.search_field)
        self.timer = create_timer()
        self.process_usage = create_process_monitor()
        self.trade_monitor = create_trade_monitor()
        self.layout = generate_layout(self.input_field, self.output_field,
                                      self.log_field, self.search_field,
                                      self.timer, self.process_usage,
                                      self.trade_monitor)
        # add self.to_stop_config to know if cancel is triggered
        self.to_stop_config: bool = False

        self.live_updates = False
        self.bindings = bindings
        self.input_handler = input_handler
        self.input_field.accept_handler = self.accept
        self.app: Optional[Application] = None

        # settings
        self.prompt_text = ">>> "
        self.pending_input = None
        self.input_event = None
        self.hide_input = False

        # start ui tasks
        loop = asyncio.get_event_loop()
        loop.create_task(start_timer(self.timer))
        loop.create_task(start_process_monitor(self.process_usage))
        loop.create_task(start_trade_monitor(self.trade_monitor))

    async def run(self):
        self.app = Application(layout=self.layout,
                               full_screen=True,
                               key_bindings=self.bindings,
                               style=load_style(),
                               mouse_support=True,
                               clipboard=PyperclipClipboard())
        await self.app.run_async()

    def accept(self, buff):
        self.pending_input = self.input_field.text.strip()

        if self.input_event:
            self.input_event.set()

        try:
            if self.hide_input:
                output = ''
            else:
                output = '\n>>>  {}'.format(self.input_field.text, )
                self.input_field.buffer.append_to_history()
        except BaseException as e:
            output = str(e)

        self.log(output)
        self.input_handler(self.input_field.text)

    def clear_input(self):
        self.pending_input = None

    def log(self, text: str, save_log: bool = True):
        if save_log:
            if self.live_updates:
                self.output_field.log(text, silent=True)
            else:
                self.output_field.log(text)
        else:
            self.output_field.log(text, save_log=False)

    def change_prompt(self, prompt: str, is_password: bool = False):
        self.prompt_text = prompt
        processors = []
        if is_password:
            processors.append(PasswordProcessor())
        processors.append(BeforeInput(prompt))
        self.input_field.control.input_processors = processors

    async def prompt(self, prompt: str, is_password: bool = False) -> str:
        self.change_prompt(prompt, is_password)
        self.app.invalidate()
        self.input_event = asyncio.Event()
        await self.input_event.wait()

        temp = self.pending_input
        self.clear_input()
        self.input_event = None

        if is_password:
            masked_string = "*" * len(temp)
            self.log(f"{prompt}{masked_string}")
        else:
            self.log(f"{prompt}{temp}")
        return temp

    def set_text(self, new_text: str):
        self.input_field.document = Document(text=new_text,
                                             cursor_position=len(new_text))

    def toggle_hide_input(self):
        self.hide_input = not self.hide_input

    def exit(self):
        self.app.exit()
Exemple #5
0
class UserInterface:
    def __init__(self, config: Configuration):
        self.config = config
        self.tomato = Tomato(self.config)
        self.prev_hash = None

        self._create_ui()

    def _create_ui(self):
        btn_start = Button("Start", handler=self.tomato.start)
        btn_pause = Button("Pause", handler=self.tomato.pause)
        btn_reset = Button("Reset", handler=self.tomato.reset)
        btn_reset_all = Button("Reset All", handler=self.tomato.reset_all)
        btn_exit = Button("Exit", handler=self._exit_clicked)
        # All the widgets for the UI.
        self.text_area = FormattedTextControl(focusable=False,
                                              show_cursor=False)
        text_window = Window(
            content=self.text_area,
            dont_extend_height=True,
            height=11,
            style="bg:#ffffff #000000",
        )
        root_container = Box(
            HSplit([
                Label(text="Press `Tab` to move the focus."),
                HSplit([
                    VSplit(
                        [
                            btn_start,
                            btn_pause,
                            btn_reset,
                            btn_reset_all,
                            btn_exit,
                        ],
                        padding=1,
                        style="bg:#cccccc",
                    ),
                    text_window,
                ]),
            ]))
        layout = Layout(container=root_container, focused_element=btn_start)
        self._set_key_bindings()

        # Styling.
        style = Style([
            ("left-pane", "bg:#888800 #000000"),
            ("right-pane", "bg:#00aa00 #000000"),
            ("button", "#000000"),
            ("button-arrow", "#000000"),
            ("button focused", "bg:#ff0000"),
            ("red", "#ff0000"),
            ("green", "#00ff00"),
        ])
        self.application = Application(layout=layout,
                                       key_bindings=self.kb,
                                       style=style,
                                       full_screen=True)

    def _set_key_bindings(self):
        self.kb = KeyBindings()

        actions = {
            "focus_next": focus_next,
            "focus_previous": focus_previous,
            "exit_clicked": self._exit_clicked,
            "start": lambda _=None: self.tomato.start(),
            "pause": lambda _=None: self.tomato.pause(),
            "reset": lambda _=None: self.tomato.reset(),
            "reset_all": lambda _=None: self.tomato.reset_all(),
        }

        for action, keys in self.config.key_bindings.items():
            for key in keys.split(","):
                try:
                    self.kb.add(key.strip())(actions[action])
                except KeyError:
                    pass

    @staticmethod
    def _exit_clicked(_=None):
        get_app().exit()

    def _draw(self):
        self.tomato.update()
        text, hash_ = self.tomato.render()
        # WHY: Avoid unnecessary updates
        if hash_ != self.prev_hash:
            self.text_area.text = text
            self.application.invalidate()
            self.prev_hash = hash_

    def run(self):
        self._draw()
        threading.Thread(target=lambda: every(0.4, self._draw),
                         daemon=True).start()
        self.application.run()
Exemple #6
0
class App:
    """Main app class to render the UI and run the application.

    It holds the top level layout of the UI and also contains several
    useful public method that user can leverage in their own customization.

    Its a bridge between all UI element and acts like a root level which
    has access to the entire application states. Similar to a app.js
    in React.js.

    Args:
        config: A :class:`~s3fm.api.config.Config` instance.
        no_history: Skip reading history.
            :class:`~s3fm.api.history.History` won't be loaded.
    """
    def __init__(self,
                 config: Config = None,
                 no_history: bool = False) -> None:
        config = config or Config()
        self._style = Style.from_dict(dict(config.style))
        self._rendered = False
        self._no_history = no_history
        self._layout_mode = LayoutMode.vertical
        self._border = config.app.border
        self._current_focus = Pane.left
        self._previous_focus = None
        self._filepane_focus = Pane.left
        self._custom_effects = config.app.custom_effects
        self._history = History(
            dir_max_size=config.history.dir_max_size,
            cmd_max_size=config.history.cmd_max_size,
        )
        self._kb_mode = KBMode.normal

        self._error_mode = Condition(lambda: self._kb_mode == KBMode.error)
        self._command_mode = Condition(lambda: self._kb_mode == KBMode.command)
        self._normal_mode = Condition(lambda: self._kb_mode == KBMode.normal)
        self._search_mode = Condition(lambda: self._kb_mode == KBMode.search)
        self._reverse_search_mode = Condition(
            lambda: self._kb_mode == KBMode.reverse_search)

        self._error = ""
        self._error_type = ErrorType.error
        self._error_pane = ErrorPane(
            error=self._error_mode,
            message=lambda: self._error,
            error_type=lambda: self._error_type,
        )

        self._layout_single = Condition(
            lambda: self._layout_mode == LayoutMode.single)
        self._layout_vertical = Condition(
            lambda: self._layout_mode == LayoutMode.vertical)

        self._left_pane = FilePane(
            pane_id=Pane.left,
            spinner_config=config.spinner,
            linemode_config=config.linemode,
            app_config=config.app,
            redraw=self.redraw,
            layout_single=self._layout_single,
            layout_vertical=self._layout_vertical,
            focus=lambda: self._filepane_focus,
            history=self._history,
            set_error=self.set_error,
        )
        self._right_pane = FilePane(
            pane_id=Pane.right,
            spinner_config=config.spinner,
            linemode_config=config.linemode,
            app_config=config.app,
            redraw=self.redraw,
            layout_single=self._layout_single,
            layout_vertical=self._layout_vertical,
            focus=lambda: self._filepane_focus,
            history=self._history,
            set_error=self.set_error,
        )
        self._command_pane = CommandPane(app=self)
        self._option_pane = OptionPane()

        self._kb = KB(
            app=self,
            kb_maps=config.kb.kb_maps,
            custom_kb_maps=config.kb.custom_kb_maps,
            custom_kb_lookup=config.kb.custom_kb_lookup,
        )

        self._app = Application(
            layout=self.layout,
            full_screen=True,
            after_render=self._after_render,
            style=self._style,
            key_bindings=self._kb,
        )

    def redraw(self) -> None:
        """Instruct the app to redraw itself to the terminal.

        This is useful when trying to force an UI update of the :class:`App`.
        """
        self._app.invalidate()

    async def _load_pane_data(self, pane: FilePane) -> None:
        """Load the data for the target pane and refersh the app.

        Args:
            pane: A `FilePane` instance to load data.
        """
        await pane.load_data()
        self.redraw()

    async def _render_task(self) -> None:
        """Read history and instruct left/right pane to load appropriate data.

        When `App` is created, `KB` is not activated and will only be activated
        once `History` is processed. This decision is made because `Hache` may
        cause the `App` UI to change and confuse the user.
        """
        if not self._no_history:
            await self._history.read()
        self._left_pane.mode = self._history.left_mode
        self._right_pane.mode = self._history.right_mode
        self._left_pane.selected_file_index = self._history.left_index
        self._right_pane.selected_file_index = self._history.right_index
        self._left_pane.path = self._history.left_path
        self._right_pane.path = self._history.right_path
        self.pane_focus(self._history.focus)
        self.layout_switch(self._history.layout)
        self._kb.activated = True
        await asyncio.gather(
            self._load_pane_data(pane=self._left_pane),
            self._load_pane_data(pane=self._right_pane),
        )

    def _after_render(self, _) -> None:
        """Run this function every time the `App` is re-rendered, same as `useEffect` in react.js.

        Using a class state `self._rendered` to force this function to only run once when
        the `App` is first created.

        Loading all relevant data in this method can turn the whole data loading into an
        async experience.
        """
        for use_effect in self._custom_effects:
            use_effect(self)
        if not self._rendered:
            self._rendered = True
            self._left_pane.loading = True
            self._right_pane.loading = True
            asyncio.create_task(self._render_task())

    async def run(self) -> None:
        """Start the application in async mode."""
        await self._app.run_async()

    def pane_focus(self, pane: Pane) -> None:
        """Focus specified pane and set the focus state.

        Args:
            pane: Target pane to focus.

        Examples:
            >>> from s3fm.app import App
            >>> from s3fm.enums import Pane
            >>> app = App() # doctest: +SKIP
            >>> app.pane_focus(Pane.left) # doctest: +SKIP
        """
        if pane in self.filepanes:
            self._kb_mode = KBMode.normal
            self._filepane_focus = pane
        else:
            _kb_mode_map = {
                CommandMode.command: KBMode.command,
                CommandMode.search: KBMode.search,
                CommandMode.reverse_search: KBMode.reverse_search,
            }
            self._kb_mode = _kb_mode_map.get(self._command_pane.mode,
                                             KBMode.command)
        self._previous_focus = self._current_focus
        self._current_focus = pane
        self._app.layout.focus(self.current_focus)

    def pane_focus_other(self) -> None:
        """Focus the other filepane.

        Theres only a maximum of 2 filepane in the app currently. Use
        this method to focus the other filepane.

        This method won't have any effect if the current UI only have
        one filepane.
        """
        if not self._layout_single():
            self.pane_focus(Pane.left if self._current_focus ==
                            Pane.right else Pane.right)

    def cmd_focus(self, mode=CommandMode.command) -> None:
        """Focus the commandpane.

        Args:
            mode: Command mode to set for the commandpane.
        """
        self._command_pane.mode = mode
        self._command_pane.buffer.text = ""
        self.pane_focus(Pane.cmd)

    def cmd_exit(self) -> None:
        """Exit the commandpane and refocus the last focused filepane."""
        self._command_pane.mode = CommandMode.clear
        self._command_pane.buffer.text = ""
        self.current_filepane.searched_indices = None
        self.pane_focus(self._previous_focus or Pane.left)

    def exit(self) -> None:
        """Exit the application and kill all spawed processes."""
        self._history.left_mode = self._left_pane.mode
        self._history.right_mode = self._right_pane.mode
        self._history.left_index = self._left_pane.selected_file_index
        self._history.right_index = self._right_pane.selected_file_index
        self._history.left_path = self._left_pane.path
        self._history.right_path = self._right_pane.path
        self._history.focus = self._filepane_focus
        self._history.layout = self._layout_mode
        if not self._no_history:
            self._history.write()
        self._app.exit()

    def layout_switch(self, layout: LayoutMode) -> None:
        """Switch to a different layout.

        Args:
            layout: Desired layout mode to switch.

        Examples:
            >>> from s3fm.app import App
            >>> from s3fm.enums import LayoutMode
            >>> app = App() # doctest: +SKIP
            >>> app.layout_switch(LayoutMode.vertical) # doctest: +SKIP
        """
        self._layout_mode = layout
        if layout != LayoutMode.single:
            self._app.layout = self.layout
            self.pane_focus(self._current_focus)

    def pane_swap(self, direction: Direction, layout: LayoutMode) -> None:
        """Swap panes left/right/up/down.

        This has side effects where it may cuase layout to change.
        When current layout is `LayoutMode.vertical` and switching
        up/down, layout will be changed to `LayoutMode.horizontal`.

        This function won't have any effect when theres only one filepane.

        Args:
            direction: Desired direction to swap.
            layout: Desired layout.

        Examples:
            >>> from s3fm.app import App
            >>> from s3fm.enums import Direction, LayoutMode
            >>> app = App() # doctest: +SKIP
            >>> app.pane_swap(Direction.left, LayoutMode.vertical) # doctest: +SKIP
        """
        if self._layout_single():
            return
        if (self._current_focus == Pane.right and
            (direction == Direction.right or direction == Direction.down)
                and self._layout_mode == layout):
            return
        if (self._current_focus == Pane.left
                and (direction == Direction.left or direction == Direction.up)
                and self._layout_mode == layout):
            return
        pane_swapped = False
        if not (self._current_focus == Pane.right and
                (direction == Direction.right or direction == Direction.down)
                and self._layout_mode != layout) and not (
                    self._current_focus == Pane.left and
                    (direction == Direction.left or direction == Direction.up)
                    and self._layout_mode != layout):
            pane_swapped = True
            self._left_pane, self._right_pane = self._right_pane, self._left_pane
            self._left_pane.id, self._right_pane.id = (
                self._right_pane.id,
                self._left_pane.id,
            )
        self._layout_mode = layout
        self._app.layout = self.layout
        if pane_swapped:
            self.pane_focus_other()
        else:
            self.pane_focus(self._current_focus)

    def set_error(self, exception: Optional["Notification"] = None) -> None:
        """Configure error notification for the application.

        This should only be used to set non-application error.

        Args:
            exception: A :class:`~s3fm.exceptions.Notification` instance.
        """
        if not exception:
            self._kb_mode = KBMode.normal
            self._error = ""
        else:
            self._kb_mode = KBMode.error
            self._error = str(exception)
            self._error_type = exception.type

    @property
    def command_mode(self) -> Condition:
        """:class:`prompt_toolkit.filters.Condition`: A callable if current focus is commandpane."""
        return self._command_mode

    @property
    def normal_mode(self) -> Condition:
        """:class:`prompt_toolkit.filters.Condition`: A callable if current focus is a filepane."""
        return self._normal_mode

    @property
    def error_mode(self) -> Condition:
        """:class:`prompt_toolkit.filters.Condition`: A callable if the application has error."""
        return self._error_mode

    @property
    def search_mode(self) -> Condition:
        """:class:`prompt_toolkit.filters.Condition`: A callable if the application is searching."""
        return self._search_mode

    @property
    def reverse_search_mode(self) -> Condition:
        """:class:`prompt_toolkit.filters.Condition`: A callable if the application is reverse searching."""
        return self._reverse_search_mode

    @property
    def current_focus(self) -> "Container":
        """:class:`prompt_toolkit.layout.Container`: Get current focused pane."""
        try:
            return {
                **self.filepanes,
                Pane.cmd: self._command_pane,
                Pane.error: self._error_pane,
            }[self._current_focus]
        except KeyError:
            self.set_error(
                Notification("Unexpected focus.",
                             error_type=ErrorType.warning))
            self.pane_focus(Pane.left)
            return self.current_focus

    @property
    def current_filepane(self) -> FilePane:
        """:class:`~s3fm.ui.filepane.FilePane`: Get current focused filepane."""
        try:
            return self.filepanes[self._filepane_focus]
        except KeyError:
            self.set_error(
                Notification("Unexpected focus.",
                             error_type=ErrorType.warning))
            self._filepane_focus = Pane.left
            return self.current_filepane

    @property
    def file_pane_focus(self) -> Pane:
        """Pane: Focused pane ID."""
        return self._filepane_focus

    @property
    def filepanes(self) -> Dict[Pane, FilePane]:
        """Dict[Pane, FilePane]: Get pane mappings."""
        return {
            Pane.left: self._left_pane,
            Pane.right: self._right_pane,
        }

    @property
    def layout(self) -> Layout:
        """:class:`prompt_toolkit.layout.Layout`: Get app layout dynamically."""
        if self._layout_mode == LayoutMode.vertical:
            layout = HSplit([
                VSplit([self._left_pane, self._right_pane]), self._command_pane
            ])

        elif (self._layout_mode == LayoutMode.horizontal
              or self._layout_mode == LayoutMode.single):
            layout = HSplit(
                [self._left_pane, self._right_pane, self._command_pane])
        else:
            self._layout_mode = LayoutMode.vertical
            self.set_error(
                Notification("Unexpected layout.",
                             error_type=ErrorType.warning))
            return self.layout
        if self._border:
            layout = Frame(layout)
        return Layout(
            FloatContainer(
                content=layout,
                floats=[Float(content=self._option_pane), self._error_pane],
            ))

    @property
    def kb(self) -> KB:
        """:class:`~s3fm.api.kb.KB`: KeyBindings."""
        return self._kb

    @property
    def rendered(self) -> bool:
        """bool: :class:`App` rendered status."""
        return self._rendered
Exemple #7
0
class Pager(object):
    """
    The Pager main application.

    Usage::
        p = Pager()
        p.add_source(...)
        p.run()

    :param source: :class:`.Source` instance.
    :param lexer: Prompt_toolkit `lexer` instance.
    :param vi_mode: Enable Vi key bindings.
    :param style: Prompt_toolkit `Style` instance.
    :param search_text: `None` or the search string that is highlighted.
    """
    def __init__(self,
                 vi_mode=False,
                 style=None,
                 search_text=None,
                 titlebar_tokens=None):
        assert isinstance(vi_mode, bool)
        assert style is None or isinstance(style, Style)

        self.sources = []
        self.current_source_index = 0  # Index in `self.sources`.
        self.highlight_search = True
        self.in_colon_mode = False
        self.message = None
        self.displaying_help = False
        self.search_text = search_text
        self.display_titlebar = bool(titlebar_tokens)
        self.titlebar_tokens = titlebar_tokens or []

        self._dummy_source = DummySource()

        # When this is True, always make sure that the cursor goes to the
        # bottom of the visible content. This is similar to 'tail -f'.
        self.forward_forever = False

        # Status information for all sources. Source -> _SourceInfo.
        # (Remember this info as long as the Source object exists.)
        self.source_info = weakref.WeakKeyDictionary()

        # Create prompt_toolkit stuff.

        def open_file(buff):
            # Open file.
            self.open_file(buff.text)

            # Focus main buffer again.
            buff.reset()

        # Buffer for the 'Examine:' input.
        self.examine_buffer = Buffer(name='EXAMINE',
                                     completer=PathCompleter(expanduser=True),
                                     accept_handler=open_file,
                                     multiline=False)

        # Search buffer.
        self.search_buffer = Buffer(multiline=False)

        self.layout = PagerLayout(self)

        bindings = create_key_bindings(self)
        self.application = Application(
            input=create_input(sys.stdout),
            layout=Layout(container=self.layout.container),
            enable_page_navigation_bindings=True,
            key_bindings=bindings,
            style=style or Style.from_dict(ui_style),
            mouse_support=True,
            after_render=self._after_render,
            full_screen=True)

        # Hide message when a key is pressed.
        def key_pressed(_):
            self.message = None

        self.application.key_processor.before_key_press += key_pressed

        if vi_mode:
            self.application.editing_mode = EditingMode.VI

    @classmethod
    def from_pipe(cls, lexer=None):
        """
        Create a pager from another process that pipes in our stdin.
        """
        assert not sys.stdin.isatty()
        self = cls()
        self.add_source(PipeSource(fileno=sys.stdin.fileno(), lexer=lexer))
        return self

    @property
    def current_source(self):
        " The current `Source`. "
        try:
            return self.sources[self.current_source_index]
        except IndexError:
            return self._dummy_source

    @property
    def current_source_info(self):
        try:
            return self.source_info[self.current_source]
        except KeyError:
            return _SourceInfo(self, self.current_source)

    def open_file(self, filename):
        """
        Open this file.
        """
        lexer = PygmentsLexer.from_filename(filename, sync_from_start=False)

        try:
            source = FileSource(filename, lexer=lexer)
        except IOError as e:
            self.message = '{}'.format(e)
        else:
            self.add_source(source)

    def add_source(self, source):
        """
        Add a new :class:`.Source` instance.
        """
        assert isinstance(source, Source)

        source_info = _SourceInfo(self, source)
        self.source_info[source] = source_info

        self.sources.append(source)

        # Focus
        self.current_source_index = len(self.sources) - 1
        self.application.layout.focus(source_info.window)

    def remove_current_source(self):
        """
        Remove the current source from the pager.
        (If >1 source is left.)
        """
        if len(self.sources) > 1:
            current_source_index = self.current_source

            # Focus the previous source.
            self.focus_previous_source()

            # Remove the last source.
            self.sources.remove(current_source_index)
        else:
            self.message = "Can't remove the last buffer."

    def focus_previous_source(self):
        self.current_source_index = (self.current_source_index - 1) % len(
            self.sources)
        self.application.layout.focus(self.current_source_info.window)
        self.in_colon_mode = False

    def focus_next_source(self):
        self.current_source_index = (self.current_source_index + 1) % len(
            self.sources)
        self.application.layout.focus(self.current_source_info.window)
        self.in_colon_mode = False

    def display_help(self):
        """
        Display help text.
        """
        if not self.displaying_help:
            source = FormattedTextSource(HELP, name='<help>')
            self.add_source(source)
            self.displaying_help = True

    def quit_help(self):
        """
        Hide the help text.
        """
        if self.displaying_help:
            self.remove_current_source()
            self.displaying_help = False

    def _after_render(self, app):
        """
        Each time when the rendering is done, we should see whether we need to
        read more data from the input pipe.
        """
        # When the bottom is visible, read more input.
        # Try at least `info.window_height`, if this amount of data is
        # available.
        info = self.layout.dynamic_body.get_render_info()
        source = self.current_source
        source_info = self.source_info[source]
        b = source_info.buffer
        line_tokens = source_info.line_tokens

        if not source_info.waiting_for_input_stream and not source.eof(
        ) and info:
            lines_below_bottom = info.ui_content.line_count - info.last_visible_line(
            )

            # Make sure to preload at least 2x the amount of lines on a page.
            if lines_below_bottom < info.window_height * 2 or self.forward_forever:
                # Lines to be loaded.
                lines = [info.window_height * 2 - lines_below_bottom
                         ]  # nonlocal

                fd = source.get_fd()

                def handle_content(tokens):
                    """ Handle tokens, update `line_tokens`, decrease
                    line count and return list of characters. """
                    data = []
                    for token_char in tokens:
                        char = token_char[1]
                        if char == '\n':
                            line_tokens.append([])

                            # Decrease line count.
                            lines[0] -= 1
                        else:
                            line_tokens[-1].append(token_char)
                        data.append(char)
                    return data

                def insert_text(list_of_fragments):
                    document = Document(b.text + ''.join(list_of_fragments),
                                        b.cursor_position)
                    b.set_document(document, bypass_readonly=True)

                    if self.forward_forever:
                        b.cursor_position = len(b.text)

                def receive_content_from_fd():
                    # Read data from the source.
                    tokens = source.read_chunk()
                    data = handle_content(tokens)

                    # Set document.
                    insert_text(data)

                    # Remove the reader when we received another whole page.
                    # or when there is nothing more to read.
                    if lines[0] <= 0 or source.eof():
                        if fd is not None:
                            get_event_loop().remove_reader(fd)
                        source_info.waiting_for_input_stream = False

                    # Redraw.
                    self.application.invalidate()

                def receive_content_from_generator():
                    " (in executor) Read data from generator. "
                    # Call `read_chunk` as long as we need more lines.
                    while lines[0] > 0 and not source.eof():
                        tokens = source.read_chunk()
                        data = handle_content(tokens)
                        insert_text(data)

                        # Schedule redraw.
                        self.application.invalidate()

                    source_info.waiting_for_input_stream = False

                # Set 'waiting_for_input_stream' and render.
                source_info.waiting_for_input_stream = True
                self.application.invalidate()

                # Add reader for stdin.
                if fd is not None:
                    get_event_loop().add_reader(fd, receive_content_from_fd)
                else:
                    # Execute receive_content_from_generator in thread.
                    # (Don't use 'run_in_executor', because we need a daemon.
                    t = threading.Thread(target=receive_content_from_generator)
                    t.daemon = True
                    t.start()

    def run(self):
        """
        Create an event loop for the application and run it.
        """
        try:
            # Set search highlighting.
            if self.search_text:
                self.application.search_state.text = self.search_text

            return self.application.run()
        finally:
            # XXX: Close all sources which are opened by the pager itself.
            pass
Exemple #8
0
class View(object):
    def __init__(self, configuration, exchange):
        self._project_name = configuration.project.name
        self._local_dir = configuration.local_dir
        self._remote_directory = None
        self._exchange = exchange
        self._loop = get_event_loop()

        self._current_screen = None
        self.root_container = HSplit([Window()])
        self.layout = Layout(container=self.root_container)
        self._render()

        self.bindings = self._create_bindings()

        self.application = Application(layout=self.layout,
                                       key_bindings=self.bindings,
                                       full_screen=True)

        self._exchange.subscribe(Messages.REMOTE_DIRECTORY_SET,
                                 self._set_remote_dir)

    def _render(self):
        top_toolbar = self._render_top_toolbar()
        if self._current_screen is not None:
            main_container = self._current_screen.main_container
        else:
            main_container = Window()
        self.root_container.children = [top_toolbar, main_container]

    def mount(self, screen):
        """
        Mount a screen into the view.

        The screen must have a `main_container` attribute and,
        optionally, a `bindings` attribute.
        """
        if self._current_screen is not None:
            self._current_screen.stop()
        self._current_screen = screen
        if screen.bindings is not None:
            if screen.use_default_bindings:
                merged_key_bindings = merge_key_bindings(
                    [self.bindings, screen.bindings])
                self.application.key_bindings = merged_key_bindings
            else:
                self.application.key_bindings = screen.bindings
        else:
            # Screen does not define additional keybindings
            self.application.key_bindings = self.bindings
        self._render()
        screen.on_mount(self.application)

    def start(self):
        def run():
            try:
                self.application.run()
            except Exception as e:
                traceback.print_exc()
                print(e)

        self._thread = threading.Thread(target=run)
        self._thread.start()
        self._register_resize_handler()

    def stop(self):
        if self.application.is_running:
            self.application.exit()
            self._remove_resize_handler()

    def _register_resize_handler(self):
        # The application receives the signal SIGWINCH
        # when the terminal has been resized.
        self._has_sigwinch = hasattr(signal, "SIGWINCH")
        if self._has_sigwinch:
            self._previous_winch_handler = self._loop.add_signal_handler(
                signal.SIGWINCH, self._on_resize)

    def _remove_resize_handler(self):
        # Remove WINCH handler.
        if self._has_sigwinch:
            self._loop.add_signal_handler(signal.SIGWINCH,
                                          self._previous_winch_handler)

    def _on_resize(self):
        logging.info("Handling application resize event.")
        self.application.invalidate()

    def _render_top_toolbar(self):
        remote_directory_text = (":{}".format(self._remote_directory)
                                 if self._remote_directory is not None else "")
        top_text = ("[Faculty Platform synchronizer]  "
                    "{local_dir} -> "
                    "{project_name}{remote_directory_text}").format(
                        local_dir=self._local_dir,
                        project_name=self._project_name,
                        remote_directory_text=remote_directory_text,
                    )
        top_toolbar = Window(FormattedTextControl(top_text),
                             height=1,
                             style="reverse")
        return top_toolbar

    def _create_bindings(self):
        bindings = KeyBindings()

        @bindings.add("c-c")
        @bindings.add("q")
        def _(event):
            self._exchange.publish(Messages.STOP_CALLED)

        return bindings

    def _set_remote_dir(self, directory):
        self._remote_directory = directory
        self._render()
class ProgressBar(object):
    """
    Progress bar context manager.

    Usage ::

        with ProgressBar(...) as pb:
            for item in pb(data):
                ...

    :param title: Text to be displayed above the progress bars. This can be a
        callable or formatted text as well.
    :param formatters: List of :class:`.Formatter` instances.
    :param bottom_toolbar: Text to be displayed in the bottom toolbar. This
        can be a callable or formatted text.
    :param style: :class:`prompt_toolkit.styles.BaseStyle` instance.
    :param key_bindings: :class:`.KeyBindings` instance.
    :param file: The file object used for rendering, by default `sys.stderr` is used.

    :param color_depth: `prompt_toolkit` `ColorDepth` instance.
    :param output: :class:`~prompt_toolkit.output.Output` instance.
    :param input: :class:`~prompt_toolkit.input.Input` instance.
    """
    def __init__(self, title=None, formatters=None, bottom_toolbar=None,
                 style=None, key_bindings=None, file=None, color_depth=None,
                 output=None, input=None):
        assert formatters is None or (
            isinstance(formatters, list) and all(isinstance(fo, Formatter) for fo in formatters))
        assert style is None or isinstance(style, BaseStyle)
        assert key_bindings is None or isinstance(key_bindings, KeyBindings)

        self.title = title
        self.formatters = formatters or create_default_formatters()
        self.bottom_toolbar = bottom_toolbar
        self.counters = []
        self.style = style
        self.key_bindings = key_bindings

        # Note that we use __stderr__ as default error output, because that
        # works best with `patch_stdout`.
        self.color_depth = color_depth
        self.output = output or create_output(stdout=file or sys.__stderr__)
        self.input = input or get_default_input()

        self._thread = None

        self._loop = get_event_loop()
        self._previous_winch_handler = None
        self._has_sigwinch = False

    def __enter__(self):
        # Create UI Application.
        title_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.title), height=1, style='class:progressbar,title'),
            filter=Condition(lambda: self.title is not None))

        bottom_toolbar = ConditionalContainer(
            Window(FormattedTextControl(lambda: self.bottom_toolbar,
                                        style='class:bottom-toolbar.text'),
                   style='class:bottom-toolbar',
                   height=1),
            filter=~is_done & renderer_height_is_known &
                Condition(lambda: self.bottom_toolbar is not None))

        def width_for_formatter(formatter):
            # Needs to be passed as callable (partial) to the 'width'
            # parameter, because we want to call it on every resize.
            return formatter.get_width(progress_bar=self)

        progress_controls = [
            Window(
                content=_ProgressControl(self, f),
                width=functools.partial(width_for_formatter, f))
            for f in self.formatters
        ]

        self.app = Application(
            min_redraw_interval=.05,
            layout=Layout(HSplit([
                title_toolbar,
                VSplit(progress_controls,
                       height=lambda: D(
                           preferred=len(self.counters),
                           max=len(self.counters))),
                Window(),
                bottom_toolbar,
            ])),
            style=self.style,
            key_bindings=self.key_bindings,
            color_depth=self.color_depth,
            output=self.output,
            input=self.input)

        # Run application in different thread.
        def run():
            with _auto_refresh_context(self.app, .3):
                try:
                    self.app.run()
                except BaseException as e:
                    traceback.print_exc()
                    print(e)

        self._thread = threading.Thread(target=run)
        self._thread.start()

        # Attach WINCH signal handler in main thread.
        # (Interrupt that we receive during resize events.)
        self._has_sigwinch = hasattr(signal, 'SIGWINCH') and in_main_thread()
        if self._has_sigwinch:
            self._previous_winch_handler = self._loop.add_signal_handler(
                signal.SIGWINCH, self.app.invalidate)

        return self

    def __exit__(self, *a):
        # Quit UI application.
        if self.app.is_running:
            self.app.exit()

        # Remove WINCH handler.
        if self._has_sigwinch:
            self._loop.add_signal_handler(signal.SIGWINCH, self._previous_winch_handler)

        self._thread.join()

    def __call__(self, data=None, label='', remove_when_done=False, total=None):
        """
        Start a new counter.

        :param label: Title text or description for this progress. (This can be
            formatted text as well).
        :param remove_when_done: When `True`, hide this progress bar.
        :param total: Specify the maximum value if it can't be calculated by
            calling ``len``.
        """
        assert is_formatted_text(label)
        assert isinstance(remove_when_done, bool)

        counter = ProgressBarCounter(
            self, data, label=label, remove_when_done=remove_when_done, total=total)
        self.counters.append(counter)
        return counter

    def invalidate(self):
        self.app.invalidate()
Exemple #10
0
class Pager(object):
    """
    The Pager main application.

    Usage::
        p = Pager()
        p.add_source(...)
        p.run()

    :param source: :class:`.Source` instance.
    :param lexer: Prompt_toolkit `lexer` instance.
    :param vi_mode: Enable Vi key bindings.
    :param style: Prompt_toolkit `Style` instance.
    :param search_text: `None` or the search string that is highlighted.
    """
    def __init__(self, vi_mode=False, style=None, search_text=None,
                 titlebar_tokens=None):
        assert isinstance(vi_mode, bool)
        assert style is None or isinstance(style, Style)

        self.sources = []
        self.current_source_index = 0  # Index in `self.sources`.
        self.highlight_search = True
        self.in_colon_mode = False
        self.message = None
        self.displaying_help = False
        self.search_text = search_text
        self.display_titlebar = bool(titlebar_tokens)
        self.titlebar_tokens = titlebar_tokens or []

        self._dummy_source = DummySource()

        # When this is True, always make sure that the cursor goes to the
        # bottom of the visible content. This is similar to 'tail -f'.
        self.forward_forever = False

        # Status information for all sources. Source -> _SourceInfo.
        # (Remember this info as long as the Source object exists.)
        self.source_info = weakref.WeakKeyDictionary()

        # Create prompt_toolkit stuff.

        def open_file(buff):
            # Open file.
            self.open_file(buff.text)

            # Focus main buffer again.
            buff.reset()

        # Buffer for the 'Examine:' input.
        self.examine_buffer = Buffer(
            name='EXAMINE',
            completer=PathCompleter(expanduser=True),
            accept_handler=open_file,
            multiline=False)

        # Search buffer.
        self.search_buffer = Buffer(multiline=False)

        self.layout = PagerLayout(self)

        bindings = create_key_bindings(self)
        self.application = Application(
            input=create_input(sys.stdout),
            layout=Layout(container=self.layout.container),
            enable_page_navigation_bindings=True,
            key_bindings=bindings,
            style=style or Style.from_dict(ui_style),
            mouse_support=True,
            after_render=self._after_render,
            full_screen=True)

        # Hide message when a key is pressed.
        def key_pressed(_):
            self.message = None
        self.application.key_processor.before_key_press += key_pressed

        if vi_mode:
            self.application.editing_mode = EditingMode.VI

    @classmethod
    def from_pipe(cls, lexer=None):
        """
        Create a pager from another process that pipes in our stdin.
        """
        assert not sys.stdin.isatty()
        self = cls()
        self.add_source(PipeSource(fileno=sys.stdin.fileno(), lexer=lexer))
        return self

    @property
    def current_source(self):
        " The current `Source`. "
        try:
            return self.sources[self.current_source_index]
        except IndexError:
            return self._dummy_source

    @property
    def current_source_info(self):
        try:
            return self.source_info[self.current_source]
        except KeyError:
            return _SourceInfo(self, self.current_source)

    def open_file(self, filename):
        """
        Open this file.
        """
        lexer = PygmentsLexer.from_filename(filename, sync_from_start=False)

        try:
            source = FileSource(filename, lexer=lexer)
        except IOError as e:
            self.message = '{}'.format(e)
        else:
            self.add_source(source)

    def add_source(self, source):
        """
        Add a new :class:`.Source` instance.
        """
        assert isinstance(source, Source)

        source_info = _SourceInfo(self, source)
        self.source_info[source] = source_info

        self.sources.append(source)

        # Focus
        self.current_source_index = len(self.sources) - 1
        self.application.layout.focus(source_info.window)

    def remove_current_source(self):
        """
        Remove the current source from the pager.
        (If >1 source is left.)
        """
        if len(self.sources) > 1:
            current_source_index = self.current_source

            # Focus the previous source.
            self.focus_previous_source()

            # Remove the last source.
            self.sources.remove(current_source_index)
        else:
            self.message = "Can't remove the last buffer."

    def focus_previous_source(self):
        self.current_source_index = (self.current_source_index - 1) % len(self.sources)
        self.application.layout.focus(self.current_source_info.window)
        self.in_colon_mode = False

    def focus_next_source(self):
        self.current_source_index = (self.current_source_index + 1) % len(self.sources)
        self.application.layout.focus(self.current_source_info.window)
        self.in_colon_mode = False

    def display_help(self):
        """
        Display help text.
        """
        if not self.displaying_help:
            source = FormattedTextSource(HELP, name='<help>')
            self.add_source(source)
            self.displaying_help = True

    def quit_help(self):
        """
        Hide the help text.
        """
        if self.displaying_help:
            self.remove_current_source()
            self.displaying_help = False

    def _after_render(self, app):
        """
        Each time when the rendering is done, we should see whether we need to
        read more data from the input pipe.
        """
        # When the bottom is visible, read more input.
        # Try at least `info.window_height`, if this amount of data is
        # available.
        info = self.layout.dynamic_body.get_render_info()
        source = self.current_source
        source_info = self.source_info[source]
        b = source_info.buffer
        line_tokens = source_info.line_tokens

        if not source_info.waiting_for_input_stream and not source.eof() and info:
            lines_below_bottom = info.ui_content.line_count - info.last_visible_line()

            # Make sure to preload at least 2x the amount of lines on a page.
            if lines_below_bottom < info.window_height * 2 or self.forward_forever:
                # Lines to be loaded.
                lines = [info.window_height * 2 - lines_below_bottom]  # nonlocal

                fd = source.get_fd()

                def handle_content(tokens):
                    """ Handle tokens, update `line_tokens`, decrease
                    line count and return list of characters. """
                    data = []
                    for token_char in tokens:
                        char = token_char[1]
                        if char == '\n':
                            line_tokens.append([])

                            # Decrease line count.
                            lines[0] -= 1
                        else:
                            line_tokens[-1].append(token_char)
                        data.append(char)
                    return data

                def insert_text(list_of_fragments):
                    document = Document(b.text + ''.join(list_of_fragments), b.cursor_position)
                    b.set_document(document, bypass_readonly=True)

                    if self.forward_forever:
                        b.cursor_position = len(b.text)

                def receive_content_from_fd():
                    # Read data from the source.
                    tokens = source.read_chunk()
                    data = handle_content(tokens)

                    # Set document.
                    insert_text(data)

                    # Remove the reader when we received another whole page.
                    # or when there is nothing more to read.
                    if lines[0] <= 0 or source.eof():
                        if fd is not None:
                            get_event_loop().remove_reader(fd)
                        source_info.waiting_for_input_stream = False

                    # Redraw.
                    self.application.invalidate()

                def receive_content_from_generator():
                    " (in executor) Read data from generator. "
                    # Call `read_chunk` as long as we need more lines.
                    while lines[0] > 0 and not source.eof():
                        tokens = source.read_chunk()
                        data = handle_content(tokens)
                        insert_text(data)

                        # Schedule redraw.
                        self.application.invalidate()

                    source_info.waiting_for_input_stream = False

                # Set 'waiting_for_input_stream' and render.
                source_info.waiting_for_input_stream = True
                self.application.invalidate()

                # Add reader for stdin.
                if fd is not None:
                    get_event_loop().add_reader(fd, receive_content_from_fd)
                else:
                    # Execute receive_content_from_generator in thread.
                    # (Don't use 'run_in_executor', because we need a daemon.
                    t = threading.Thread(target=receive_content_from_generator)
                    t.daemon = True
                    t.start()

    def run(self):
        """
        Create an event loop for the application and run it.
        """
        try:
            # Set search highlighting.
            if self.search_text:
                self.application.search_state.text = self.search_text

            return self.application.run()
        finally:
            # XXX: Close all sources which are opened by the pager itself.
            pass
Exemple #11
0
class HummingbotCLI(PubSub):
    def __init__(self, input_handler: Callable, bindings: KeyBindings,
                 completer: Completer, command_tabs: Dict[str, CommandTab]):
        super().__init__()
        self.command_tabs = command_tabs
        self.search_field = create_search_field()
        self.input_field = create_input_field(completer=completer)
        self.output_field = create_output_field()
        self.log_field = create_log_field(self.search_field)
        self.right_pane_toggle = create_log_toggle(self.toggle_right_pane)
        self.live_field = create_live_field()
        self.log_field_button = create_tab_button("Log-pane",
                                                  self.log_button_clicked)
        self.timer = create_timer()
        self.process_usage = create_process_monitor()
        self.trade_monitor = create_trade_monitor()
        self.layout, self.layout_components = generate_layout(
            self.input_field, self.output_field, self.log_field,
            self.right_pane_toggle, self.log_field_button, self.search_field,
            self.timer, self.process_usage, self.trade_monitor,
            self.command_tabs)
        # add self.to_stop_config to know if cancel is triggered
        self.to_stop_config: bool = False

        self.live_updates = False
        self.bindings = bindings
        self.input_handler = input_handler
        self.input_field.accept_handler = self.accept
        self.app: Optional[Application] = None

        # settings
        self.prompt_text = ">>> "
        self.pending_input = None
        self.input_event = None
        self.hide_input = False

        # stdout redirection stack
        self._stdout_redirect_context: ExitStack = ExitStack()

        # start ui tasks
        loop = asyncio.get_event_loop()
        loop.create_task(start_timer(self.timer))
        loop.create_task(start_process_monitor(self.process_usage))
        loop.create_task(start_trade_monitor(self.trade_monitor))

    def did_start_ui(self):
        self._stdout_redirect_context.enter_context(
            patch_stdout(log_field=self.log_field))

        log_level = global_config_map.get("log_level").value
        init_logging("hummingbot_logs.yml", override_log_level=log_level)

        self.trigger_event(HummingbotUIEvent.Start, self)

    async def run(self):
        self.app = Application(layout=self.layout,
                               full_screen=True,
                               key_bindings=self.bindings,
                               style=load_style(),
                               mouse_support=True,
                               clipboard=PyperclipClipboard())
        await self.app.run_async(pre_run=self.did_start_ui)
        self._stdout_redirect_context.close()

    def accept(self, buff):
        self.pending_input = self.input_field.text.strip()

        if self.input_event:
            self.input_event.set()

        try:
            if self.hide_input:
                output = ''
            else:
                output = '\n>>>  {}'.format(self.input_field.text, )
                self.input_field.buffer.append_to_history()
        except BaseException as e:
            output = str(e)

        self.log(output)
        self.input_handler(self.input_field.text)

    def clear_input(self):
        self.pending_input = None

    def log(self, text: str, save_log: bool = True):
        if save_log:
            if self.live_updates:
                self.output_field.log(text, silent=True)
            else:
                self.output_field.log(text)
        else:
            self.output_field.log(text, save_log=False)

    def change_prompt(self, prompt: str, is_password: bool = False):
        self.prompt_text = prompt
        processors = []
        if is_password:
            processors.append(PasswordProcessor())
        processors.append(BeforeInput(prompt))
        self.input_field.control.input_processors = processors

    async def prompt(self, prompt: str, is_password: bool = False) -> str:
        self.change_prompt(prompt, is_password)
        self.app.invalidate()
        self.input_event = asyncio.Event()
        await self.input_event.wait()

        temp = self.pending_input
        self.clear_input()
        self.input_event = None

        if is_password:
            masked_string = "*" * len(temp)
            self.log(f"{prompt}{masked_string}")
        else:
            self.log(f"{prompt}{temp}")
        return temp

    def set_text(self, new_text: str):
        self.input_field.document = Document(text=new_text,
                                             cursor_position=len(new_text))

    def toggle_hide_input(self):
        self.hide_input = not self.hide_input

    def toggle_right_pane(self):
        if self.layout_components["pane_right"].filter():
            self.layout_components["pane_right"].filter = lambda: False
            self.layout_components["item_top_toggle"].text = '< log pane'
        else:
            self.layout_components["pane_right"].filter = lambda: True
            self.layout_components["item_top_toggle"].text = '> log pane'

    def log_button_clicked(self):
        for tab in self.command_tabs.values():
            tab.is_selected = False
        self.redraw_app()

    def tab_button_clicked(self, command_name: str):
        for tab in self.command_tabs.values():
            tab.is_selected = False
        self.command_tabs[command_name].is_selected = True
        self.redraw_app()

    def exit(self):
        self.app.exit()

    def redraw_app(self):
        self.layout, self.layout_components = generate_layout(
            self.input_field, self.output_field, self.log_field,
            self.right_pane_toggle, self.log_field_button, self.search_field,
            self.timer, self.process_usage, self.trade_monitor,
            self.command_tabs)
        self.app.layout = self.layout
        self.app.invalidate()

    def tab_navigate_left(self):
        selected_tabs = [
            t for t in self.command_tabs.values() if t.is_selected
        ]
        if not selected_tabs:
            return
        selected_tab: CommandTab = selected_tabs[0]
        if selected_tab.tab_index == 1:
            self.log_button_clicked()
        else:
            left_tab = [
                t for t in self.command_tabs.values()
                if t.tab_index == selected_tab.tab_index - 1
            ][0]
            self.tab_button_clicked(left_tab.name)

    def tab_navigate_right(self):
        current_tabs = [
            t for t in self.command_tabs.values() if t.tab_index > 0
        ]
        if not current_tabs:
            return
        selected_tab = [t for t in current_tabs if t.is_selected]
        if selected_tab:
            right_tab = [
                t for t in current_tabs
                if t.tab_index == selected_tab[0].tab_index + 1
            ]
        else:
            right_tab = [t for t in current_tabs if t.tab_index == 1]
        if right_tab:
            self.tab_button_clicked(right_tab[0].name)

    def close_buton_clicked(self, command_name: str):
        self.command_tabs[command_name].button = None
        self.command_tabs[command_name].close_button = None
        self.command_tabs[command_name].output_field = None
        self.command_tabs[command_name].is_selected = False
        for tab in self.command_tabs.values():
            if tab.tab_index > self.command_tabs[command_name].tab_index:
                tab.tab_index -= 1
        self.command_tabs[command_name].tab_index = 0
        if self.command_tabs[command_name].task is not None:
            self.command_tabs[command_name].task.cancel()
            self.command_tabs[command_name].task = None
        self.redraw_app()

    def handle_tab_command(self, hummingbot: "HummingbotApplication",
                           command_name: str, kwargs: Dict[str, Any]):
        if command_name not in self.command_tabs:
            return
        cmd_tab = self.command_tabs[command_name]
        if "close" in kwargs and kwargs["close"]:
            if cmd_tab.close_button is not None:
                self.close_buton_clicked(command_name)
            return
        if "close" in kwargs:
            kwargs.pop("close")
        if cmd_tab.button is None:
            cmd_tab.button = create_tab_button(
                command_name, lambda: self.tab_button_clicked(command_name))
            cmd_tab.close_button = create_tab_button(
                "x", lambda: self.close_buton_clicked(command_name), 1, '',
                ' ')
            cmd_tab.output_field = create_live_field()
            cmd_tab.tab_index = max(t.tab_index
                                    for t in self.command_tabs.values()) + 1
        self.tab_button_clicked(command_name)
        self.display_tab_output(cmd_tab, hummingbot, kwargs)

    def display_tab_output(self, command_tab: CommandTab,
                           hummingbot: "HummingbotApplication",
                           kwargs: Dict[Any, Any]):
        if command_tab.task is not None and not command_tab.task.done():
            return
        if threading.current_thread() != threading.main_thread():
            hummingbot.ev_loop.call_soon_threadsafe(self.display_tab_output,
                                                    command_tab, hummingbot,
                                                    kwargs)
            return
        command_tab.task = safe_ensure_future(
            command_tab.tab_class.display(command_tab.output_field, hummingbot,
                                          **kwargs))
Exemple #12
0
class Application:
    def __init__(self):
        self.style_name = 'trac'
        self.frame = None
        self.argsnlocals = ArgsnLocalsWindow(app=self)
        self.console = ConsoleWindow(app=self, callback=self._gdb_callback)
        self.source = SourceWindow(self)
        self.breakpoints = BreakpointsWindow(self, show=False)
        self.callstack = CallstackWindow(self, show=False)
        self.disassembly = DisassemblyWindow(self, show=False)
        self.registers = RegistersWindow(self, show=False)
        self.threads = ThreadsWindow(self, show=False)
        self.inferiors = ''

        self.col1 = HSplit([
            self.source.get_ui(),
            self.disassembly.get_ui(),
            self.console.get_ui()
        ])

        self.col2 = HSplit([
            self.argsnlocals.get_ui(),
            self.registers.get_ui(),
            self.callstack.get_ui(),
            self.threads.get_ui(),
            self.breakpoints.get_ui()
        ])

        self.container = VSplit([self.col1, self.col2])
        self.layout = Layout(container=self.container,
                             focused_element=self.console.get_ui())

        self.style = style_from_pygments_cls(get_style_by_name(
            self.style_name))

        kb = self._get_key_bindings()

        self.app = PromptApplication(layout=self.layout,
                                     style=self.style,
                                     full_screen=True,
                                     mouse_support=True,
                                     key_bindings=kb)

    def run(self):
        self.log('*** Running application')
        self.app.run()

    def log(self, msg):
        self.console.log(msg)

    def has_breakpoint(self, loc):
        return self.breakpoints.has_breakpoint(loc)

    def _hide_tui(self):
        def hide(c):
            if c.show:
                c.toggle_show()

        hide(self.source)
        hide(self.disassembly)
        hide(self.argsnlocals)
        hide(self.registers)
        hide(self.callstack)
        hide(self.threads)
        hide(self.breakpoints)
        self.app.invalidate()

    def _get_key_bindings(self):
        kb = KeyBindings()

        @kb.add('c-up')
        def _(event):
            self.console.enter_copy_mode()

        @kb.add('c-x', 'a', eager=True)
        def _(event):
            self._hide_tui()
            self.app.invalidate()

        @kb.add('c-x', '1', eager=True)
        def _(event):
            self._hide_tui()
            self.source.show = True
            self.app.invalidate()

        @kb.add('c-x', '2', eager=True)
        def _(event):
            self._hide_tui()
            self.source.show = True
            self.disassembly.show = True

        @kb.add('c-x', 's', eager=True)
        def _(event):
            self.source.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 'd', eager=True)
        def _(event):
            self.disassembly.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 'c', eager=True)
        def _(event):
            self.callstack.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 'v', eager=True)
        def _(event):
            self.argsnlocals.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 'b', eager=True)
        def _(event):
            self.breakpoints.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 'r', eager=True)
        def _(event):
            self.registers.toggle_show()
            self.app.invalidate()

        @kb.add('c-x', 't', eager=True)
        def _(event):
            self.threads.toggle_show()

        @kb.add('c-s', eager=True)
        def _(event):
            self._next_style()

        @kb.add('c-l')
        def _(event):
            pass

        @kb.add('c-x')
        def _(event):
            pass

        @kb.add('s-right')
        def _(event):
            self.layout.focus_next()

        @kb.add('s-left')
        def _(event):
            self.layout.focus_previous()

        kb = merge_key_bindings([load_key_bindings(), kb])
        return kb

    def _handle_info_inferiors(self, output):
        m = search('process (\d+)', output)
        if m and m[1] != self.inferiors:
            # Program (re)run
            self.log('**match %s' % m[1])
            self.inferiors = m[1]
            self.breakpoints.reset()
            self.source.reset()
            self.disassembly.reset()
            self.registers.reset()
            self.frame = None

    def _handle_info_frame(self, output):
        self.callstack.handle_info_frame(output)
        m = search('frame at (0x.+):', output)
        frame = m[1] if m else None
        changed = frame != self.frame
        self.frame = frame
        if changed:
            self.argsnlocals.handle_frame_change()
        self.source.handle_info_frame(output)

    def _gdb_callback(self, response):
        self.log('***Received \n%s' % response)
        try:
            p = response.find('\n')
            cmd = response[:p]
            output = response[p + 1:]
            if cmd.startswith('info args'):
                self.argsnlocals.handle_info_args(output)
            elif cmd.startswith('info locals'):
                self.argsnlocals.handle_info_locals(output)

            elif cmd.startswith('disassemble'):
                self.disassembly.handle_disassemble(output)
            elif cmd.startswith('info registers'):
                self.registers.handle_info_registers(output)

            elif cmd.startswith('info breakpoints'):
                self.breakpoints.handle_info_breakpoints(output)

            elif cmd.startswith('bt '):
                self.callstack.handle_bt(output)

            elif cmd.startswith('info frame'):
                self._handle_info_frame(output)
                app.invalidate()
            elif cmd.startswith('info inferiors'):
                self._handle_info_inferiors(output)

            elif cmd.startswith('info threads'):
                self.threads.handle_info_threads(output)
        except:
            self.log('***Exception %s' % (exec_info()[1]))

    def _next_style(self):
        styles = list(get_all_styles())
        for i in range(0, len(styles)):
            if styles[i] == self.style_name:
                if i + 1 == len(styles):
                    self.style_name = styles[0]
                else:
                    self.style_name = styles[i + 1]
                    break

        self.style = style_from_pygments_cls(get_style_by_name(
            self.style_name))
        self.app.style = self.style
        self.console.update_info()
        self.app.invalidate()
Exemple #13
0
class NoobitCLI:
    def __init__(self,
                #  input_handler: Callable,
                #  completer: Completer):
                ):

        completer = None
        self.process_usage = create_process_monitor()
        
        self.search_log_field = create_search_field("logs")
        self.search_out_field = create_search_field("ouput")
        self.input_field = create_input_field(completer=completer)
        self.output_field = create_output_field(self.search_out_field)

        # right hand window
        self.log_field = create_log_field(self.search_log_field)

        self.timer = create_timer()
        self.layout = generate_layout(self.input_field, self.output_field, self.log_field, self.search_log_field, self.search_out_field, self.timer, self.process_usage)
        # add self.to_stop_config to know if cancel is triggered
        self.to_stop_config: bool = False

        self.live_updates = False
        self.bindings = load_key_bindings(self)

        self.input_handler = self._input_handler
        self.input_field.accept_handler = self.accept
        self.app = Application(layout=self.layout, full_screen=True, key_bindings=self.bindings, style=load_style(),
                               mouse_support=True, clipboard=PyperclipClipboard())

        # settings
        self.prompt_text = ">>> "
        self.pending_input = None
        self.input_event = None
        self.hide_input = False

        # start ui tasks
        self.loop = asyncio.get_event_loop()
        self.loop.create_task(start_timer(self.timer))

        #! maximaus added
        self.argparser = load_parser(self)
        self.client = httpx.AsyncClient()
        # TODO update type annotation
        self.ws: typing.Dict[str, KrakenWsPublic] = {}
        # TODO we dont want to hardcode this for every exchange
        self.symbols: typing.Dict = {}
        #     "KRAKEN": None,
        #     "BINANCE": None
        # }



    def _input_handler(self, raw_command):
        """parse input and map it to functions
        """
        try:
            _handle_commands(self, raw_command)
        except Exception as e:
            self.log(e)


    async def run(self):
        await self.app.run_async()


    def accept(self, buff):
        self.pending_input = self.input_field.text.strip()

        if self.input_event:
            self.input_event.set()

        try:
            if self.hide_input:
                output = ''
            else:
                output = '\n>>>  {}'.format(self.input_field.text,)
                self.input_field.buffer.append_to_history()
        except BaseException as e:
            output = str(e)

        self.log(output)
        self.input_handler(self.input_field.text)


    def clear_input(self):
        self.pending_input = None


    def log(self, text: str, save_log: bool = True):
        if save_log:
            if self.live_updates:
                self.output_field.log(text, silent=True)
            else:
                self.output_field.log(text)
        else:
            self.output_field.log(text, save_log=False)


    def clear(self):
        self.output_field.clear()


    def change_prompt(self, prompt: str, is_password: bool = False):
        self.prompt_text = prompt
        processors: typing.List[typing.Any] = []
        if is_password:
            processors.append(PasswordProcessor())
        processors.append(BeforeInput(prompt))
        self.input_field.control.input_processors = processors


    async def prompt(self, prompt: str, is_password: bool = False) -> str:
        self.change_prompt(prompt, is_password)
        self.app.invalidate()
        self.input_event = asyncio.Event()
        await self.input_event.wait()

        temp = self.pending_input
        self.clear_input()
        self.input_event = None

        if is_password:
            masked_string = "*" * len(temp)
            self.log(f"{prompt}{masked_string}")
        else:
            self.log(f"{prompt}{temp}")
        return temp


    def set_text(self, new_text: str):
        self.input_field.document = Document(text=new_text, cursor_position=len(new_text))


    def toggle_hide_input(self):
        self.hide_input = not self.hide_input


    def exit(self):
        self.app.exit()


    # ========================================
    # == COMMANDS


    def set_vars(self, exchange: str, symbol: str, ordType: str, orderQty: float):
        if exchange: settings.EXCHANGE = exchange.upper()
        if symbol: settings.SYMBOL = symbol.upper()
        if ordType: settings.ORDTYPE = ordType.upper()
        if orderQty: settings.ORDQTY = orderQty



    # ========================================
    # == TESTING THAT COUNTING WORKS

    def count(self, start: int, finish: int, step: int):

        async def enum(start, finish, step):
            n = start
            while n < finish:
                # write to a different field
                # save_log option means we will display the record, otherwise log gets cleared on each updated and replaced
                self.log_field.log(text=n, save_log=True)
                n += 1
                await asyncio.sleep(1)

        # async def _await(start, finish, step):
        #     await enum(start, finish, step)

        safe_ensure_future(self, enum(start, finish, step))

    
    async def acount(self, start: int, finish: int, step: int):

        n = start
        while n < finish:
            self.log_field.log(text=n, save_log=True)
            n+=1
            await asyncio.sleep(1)


    # ========================================
    # == UTIL

    def check_symbols(self):
        _ok = True
        for exchange in ntypes.EXCHANGE:
            self.log_field.log(f"Checking symbols for {exchange}")
            if not self.symbols[exchange]:
                self.log_field.log(f"Please initialize symbols for {exchange}")
                _ok = False
        
        return _ok




    # ========================================
    # == HELP

    async def help(
            self,  #type: NoobitCLI
            command: str
            ):
        if command == 'all':
            self.log(self.argparser.format_help())
        else:
            subparsers_actions = [
                action for action in self.argparser._actions if isinstance(action, argparse._SubParsersAction)]

            for subparsers_action in subparsers_actions:
                subparser = subparsers_action.choices.get(command)
                if subparser:
                    self.log(subparser.format_help())

    
    async def list(
        self
    ):
        await self.help("all")



    # ========================================
    # ADD API KEYS

    async def add_keys(self, exchange: str, key: str, secret: str):

        if not exchange: exchange = settings.EXCHANGE 
        else: exchange = exchange.upper()

        from noobit_markets.path import APP_PATH
        import time
        import os

        self.log(APP_PATH)

        path = os.path.join(APP_PATH, "exchanges", exchange.lower(), "rest", ".env")
        self.log(path)

        with open(path, "a") as file:
            timeid = int(time.time())
            file.write("\n")
            file.write(f"\n{exchange}_API_KEY_{timeid} = {key}")
            file.write(f"\n{exchange}_API_SECRET_{timeid} = {secret}")

            # TODO we should probably make a bogus private rest call to check if it works

            self.log_field.log("API Credentials added\n")


    # ========================================
    # PUBLIC ENDPOINTS COMMANDS


    async def fetch_symbols(self):
        kraken_symbols = await KRAKEN.rest.public.symbols(self.client)
        binance_symbols = await BINANCE.rest.public.symbols(self.client)
        ftx_symbols = await FTX.rest.public.symbols(self.client)

        if kraken_symbols.is_ok():
            self.symbols["KRAKEN"] = kraken_symbols
        else:
            self.log_field.log("Error fetching kraken symbols")
            self.log("Error fetching kraken symbols")
            self.log(kraken_symbols.value)
        
        if binance_symbols.is_ok():
            self.symbols["BINANCE"] = binance_symbols
        else:
            self.log_field.log("Error fetching binance symbols")
            self.log("Error fetching binance symbols")
            self.log(binance_symbols.value)
            
        if ftx_symbols.is_ok():
            self.symbols["FTX"] = ftx_symbols
        else:
            self.log_field.log("Error fetching ftx symbols")
            self.log("Error fetching ftx symbols")
            self.log(ftx_symbols.value)



    def show_symbols(self, exchange: str):
        from noobit_markets.base.models.rest.response import NSymbol

        cap_exch = exchange.upper()
        
        if cap_exch in ntypes.EXCHANGE.__members__.keys():
            if cap_exch in self.symbols.keys():

                self.log_field.log(f"Exchange is accepted: {cap_exch}")
                _sym = NSymbol(self.symbols[cap_exch])
                if _sym.is_err():
                    self.log("Err")
                    self.log(_sym.result)
                else:
                    self.log("OK")
                    self.log(_sym.table)

            else:
                self.log("Please initialize symbols for this exchange")

        else:
            self.log("Unknown Exchange requested")


    @ensure_symbols
    async def fetch_ohlc(self, exchange: str, symbol: str, timeframe: str, since: typing.Optional[int]=None):
        from noobit_markets.base.models.rest.response import NOhlc

        self.log_field.log("CALLED fetch_ohlc")

        if not symbol:
            if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                return Err("Please set or pass <symbol> argument")
            else:
                symbol = settings.SYMBOL
        else: symbol=symbol.upper()

        interface = globals()[exchange]
        _res = await interface.rest.public.ohlc(self.client, symbol.upper(), self.symbols[exchange].value, timeframe.upper(), since)
        _ohlc = NOhlc(_res)
        return _ohlc

        

    @ensure_symbols
    async def fetch_orderbook(self, exchange: str, symbol: str, depth: int):
        from noobit_markets.base.models.rest.response import NOrderBook

        self.log_field.log("CALLED fetch_orderbook")

        if not symbol:
            if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                return Err("Please set or pass <symbol> argument")
            else:
                symbol = settings.SYMBOL
        else: symbol=symbol.upper()
        
        interface = globals()[exchange]
        _res = await interface.rest.public.orderbook(self.client, symbol.upper(), self.symbols[exchange].value, depth)
        _book = NOrderBook(_res)
        return _book


    @ensure_symbols
    async def fetch_trades(self, exchange: str, symbol: str, since: typing.Optional[int]=None):
        from noobit_markets.base.models.rest.response import NTrades

        self.log_field.log("CALLED fetch_trades")
        
        if not symbol: 
            if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                return Err("Please set or pass <symbol> argument")
            else: symbol = settings.SYMBOL
        else: symbol=symbol.upper()
        
        interface = globals()[exchange]
        _res = await interface.rest.public.trades(self.client, symbol, self.symbols[exchange].value, since)
        _trd = NTrades(_res)
        return _trd
        


    # ========================================
    # PRIVATE ENDPOINTS COMMANDS


    @ensure_symbols
    async def fetch_balances(self, exchange: str):
        from noobit_markets.base.models.rest.response import NBalances
        
        self.log_field.log("CALLED fetch_balances")
        
        
        interface = globals()[exchange]
        _res = await interface.rest.private.balances(self.client, self.symbols[exchange].value)
        _bal = NBalances(_res)
        return _bal


    @ensure_symbols
    async def fetch_exposure(self, exchange: str):
        from noobit_markets.base.models.rest.response import NExposure

        self.log_field.log("CALLED fetch_exposure")
        self.log_field.log(f"Requested Exchange : {exchange.upper()}")

        interface = globals()[exchange]

        _res = await interface.rest.private.exposure(self.client, self.symbols[exchange].value)
        _exp = NExposure(_res)
        return _exp


    @ensure_symbols
    async def fetch_usertrades(self, exchange: str, symbol: str):
        from noobit_markets.base.models.rest.response import NTrades

        self.log_field.log("CALLED fetch_usertrades")

        if not symbol: symbol = settings.SYMBOL
        else: symbol=symbol.upper()

        self.log_field.log(f"Requested Symbol : {symbol}")
        self.log_field.log(f"Requested Exchange : {exchange}")
        
        interface = globals()[exchange]
        _res = await interface.rest.private.trades(self.client, symbol, self.symbols[exchange].value)
        _utr = NTrades(_res)
        return _utr


    @ensure_symbols
    async def fetch_openorders(self, exchange: str, symbol: str):
        from noobit_markets.base.models.rest.response import NOrders
    
        self.log_field.log("CALLED fetch_openorders") 
        
        if not symbol: 
            if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                return Err("Please set or pass <symbol> argument")
            else: symbol = settings.SYMBOL
        else: symbol=symbol.upper()

        self.log_field.log(f"Requested Symbol : {symbol}")
        self.log_field.log(f"Requested Exchange : {exchange}")
        
        interface = globals()[exchange]
        _res = await interface.rest.private.open_orders(self.client, symbol, self.symbols[exchange].value)
        _opo = NOrders(_res)
        return _opo
        

    @ensure_symbols
    async def create_neworder(
        self,
        exchange: str,
        symbol: str,
        ordType: str,
        clOrdID,
        orderQty: float,
        price: float,
        timeInForce: str = "GOOD-TIL-CANCEL",
        quoteOrderQty: typing.Optional[float] = None,
        stopPrice: typing.Optional[float] = None,
        *,
        side: str,
        blind: bool,
        split: typing.Optional[int] = None,
        delay: typing.Optional[int] = None,
        step: typing.Optional[float] = None,
    ):
        from noobit_markets.base.models.rest.response import NSingleOrder

        self.log_field.log("CALLED create_neworder") 

        if not symbol: 
            if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                return Err("Please set or pass <symbol> argument")
            else: symbol = settings.SYMBOL
        else: symbol=symbol.upper()

        if not ordType:
            if not settings.ORDTYPE or settings.ORDTYPE.isspace(): 
                return Err("Please set or pass <ordType> argument")
            else: ordType = settings.ORDTYPE
        # TODO be consistent: either all noobit types in capital or in lowercase
        else: ordType = ordType.upper()

        if not orderQty:
            if not settings.ORDQTY: 
                return Err("Please set or pass <orderQty> argument")
            else: orderQty = settings.ORDQTY

        if not timeInForce and ordType in ["LIMIT", "STOP-LOSS-LIMIT", "TAKE-PROFIT-LIMIT"]:
            self.log("WARNING : <timeInForce> not provided, defaulting to <GOOD-TIL-CANCEL>")
            timeInForce = "GOOD-TIL-CANCEL"

        
        interface = globals()[exchange]

        if split:
            # step is only for limit orders
            if step:
                if not ordType in ["LIMIT", "STOP_LIMIT", "TAKE_PROFIT"]:
                    return Err(f"Argument <step>: Ordertype can not be {ordType}")
            
            # only one of delay or step
            if not any([delay, step]) or all([delay, step]):
                return Err("Please set only one of <delay> or <step> argument to split orders")
            else: 
                _acc = []
                acc_price = price

                for i in range(split):
                    _res = await interface.rest.private.new_order(
                        client=self.client,
                        symbol=symbol, 
                        symbols_resp=self.symbols[exchange].value, 
                        side=side, 
                        ordType=ordType, 
                        clOrdID=clOrdID, 
                        # orderQty=round(orderQty/split, self.symbols[exchange].value.asset_pairs[symbol].volume_decimals), # TODO atm we limit to 2 decimal places, could check max decimals for pair, ALSO this can lead to keyerror if symbol is not on exchange
                        orderQty=round(orderQty/split, 2), # TODO atm we limit to 2 decimal places, could check max decimals for pair, ALSO this can lead to keyerror if symbol is not on exchange
                        price=acc_price, 
                        timeInForce=timeInForce, 
                        quoteOrderQty=quoteOrderQty, 
                        stopPrice=stopPrice
                        )
                    if _res.is_ok():
                        if blind:
                            _res.value.price = None
                        _acc.append(_res.value)
                        self.log_field.log(f"Successful order, count {len(_acc)}")
                    else:
                        self.log_field.log(f"Failed order")
                        return _res
                    
                    if step:
                        acc_price = round(float(acc_price + step), 2) # FIXME this will create decimal place precision errors sometimes, we need to round somehow (use context ??) 
                        await asyncio.sleep(1)
                    else:
                        await asyncio.sleep(delay)  # avoid rate limiting


                try:
                    
                    if blind:
                        self.log("Argument <blind>: Setting Order Price to <None>")
                    
                    _splitorders = NoobitResponseClosedOrders(
                        exchange="KRAKEN",
                        rawjson={},
                        orders=_acc
                    )
                    # request coros always return a result
                    # so we wrap the validated model in an OK container
                    
                    _nords = NOrders(Ok(_splitorders))
                    return _nords
                except ValidationError as e:
                    return Err(e)


        else:

            if any([delay, step]):
                self.log_field.log("Argument <delay> or <step> require <split>")    
                return Err("Argument <delay> or <step> require <split>")    

            _res = await interface.rest.private.new_order(
                client=self.client,
                symbol=symbol, 
                symbols_resp=self.symbols[exchange].value, 
                side=side, 
                ordType=ordType, 
                clOrdID=clOrdID, 
                orderQty=orderQty, 
                price=price, 
                timeInForce=timeInForce, 
                quoteOrderQty=quoteOrderQty, 
                stopPrice=stopPrice
                )

            if blind:
                self.log("Argument <blind>: Setting Order Price to <None>")
                if _res.is_ok():
                    _res.value.price = None

            _nord = NSingleOrder(_res)
        
            return _nord


    # side argument isnt registered for some reason (in following partials)
    # create_buyorder: typing.Coroutine = functools.partialmethod(create_neworder, "BUY")
    # create_sellorder: typing.Coroutine = functools.partialmethod(create_neworder, "SELL")

    
    async def create_buyorder(
        self,
        exchange: str,
        symbol: str,
        ordType: str,
        clOrdID,
        orderQty: float,
        price: float,
        timeInForce: str,
        stopPrice: float,
        blind: bool,
        split: int,
        delay: int,
        step: int
    ):

        await self.create_neworder(exchange, symbol, ordType, clOrdID, orderQty, price, timeInForce, stopPrice, blind=blind, split=split, delay=delay, step=step, side="BUY")


    async def create_sellorder(
        self,
        exchange: str,
        symbol: str,
        ordType: str,
        clOrdID,
        orderQty: float,
        price: float,
        timeInForce: str,
        stopPrice: float,
        blind: bool,
        split: int,
        delay: int,
        step: int
    ):

        await self.create_neworder(exchange, symbol, ordType, clOrdID, orderQty, price, timeInForce, stopPrice, blind=blind, split=split, delay=delay, step=step, side="SELL")


    # TODO add remove_order to binance interface
    @ensure_symbols
    async def cancel_order(
        self,
        exchange: str,
        symbol: str,
        slice: str,
        all: bool
    ):
        
        self.log_field.log("CALLED cancel_order") 

        # testlist = [x for x in range(0, 100)]
        # regex = re.compile(r"^\[[-]?[0-9]+:[-]?[0-9]+\]$")
        regex = "^\[[-]?[0-9]+:[-]?[0-9]+:[-]?[0-9]\]$"
        match = re.match(regex, slice)
        
        if not match:
            self.log(match)
            return Err(f"Argument position did not match regex - Given {slice}")
        else:
            # sliced = eval(f"testlist{position}")
            # self.log(sliced)
            # return Ok("SUCCESS")
        
            if not symbol: 
                if not settings.SYMBOL or settings.SYMBOL.isspace(): 
                    return Err("Please set or pass <symbol> argument")
                else: symbol = settings.SYMBOL
            else: symbol=symbol.upper()

            self.log_field.log(f"Requested Symbol : {symbol}")
            self.log_field.log(f"Requested Exchange : {exchange}")
            
            interface = globals()[exchange]
            _res = await interface.rest.private.open_orders(self.client, symbol, self.symbols[exchange].value)

            if _res.is_err():
                return _res.value
            else:
                _acc = []

                _all_orders = sorted([order for order in _res.value.orders], key=lambda x: getattr(x, "price"), reverse=False)
                _sliced_orders = eval(f"_all_orders{slice}") 

                for _order in _sliced_orders:

                    _ord = await interface.rest.private.remove_order(self.client, symbol, self.symbols[exchange].value, _order.orderID)

                    if _ord.is_ok():
                        _acc.append(_ord.value)
                    else:
                        return _ord

                    await asyncio.sleep(1)
                
                try:
                    _canceled_orders = NoobitResponseClosedOrders(
                        exchange="KRAKEN",
                        rawjson={},
                        orders=_acc
                    )
                    # request coros always return a result
                    # so we wrap the validated model in an OK container
                    _nords = NOrders(Ok(_canceled_orders))
                    return _nords
                except ValidationError as e:
                    return Err(e)




    # ========================================
    # START WEBSOCKET PUBLIC STREAMS

    async def connect(self):
        import websockets
        from noobit_markets.exchanges.kraken.websockets.public.routing import msg_handler
        from noobit_markets.exchanges.kraken.websockets.public.api import KrakenWsPublic

        
        feed_map = {
            "trade": "trade",
            "ticker": "instrument",
            "book": "orderbook",
            "spread": "spread"
        }

        #! only connect Kraken for now
        client = await websockets.connect("wss://ws.kraken.com")
        self.ws["KRAKEN"] = KrakenWsPublic(client, msg_handler, self.loop, feed_map)

        self.log(client)


    #! only stream Kraken for now, need to call connect before
    async def stream_orderbook(self, exchange: str, symbol: str, depth: str):


        if not exchange: exchange = settings.EXCHANGE
        if not symbol: symbol = settings.SYMBOL
        
        self.log(self.ws[exchange])
        self.log(self.ws[exchange].client)
        self.log(self.ws[exchange].orderbook)

        # while True:
            # self.log_field.log("Heartbeat")
            # await asyncio.sleep(2)

        async for msg in self.ws[exchange].orderbook(self.symbols[exchange].value, symbol, depth, True):
            self.log_field.log("Got new message from orderbook websocket")