diff --git a/console.py b/console.py new file mode 100644 index 0000000..444faa4 --- /dev/null +++ b/console.py @@ -0,0 +1,187 @@ +# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe) +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://opensource.org/licenses/mit-license.php + +import curses +import curses.textpad +import asyncio +import decimal + +try: + import ujson as json +except ImportError: + import json + +import view + + +class ConsoleView(view.View): + _mode_name = "console" + + def __init__(self, client): + self._client = client + + self._textbox_active = False + # TODO: implement history properly + self._command_history = [""] + self._response_history = [] + self._response_history_strings = [] + self._response_history_offset = 0 + + super().__init__() + + async def _draw(self): + self._clear_init_pad() + + CGREEN = curses.color_pair(1) + CRED = curses.color_pair(3) + CYELLOW = curses.color_pair(5) + CBOLD = curses.A_BOLD + CREVERSE = curses.A_REVERSE + + self._pad.addstr(0, 63, "[UP/DOWN: browse, TAB: enter command]", CYELLOW) + offset = self._response_history_offset + if offset > 0: + self._pad.addstr(0, 36, "... ^ ...", CBOLD) + if offset < len(self._response_history_strings) - 17: + self._pad.addstr(17, 36, "... v ...", CBOLD) + + for i, (t, string) in enumerate(self._response_history_strings): + if i < offset: + continue + if i > offset+15: # TODO + break + + color = CBOLD + CGREEN if t == 0 else CBOLD + self._pad.addstr(1+i-offset, 1, string, color) + + cmd = self._command_history[-1] + cmd2 = None + if len(cmd) > 97: + cmd2, cmd = cmd[97:], cmd[:97] + + self._pad.addstr(18, 1, "> {}".format(cmd), + CRED + CBOLD + CREVERSE if self._textbox_active else 0) + if cmd2 is not None: + self._pad.addstr(19, 3, cmd2, + CRED + CBOLD + CREVERSE if self._textbox_active else 0) + + self._draw_pad_to_screen() + + @staticmethod + def _convert_reqresp_to_strings(request, response): + srequest = [ + (0, request[i:i+95]) + for i in range(0, len(request), 95) + ] + srequest[0] = (0, ">>> " + srequest[0][1]) + + jresponse = json.dumps(response, indent=4, sort_keys=True).split("\n") + # TODO: if error, set 2 not 1 + sresponse = [ + (1, l[i:i+99]) + for l in jresponse + for i in range(0, len(l), 99) + ] + + return srequest + sresponse + [(-1, "")] + + async def _submit_command(self): + # TODO: parse, allow nested, use brackets etc + request = self._command_history[-1] + if len(request) == 0: + return + + parts = request.split(" ") + for i in range(len(parts)): + # TODO: parse better. + if parts[i].isdigit(): + parts[i] = int(parts[i]) + elif parts[i] == "false" or parts[i] == "False": + parts[i] = False + elif parts[i] == "true" or parts[i] == "True": + parts[i] = True + else: + try: + parts[i] = decimal.Decimal(parts[i]) + except: + pass + + cmd = parts[0] + if len(parts) > 1: + params = parts[1:] + else: + params = None + + response = await self._client.request(cmd, params=params) + self._response_history.append( + (request, response), + ) + self._response_history_strings.extend( + self._convert_reqresp_to_strings(request, response), + ) + + self._command_history.append("") # add a new, empty command + self._response_history_offset = len(self._response_history_strings) - 17 + self._textbox_active = not self._textbox_active + + await self._draw_if_visible() + + async def _scroll_back_response_history(self): + if self._response_history_offset == 0: + return # At the beginning already. + + self._response_history_offset -= 1 + + await self._draw_if_visible() + + async def _scroll_forward_response_history(self): + if self._response_history_offset > len(self._response_history_strings) - 18: + return # At the end already. + + self._response_history_offset += 1 + + await self._draw_if_visible() + + async def handle_keypress(self, key): + if key == "\t" or key == "KEY_TAB": + self._textbox_active = not self._textbox_active + key = None + elif self._textbox_active: + if (len(key) == 1 and ord(key) == 127) or key == "KEY_BACKSPACE": + self._command_history[-1] = self._command_history[-1][:-1] + + key = None + elif key == "KEY_RETURN" or key == "\n": + # We use ensure_future so as not to block the keypad loop on + # an RPC call + # asyncio.ensure_future(self._submit_command()) + await self._submit_command() + return None + elif len(key) == 1: + # TODO: check if it's printable etc + if len(self._command_history[-1]) < 190: + self._command_history[-1] += key + + key = None + else: + if key == "KEY_UP": + await self._scroll_back_response_history() + key = None + elif key == "KEY_DOWN": + await self._scroll_forward_response_history() + key = None + + await self._draw_if_visible() + + return key + + async def on_mode_change(self, newmode): + """ Overrides view.View to set the textbox inactive. """ + if newmode != self._mode_name: + self._textbox_active = False + self._visible = False + return + + self._visible = True + await self._draw_if_visible() diff --git a/macros.py b/macros.py index 8fb1a89..0b728d0 100644 --- a/macros.py +++ b/macros.py @@ -4,11 +4,7 @@ VERSION_STRING = "bitcoind-ncurses v0.2.0-dev" -# MODES = [ -# "monitor", "wallet", "peers", "block", -# "tx", "console", "net", "forks", -# ] -MODES = ["monitor", "peers", "wallet", "block", "transaction", "net"] +MODES = ["monitor", "peers", "wallet", "block", "transaction", "console", "net"] DEFAULT_MODE = "monitor" # TX_VERBOSE_MODE controls whether the prevouts for an input are fetched. diff --git a/main.py b/main.py index b8e7846..95faa1d 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,7 @@ import block import transaction import net import wallet +import console async def keypress_loop(window, callback, resize_callback): @@ -133,16 +134,20 @@ def create_tasks(client, window, nosplash): modehandler.set_mode, ) + consoleview = console.ConsoleView(client) + modehandler.add_callback("monitor", monitorview.on_mode_change) modehandler.add_callback("peers", peerview.on_mode_change) modehandler.add_callback("block", blockview.on_mode_change) modehandler.add_callback("transaction", transactionview.on_mode_change) modehandler.add_callback("net", netview.on_mode_change) modehandler.add_callback("wallet", walletview.on_mode_change) + modehandler.add_callback("console", consoleview.on_mode_change) modehandler.add_keypress_handler("block", blockview.handle_keypress) modehandler.add_keypress_handler("transaction", transactionview.handle_keypress) modehandler.add_keypress_handler("wallet", walletview.handle_keypress) + modehandler.add_keypress_handler("console", consoleview.handle_keypress) async def on_nettotals(key, obj): await headerview.on_nettotals(key, obj) @@ -172,6 +177,7 @@ def create_tasks(client, window, nosplash): await transactionview.on_window_resize(y, x) await netview.on_window_resize(y, x) await walletview.on_window_resize(y, x) + await consoleview.on_window_resize(y, x) ty, tx = window.getmaxyx() tasks = [ diff --git a/modes.py b/modes.py index 7c9cfb2..7a5c182 100644 --- a/modes.py +++ b/modes.py @@ -62,6 +62,22 @@ class ModeHandler(object): await self.set_mode(newmode) async def handle_keypress(self, key): + # See if the current mode can handle it. + if self._mode is None: + return key + + handler = None + try: + handler = self._keypress_handlers[self._mode] + except KeyError: + pass + + if handler: + key = await handler(key) + + if key is None: + return key + # See if it's related to switching modes. if key == "KEY_LEFT": await self._seek_mode(-1) @@ -77,15 +93,4 @@ class ModeHandler(object): await self.set_mode(mode) return None - # See if the current mode can handle it. - if self._mode is None: - return key - - try: - handler = self._keypress_handlers[self._mode] - except KeyError: - return key - - key = await handler(key) - return key # Either none by this point, or still there.