diff --git a/block.py b/block.py index 1c9a60e..97547f1 100644 --- a/block.py +++ b/block.py @@ -8,7 +8,7 @@ import curses import asyncio # from decimal import Decimal -from macros import MIN_WINDOW_SIZE +import view class BlockStore(object): @@ -119,22 +119,19 @@ class BlockStore(object): return self._bestblockhash -class BlockView(object): +class BlockView(view.View): + _mode_name = "block" + def __init__(self, blockstore, txidsetter, modesetter): self._blockstore = blockstore - self._txidsetter = txidsetter self._modesetter = modesetter - self._pad = None - - self._visible = False - self._hash = None # currently browsed hash. self._selected_tx = None # (index, blockhash) self._tx_offset = None # (offset, blockhash) - self._window_size = MIN_WINDOW_SIZE + super().__init__() async def _set_hash(self, newhash): # TODO: lock? @@ -209,28 +206,20 @@ class BlockView(object): else: self._pad.addstr(8+i-offset, 36, "{}".format(txid)) + async def _draw(self): + self._clear_init_pad() - async def _draw(self, block, bestblockhash): - # TODO: figure out window width etc. - - if self._pad is not None: - self._pad.clear() - else: - self._pad = curses.newpad(20, 100) - + block = None + bestblockhash = None + if self._hash: + block = await self._blockstore.get_block(self._hash) + bestblockhash = await self._blockstore.get_bestblockhash() if block: await self._draw_block(block, bestblockhash) await self._draw_transactions(block, bestblockhash) - await self._draw_pad_to_screen() - - async def _draw_pad_to_screen(self): - maxy, maxx = self._window_size - if maxy < 8 or maxx < 3: - return # Can't do it - - self._pad.refresh(0, 0, 4, 0, min(maxy-3, 24), min(maxx-1, 100)) + self._draw_pad_to_screen() async def _select_previous_transaction(self): if self._hash is None: @@ -250,7 +239,7 @@ class BlockView(object): self._selected_tx = (self._selected_tx[0] - 1, self._selected_tx[1]) - await self.draw() + await self._draw_if_visible() async def _select_next_transaction(self): if self._hash is None: @@ -275,7 +264,7 @@ class BlockView(object): self._selected_tx = (self._selected_tx[0] + 1, self._selected_tx[1]) - await self.draw() + await self._draw_if_visible() async def _enter_transaction_view(self): if self._hash is None: @@ -307,7 +296,7 @@ class BlockView(object): return # Can't do anything await self._set_hash(newhash) - await self.draw() + await self._draw_if_visible() async def _select_next_block(self): if self._hash is None: @@ -319,7 +308,7 @@ class BlockView(object): return # Can't do anything await self._set_hash(newhash) - await self.draw() + await self._draw_if_visible() async def _select_previous_block_n(self, n): if self._hash is None: @@ -331,7 +320,7 @@ class BlockView(object): return # Can't do anything await self._set_hash(newhash) - await self.draw() + await self._draw_if_visible() async def _select_next_block_n(self, n): if self._hash is None: @@ -343,7 +332,7 @@ class BlockView(object): return # Can't do anything await self._set_hash(newhash) - await self.draw() + await self._draw_if_visible() async def _select_best_block(self): if self._hash is None: @@ -355,19 +344,7 @@ class BlockView(object): return # Can't do anything await self._set_hash(newhash) - await self.draw() - - async def draw(self): - if not self._visible: - return - - block = None - bestblockhash = None - if self._hash: - block = await self._blockstore.get_block(self._hash) - bestblockhash = await self._blockstore.get_bestblockhash() - - await self._draw(block, bestblockhash) + await self._draw_if_visible() async def on_bestblockhash(self, key, obj): try: @@ -383,21 +360,7 @@ class BlockView(object): await self._set_hash(newhash) # Redraw so that we know if it's the best - await self.draw() - - async def on_mode_change(self, newmode): - if newmode != "block": - self._visible = False - return - - self._visible = True - await self.draw() - - async def on_window_resize(self, y, x): - # At the moment we ignore the x size and limit to 100. - self._window_size = (y, x) - if self._visible: - await self.draw() + await self._draw_if_visible() async def handle_keypress(self, key): assert self._visible diff --git a/macros.py b/macros.py index 782470a..93f8841 100644 --- a/macros.py +++ b/macros.py @@ -8,9 +8,11 @@ VERSION_STRING = "bitcoind-ncurses v0.2.0-dev" # "monitor", "wallet", "peers", "block", # "tx", "console", "net", "forks", # ] -MODES = ["monitor", "peers", "block", "transaction"] +MODES = ["monitor", "peers", "block", "transaction", "net"] +DEFAULT_MODE = "monitor" # TX_VERBOSE_MODE controls whether the prevouts for an input are fetched. +# TX_VERBOSE_MODE = True TX_VERBOSE_MODE = False MIN_WINDOW_SIZE = (10, 20) diff --git a/monitor.py b/monitor.py index 9946f35..9b4a0a4 100644 --- a/monitor.py +++ b/monitor.py @@ -8,17 +8,15 @@ import curses import asyncio from decimal import Decimal -from macros import MIN_WINDOW_SIZE +import view -class MonitorView(object): +class MonitorView(view.View): + _mode_name = "monitor" + def __init__(self, client): self._client = client - self._pad = None - - self._visible = False - self._lock = asyncio.Lock() self._bestblockhash = None self._bestblockheader = None # raw json blockheader @@ -29,15 +27,10 @@ class MonitorView(object): self._dt = None self._uptime = None # raw uptime from bitcoind (seconds) - self._window_size = MIN_WINDOW_SIZE + super().__init__() async def _draw(self): - # TODO: figure out window width etc. - - if self._pad is not None: - self._pad.clear() - else: - self._pad = curses.newpad(20, 100) + self._clear_init_pad() if self._bestblockheader: bbh = self._bestblockheader @@ -132,16 +125,10 @@ class MonitorView(object): if self._uptime: self._pad.addstr(13, 1, "uptime: {}".format(datetime.timedelta(seconds=self._uptime))) - await self._draw_pad_to_screen() - - async def _draw_pad_to_screen(self): - maxy, maxx = self._window_size - if maxy < 8 or maxx < 3: - return # Can't do it + self._draw_pad_to_screen() - self._pad.refresh(0, 0, 4, 0, min(maxy-3, 24), min(maxx-1, 100)) - - async def draw(self): + async def _draw_if_visible(self): + """ Override the view.View method because we need to lock. """ with await self._lock: if self._visible: await self._draw() @@ -168,7 +155,7 @@ class MonitorView(object): self._bestcoinbase = j["result"] if draw: - await self.draw() + await self._draw_if_visible() async def on_mempoolinfo(self, key, obj): try: @@ -176,7 +163,7 @@ class MonitorView(object): except KeyError: return - await self.draw() + await self._draw_if_visible() async def on_estimatesmartfee(self, key, obj): try: @@ -190,13 +177,13 @@ class MonitorView(object): except KeyError: self._estimatesmartfee = None - await self.draw() + await self._draw_if_visible() async def on_tick(self, dt): with await self._lock: self._dt = dt - await self.draw() + await self._draw_if_visible() async def on_uptime(self, key, obj): try: @@ -204,17 +191,4 @@ class MonitorView(object): except KeyError: return - await self.draw() - - async def on_mode_change(self, newmode): - if newmode != "monitor": - self._visible = False - return - - self._visible = True - await self.draw() - - async def on_window_resize(self, y, x): - # At the moment we ignore the x size and limit to 100. - self._window_size = (y, x) - await self.draw() + await self._draw_if_visible() diff --git a/net.py b/net.py index 3e2b59a..b20d818 100644 --- a/net.py +++ b/net.py @@ -6,27 +6,40 @@ import math import curses import asyncio -from macros import MIN_WINDOW_SIZE +import view -class NetView(object): +class NetView(view.View): + _mode_name = "net" + def __init__(self): - self._pad = None + self._nettotals_history = [] - self._visible = False + super().__init__() - self._nettotals_history = [] + async def _draw(self): + self._clear_init_pad() - self._window_size = MIN_WINDOW_SIZE + deltas = [] + if self._nettotals_history: + hist = self._nettotals_history + i = 1 + while i < len(hist): + prev = hist[i-1] + current = hist[i] + seconds = (current["timemillis"] - prev["timemillis"]) / 1000 + if seconds <= 0: + continue + up = current["totalbytessent"] - prev["totalbytessent"] + down = current["totalbytesrecv"] - prev["totalbytesrecv"] - async def _draw(self, deltas): - ph, pw = 20, 100 + deltas.append( + (up/seconds, down/seconds), + ) - if self._pad is not None: - self._pad.clear() - else: - self._pad = curses.newpad(ph, pw) + i += 1 + ph, pw = 20, 100 plot_height = (ph-3) // 2 plot_offset = plot_height chart_offset = 13 @@ -74,36 +87,6 @@ class NetView(object): self._draw_pad_to_screen() - def _draw_pad_to_screen(self): - maxy, maxx = self._window_size - if maxy < 8 or maxx < 3: - return # Can't do it - - self._pad.refresh(0, 0, 4, 0, min(maxy-3, maxy-2), min(maxx-1, 100)) - - async def draw(self): - if self._visible: - deltas = [] - if self._nettotals_history: - hist = self._nettotals_history - i = 1 - while i < len(hist): - prev = hist[i-1] - current = hist[i] - seconds = (current["timemillis"] - prev["timemillis"]) / 1000 - if seconds <= 0: - continue - up = current["totalbytessent"] - prev["totalbytessent"] - down = current["totalbytesrecv"] - prev["totalbytesrecv"] - - deltas.append( - (up/seconds, down/seconds), - ) - - i += 1 - - await self._draw(deltas) - async def on_nettotals(self, key, obj): try: self._nettotals_history.append(obj["result"]) @@ -114,17 +97,4 @@ class NetView(object): if len(self._nettotals_history) > 500: self._nettotals_history = self._nettotals_history[:300] - await self.draw() - - async def on_mode_change(self, newmode): - if newmode != "net": - self._visible = False - return - - self._visible = True - await self.draw() - - async def on_window_resize(self, y, x): - # At the moment we ignore the x size and limit to 100. - self._window_size = (y, x) - await self.draw() + await self._draw_if_visible() diff --git a/peers.py b/peers.py index 9fabd9f..37ea00b 100644 --- a/peers.py +++ b/peers.py @@ -8,23 +8,19 @@ import math import curses import asyncio -from macros import MIN_WINDOW_SIZE +import view + + +class PeersView(view.View): + _mode_name = "peers" -class PeersView(object): def __init__(self): - self._pad = None - self._visible = False self._peerinfo = None # raw data from getpeerinfo - self._window_size = MIN_WINDOW_SIZE + super().__init__() async def _draw(self): - # TODO: figure out window width etc. - - if self._pad is not None: - self._pad.clear() - else: - self._pad = curses.newpad(20, 100) # y, x + self._clear_init_pad() if self._peerinfo: po = self._peerinfo @@ -88,18 +84,7 @@ class PeersView(object): if 'synced_headers' in peer: self._pad.addstr(1+index-offset, 93, str(peer['synced_headers']).rjust(7) ) - await self._draw_pad_to_screen() - - async def draw(self): - if self._visible: - await self._draw() - - async def _draw_pad_to_screen(self): - maxy, maxx = self._window_size - if maxy < 8 or maxx < 3: - return # Can't do it - - self._pad.refresh(0, 0, 4, 0, min(maxy-3, 24), min(maxx-1, 100)) + self._draw_pad_to_screen() async def on_peerinfo(self, key, obj): try: @@ -107,17 +92,4 @@ class PeersView(object): except KeyError: return - await self.draw() - - async def on_mode_change(self, newmode): - if newmode != "peers": - self._visible = False - return - - self._visible = True - await self.draw() - - async def on_window_resize(self, y, x): - # At the moment we ignore the x size and limit to 100. - self._window_size = (y, x) - await self.draw() + await self._draw_if_visible() diff --git a/splash.py b/splash.py index c0afd67..f5d8e64 100644 --- a/splash.py +++ b/splash.py @@ -24,6 +24,7 @@ splash_array = [ width = len(splash_array[0]) height = len(splash_array) +# TODO: potentially move SplashView over to view.View class SplashView(object): def __init__(self, set_mode_callback): self._set_mode_callback = set_mode_callback # ModeHandler diff --git a/transaction.py b/transaction.py index e08af4f..484367f 100644 --- a/transaction.py +++ b/transaction.py @@ -6,7 +6,8 @@ import datetime import curses import asyncio -from macros import MIN_WINDOW_SIZE, TX_VERBOSE_MODE +import view +from macros import TX_VERBOSE_MODE class TransactionStore(object): @@ -28,21 +29,19 @@ class TransactionStore(object): return j["result"] -class TransactionView(object): +class TransactionView(view.View): + _mode_name = "transaction" + def __init__(self, transactionstore): self._transactionstore = transactionstore - self._pad = None - - self._visible = False - self._txid = None # currently browsed txid. self._selected_input = None # (index, txid) self._input_offset = None # (offset, txid) self._selected_output = None # (index, txid) self._output_offset = None # (offset, txid) - self._window_size = MIN_WINDOW_SIZE + super().__init__() async def _set_txid(self, txid, vout=None): # TODO: lock? @@ -196,13 +195,22 @@ class TransactionView(object): self._pad.addstr(14+i-offset, 1, "{:05d} {} {: 15.8f} BTC".format(i, outstring, out["value"]), outputcolor) - async def _draw(self, transaction, inouts): - # TODO: figure out window width etc. + async def _draw(self): + self._clear_init_pad() - if self._pad is not None: - self._pad.clear() - else: - self._pad = curses.newpad(20, 100) + transaction = None + inouts = None + if self._txid: + transaction = await self._transactionstore.get_transaction(self._txid) + if TX_VERBOSE_MODE: + inouts = [] + for vin in transaction["vin"]: + if not "txid" in vin: + # It's a coinbase + inouts = None + break + prevtx = await self._transactionstore.get_transaction(vin["txid"]) + inouts.append(prevtx["vout"][vin["vout"]]) if transaction: await self._draw_transaction(transaction) @@ -211,14 +219,7 @@ class TransactionView(object): if "vout" in transaction: await self._draw_outputs(transaction) - await self._draw_pad_to_screen() - - async def _draw_pad_to_screen(self): - maxy, maxx = self._window_size - if maxy < 8 or maxx < 3: - return # Can't do it - - self._pad.refresh(0, 0, 4, 0, min(maxy-3, 24), min(maxx-1, 100)) + self._draw_pad_to_screen() async def _select_previous_input(self): if self._txid is None: @@ -238,7 +239,7 @@ class TransactionView(object): self._selected_input = (self._selected_input[0] - 1, self._selected_input[1]) - await self.draw() + await self._draw_if_visible() async def _select_next_input(self): if self._txid is None: @@ -263,7 +264,7 @@ class TransactionView(object): self._selected_input = (self._selected_input[0] + 1, self._selected_input[1]) - await self.draw() + await self._draw_if_visible() async def _select_input_as_transaction(self): if self._txid is None: @@ -287,7 +288,7 @@ class TransactionView(object): else: await self._set_txid(inp["txid"], vout=inp["vout"]) - await self.draw() + await self._draw_if_visible() async def _select_previous_output(self): if self._txid is None: @@ -307,7 +308,7 @@ class TransactionView(object): self._selected_output = (self._selected_output[0] - 1, self._selected_output[1]) - await self.draw() + await self._draw_if_visible() async def _select_next_output(self): if self._txid is None: @@ -332,41 +333,7 @@ class TransactionView(object): self._selected_output = (self._selected_output[0] + 1, self._selected_output[1]) - await self.draw() - - async def draw(self): - if not self._visible: - return - - transaction = None - inouts = None - if self._txid: - transaction = await self._transactionstore.get_transaction(self._txid) - if TX_VERBOSE_MODE: - inouts = [] - for vin in transaction["vin"]: - if not "txid" in vin: - # It's a coinbase - inouts = None - break - prevtx = await self._transactionstore.get_transaction(vin["txid"]) - inouts.append(prevtx["vout"][vin["vout"]]) - - await self._draw(transaction, inouts) - - async def on_mode_change(self, newmode): - if newmode != "transaction": - self._visible = False - return - - self._visible = True - await self.draw() - - async def on_window_resize(self, y, x): - # At the moment we ignore the x size and limit to 100. - self._window_size = (y, x) - if self._visible: - await self.draw() + await self._draw_if_visible() async def handle_keypress(self, key): assert self._visible @@ -391,6 +358,4 @@ class TransactionView(object): await self._select_input_as_transaction() return None - raise Exception(key) - return key diff --git a/view.py b/view.py new file mode 100644 index 0000000..729ce7f --- /dev/null +++ b/view.py @@ -0,0 +1,49 @@ +# 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 asyncio + +from macros import MIN_WINDOW_SIZE + + +class View(object): + """ Handles basic operations for the central views. """ + def __init__(self): + self._pad = None + self._visible = False + + self._nettotals_history = [] + + self._window_size = MIN_WINDOW_SIZE + + def _clear_init_pad(self): + if self._pad is not None: + self._pad.clear() + else: + self._pad = curses.newpad(20, 100) + + def _draw_pad_to_screen(self): + maxy, maxx = self._window_size + if maxy < 8 or maxx < 3: + return # Can't do it + + self._pad.refresh(0, 0, 4, 0, min(maxy-3, maxy-2), min(maxx-1, 100)) + + async def _draw_if_visible(self): + if self._visible: + await self._draw() + + async def on_mode_change(self, newmode): + if newmode != self._mode_name: + self._visible = False + return + + self._visible = True + await self._draw_if_visible() + + async def on_window_resize(self, y, x): + # All of the current views assume an x width of 100. + self._window_size = (y, x) + await self._draw_if_visible()