mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-04-28 03:00:18 -04:00
bin/deg: introduce eventgraph debugging tool DEG
This commit is contained in:
75
bin/deg/README.md
Normal file
75
bin/deg/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# deg (Debugging Event Graph)
|
||||
|
||||
A simple tui to explore darkfi's Event Graph state. Displays:
|
||||
|
||||
1. Active components of EventGraph.
|
||||
2. Live protocol msgs.
|
||||
3. Update active components.
|
||||
|
||||
## Run
|
||||
|
||||
### Using a venv
|
||||
|
||||
`Deg` requires Python 3.12.0. Make sure Python is installed and on the
|
||||
latest version.
|
||||
|
||||
Depending on your setup you may need to install a virtual environment
|
||||
for Python. Do so as follows:
|
||||
|
||||
```shell
|
||||
% python -m venv python-env
|
||||
% source python-env/bin/activate
|
||||
```
|
||||
|
||||
Then install the requirements:
|
||||
|
||||
```shell
|
||||
% pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Run deg:
|
||||
|
||||
```shell
|
||||
% ./deg
|
||||
```
|
||||
|
||||
You will need to reactivate the venv in your current terminal session
|
||||
each time you use `deg` as follows:
|
||||
|
||||
```shell
|
||||
% source python-env/bin/activate
|
||||
```
|
||||
|
||||
### Without a venv
|
||||
|
||||
If you don't require a venv, install the requirements and run `deg` as follows:
|
||||
|
||||
```shell
|
||||
% pip install -r requirements.txt
|
||||
% python main.py
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
On first run, `deg` will create a config file in the config directory
|
||||
specific to your operating system.
|
||||
|
||||
To use `deg` you will need to open the config file and modify it. Enter
|
||||
the RPC ports of the nodes you want to connect to and title them as you
|
||||
see fit. The default config file uses localhost, but you can replace
|
||||
this with hostnames or external IP addresses. You must also specify
|
||||
whether it is a `NORMAL` or a `LILITH` node.
|
||||
|
||||
## Usage
|
||||
|
||||
Navigate up and down using the arrow keys. Scroll the message log using
|
||||
`PageUp` and `PageDown`. Type `q` to quit.
|
||||
|
||||
## Logging
|
||||
|
||||
deg creates a log file in `bin/deg/deg.log`. To see json data and
|
||||
other debug info, tail the file like so:
|
||||
|
||||
```shell
|
||||
tail -f deg.log
|
||||
```
|
||||
142
bin/deg/deg
Executable file
142
bin/deg/deg
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import json, urwid, asyncio, logging
|
||||
import src.util
|
||||
|
||||
from os.path import join
|
||||
from src.model import Model
|
||||
from src.rpc import JsonRpc
|
||||
from src.view import View
|
||||
|
||||
class Degview:
|
||||
|
||||
def __init__(self):
|
||||
self.ev = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.ev)
|
||||
self.queue = asyncio.Queue()
|
||||
|
||||
os = src.util.get_os()
|
||||
config_path = src.util.user_config_dir('darkfi', os)
|
||||
|
||||
suffix = '.toml'
|
||||
filename = 'deg_config'
|
||||
path = join(config_path, filename + suffix)
|
||||
self.config = src.util.spawn_config(path)
|
||||
|
||||
self.model = Model()
|
||||
self.view = View(self.model)
|
||||
|
||||
async def subscribe(self, rpc, node):
|
||||
name = node['name']
|
||||
host = node['host']
|
||||
port = node['port']
|
||||
type = node['type']
|
||||
info = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
await rpc.start(host, port)
|
||||
logging.debug(f'Started {name} RPC on port {port}')
|
||||
break
|
||||
except Exception as e:
|
||||
info[name] = {}
|
||||
await self.queue.put(info)
|
||||
continue
|
||||
|
||||
if type == 'NORMAL':
|
||||
data = await rpc._make_request('eventgraph.get_info', [])
|
||||
info[name] = data
|
||||
|
||||
await self.queue.put(info)
|
||||
await rpc.deg_switch(True)
|
||||
await rpc.deg_subscribe_events()
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(0.01)
|
||||
data = await rpc.reader.readline()
|
||||
try:
|
||||
data = json.loads(data)
|
||||
info[name] = data
|
||||
await self.queue.put(info)
|
||||
except:
|
||||
info[name] = {}
|
||||
await self.queue.put(info)
|
||||
|
||||
await rpc.deg_switch(False)
|
||||
|
||||
await rpc.stop()
|
||||
|
||||
async def start_connect_slots(self, nodes):
|
||||
tasks = []
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for i, node in enumerate(nodes):
|
||||
rpc = JsonRpc()
|
||||
subscribe = tg.create_task(self.subscribe(
|
||||
rpc, node))
|
||||
nodes = tg.create_task(self.update_info())
|
||||
|
||||
async def update_info(self):
|
||||
while True:
|
||||
info = await self.queue.get()
|
||||
values = list(info.values())[0]
|
||||
|
||||
if not values:
|
||||
self.model.add_offline(info)
|
||||
|
||||
logging.debug(f"values: {values}")
|
||||
|
||||
if 'result' in values:
|
||||
result = values.get('result')
|
||||
if 'eventgraph_info' in result:
|
||||
self.model.add_eg(info)
|
||||
|
||||
if 'params' in values:
|
||||
self.model.add_event(info)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
def main(self):
|
||||
logging.basicConfig(filename='deg.log',
|
||||
encoding='utf-8',
|
||||
level=logging.DEBUG)
|
||||
nodes = self.config.get('nodes')
|
||||
|
||||
loop = urwid.MainLoop(self.view.ui, self.view.palette,
|
||||
unhandled_input=self.unhandled_input,
|
||||
event_loop=urwid.AsyncioEventLoop(
|
||||
loop=self.ev))
|
||||
|
||||
self.ev.create_task(self.start_connect_slots(nodes))
|
||||
self.ev.create_task(self.view.update_view(self.ev, loop))
|
||||
|
||||
loop.run()
|
||||
|
||||
def unhandled_input(self, key):
|
||||
if isinstance(key, tuple):
|
||||
return
|
||||
if key in ('q'):
|
||||
for task in asyncio.all_tasks():
|
||||
task.cancel()
|
||||
raise urwid.ExitMainLoop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
deg = Degview()
|
||||
deg.main()
|
||||
11
bin/deg/deg_config.toml
Normal file
11
bin/deg/deg_config.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[[nodes]]
|
||||
name = "darkirc"
|
||||
host = "localhost"
|
||||
port = 26660
|
||||
type = "NORMAL"
|
||||
|
||||
[[nodes]]
|
||||
name = "genev"
|
||||
host = "localhost"
|
||||
port = 28880
|
||||
type = "NORMAL"
|
||||
2
bin/deg/requirements.txt
Normal file
2
bin/deg/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
toml==0.10.2
|
||||
urwid==2.2.3
|
||||
0
bin/deg/src/__init__.py
Normal file
0
bin/deg/src/__init__.py
Normal file
89
bin/deg/src/model.py
Normal file
89
bin/deg/src/model.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging, time
|
||||
import datetime as dt
|
||||
from collections import defaultdict as dd
|
||||
|
||||
|
||||
class Model:
|
||||
|
||||
def __init__(self):
|
||||
self.nodes = {}
|
||||
|
||||
def add_eg(self, node):
|
||||
name = list(node.keys())[0]
|
||||
values = list(node.values())[0]
|
||||
info = values['result']['eventgraph_info']
|
||||
|
||||
self.nodes[name] = {}
|
||||
self.nodes[name]['current_genesis'] = {}
|
||||
self.nodes[name]['broadcasted_ids'] = {}
|
||||
self.nodes[name]['synced'] = {}
|
||||
self.nodes[name]['event'] = {}
|
||||
self.nodes[name]['unreferenced_tips'] = {}
|
||||
self.nodes[name]['msgs'] = dd(list)
|
||||
|
||||
if info['current_genesis']:
|
||||
self.nodes[name]['current_genesis'] = info['current_genesis']
|
||||
|
||||
if info['broadcasted_ids']:
|
||||
self.nodes[name]['broadcasted_ids'] = info['broadcasted_ids']
|
||||
|
||||
if info['synced']:
|
||||
self.nodes[name]['synced'] = info['synced']
|
||||
|
||||
if info['unreferenced_tips']:
|
||||
self.nodes[name]['unreferenced_tips'] = info['unreferenced_tips']
|
||||
|
||||
def add_offline(self, node):
|
||||
name = list(node.keys())[0]
|
||||
values = list(node.values())[0]
|
||||
self.nodes[name] = values
|
||||
|
||||
def add_event(self, event):
|
||||
name = list(event.keys())[0]
|
||||
values = list(event.values())[0]
|
||||
params = values.get('params')
|
||||
event = params[0].get('event')
|
||||
info = params[0].get('info')
|
||||
|
||||
t = time.localtime()
|
||||
current_time = time.strftime('%H:%M:%S', t)
|
||||
|
||||
match event:
|
||||
case 'send':
|
||||
nano = info.get('time')
|
||||
cmd = info.get('cmd')
|
||||
ev_info = info.get('info')
|
||||
t = (dt.datetime
|
||||
.fromtimestamp(int(nano)/1000000000)
|
||||
.strftime('%H:%M:%S'))
|
||||
msgs = self.nodes[name]['msgs']
|
||||
msgs[name].append((t, "send", cmd, ev_info))
|
||||
case 'recv':
|
||||
nano = info.get('time')
|
||||
cmd = info.get('cmd')
|
||||
ev_info = info.get('info')
|
||||
t = (dt.datetime
|
||||
.fromtimestamp(int(nano)/1000000000)
|
||||
.strftime('%H:%M:%S'))
|
||||
msgs = self.nodes[name]['msgs']
|
||||
msgs[name].append((t, "recv", cmd, ev_info))
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.nodes}'
|
||||
74
bin/deg/src/rpc.py
Normal file
74
bin/deg/src/rpc.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import random
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
|
||||
class JsonRpc:
|
||||
|
||||
async def start(self, host, port):
|
||||
logging.info(f"trying to connect to {host}:{port}")
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
|
||||
async def stop(self):
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
|
||||
async def _make_request(self, method, params):
|
||||
ident = random.randint(0, 2**16)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": ident,
|
||||
}
|
||||
|
||||
message = json.dumps(request) + "\n"
|
||||
self.writer.write(message.encode())
|
||||
await self.writer.drain()
|
||||
data = await self.reader.readline()
|
||||
message = data.decode().strip()
|
||||
response = json.loads(message)
|
||||
return response
|
||||
|
||||
async def _subscribe(self, method, params):
|
||||
ident = random.randint(0, 2**16)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": ident,
|
||||
}
|
||||
|
||||
message = json.dumps(request) + "\n"
|
||||
self.writer.write(message.encode())
|
||||
await self.writer.drain()
|
||||
logging.debug("Subscribed")
|
||||
|
||||
async def ping(self):
|
||||
return await self._make_request("ping", [])
|
||||
|
||||
async def deg_switch(self, state):
|
||||
return await self._make_request("deg.switch", [state])
|
||||
|
||||
async def deg_subscribe_events(self):
|
||||
return await self._subscribe("deg.subscribe_events", [])
|
||||
576
bin/deg/src/scroll.py
Normal file
576
bin/deg/src/scroll.py
Normal file
@@ -0,0 +1,576 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import urwid
|
||||
from urwid.widget import (BOX, FLOW, FIXED)
|
||||
|
||||
# Scroll actions
|
||||
SCROLL_LINE_UP = 'line up'
|
||||
SCROLL_LINE_DOWN = 'line down'
|
||||
SCROLL_PAGE_UP = 'page up'
|
||||
SCROLL_PAGE_DOWN = 'page down'
|
||||
SCROLL_TO_TOP = 'to top'
|
||||
SCROLL_TO_END = 'to end'
|
||||
|
||||
# Scrollbar positions
|
||||
SCROLLBAR_LEFT = 'left'
|
||||
SCROLLBAR_RIGHT = 'right'
|
||||
|
||||
# Add support for ScrollBar class (see stig.tui.scroll)
|
||||
# https://github.com/urwid/urwid/issues/226
|
||||
class ListBox_patched(urwid.ListBox):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._rows_max = None
|
||||
|
||||
def _invalidate(self):
|
||||
super()._invalidate()
|
||||
self._rows_max = None
|
||||
|
||||
def get_scrollpos(self, size, focus=False):
|
||||
"""Current scrolling position
|
||||
Lower limit is 0, upper limit is the highest index of `body`.
|
||||
"""
|
||||
middle, top, bottom = self.calculate_visible(size, focus)
|
||||
if middle is None:
|
||||
return 0
|
||||
else:
|
||||
offset_rows, _, focus_pos, _, _ = middle
|
||||
maxcol, maxrow = size
|
||||
flow_size = (maxcol,)
|
||||
|
||||
body = self.body
|
||||
if hasattr(body, 'positions'):
|
||||
# For body[pos], pos can be anything, not just an int. In that
|
||||
# case, the positions() method returns an interable of valid
|
||||
# positions.
|
||||
positions = tuple(self.body.positions())
|
||||
focus_index = positions.index(focus_pos)
|
||||
widgets_above_focus = (body[pos] for pos in positions[:focus_index])
|
||||
else:
|
||||
# Treat body like a normal list
|
||||
widgets_above_focus = (w for w in body[:focus_pos])
|
||||
|
||||
rows_above_focus = sum(w.rows(flow_size) for w in widgets_above_focus)
|
||||
rows_above_top = rows_above_focus - offset_rows
|
||||
return rows_above_top
|
||||
|
||||
def rows_max(self, size, focus=False):
|
||||
if self._rows_max is None:
|
||||
flow_size = (size[0],)
|
||||
body = self.body
|
||||
if hasattr(body, 'positions'):
|
||||
self._rows_max = sum(body[pos].rows(flow_size) for pos in body.positions())
|
||||
else:
|
||||
self._rows_max = sum(w.rows(flow_size) for w in self.body)
|
||||
return self._rows_max
|
||||
|
||||
urwid.ListBox = ListBox_patched
|
||||
|
||||
class Scrollable(urwid.WidgetDecoration):
|
||||
|
||||
def sizing(self):
|
||||
return frozenset([BOX,])
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, widget):
|
||||
"""Box widget that makes a fixed or flow widget vertically scrollable
|
||||
|
||||
TODO: Focusable widgets are handled, including switching focus, but
|
||||
possibly not intuitively, depending on the arrangement of widgets. When
|
||||
switching focus to a widget that is ouside of the visible part of the
|
||||
original widget, the canvas scrolls up/down to the focused widget. It
|
||||
would be better to scroll until the next focusable widget is in sight
|
||||
first. But for that to work we must somehow obtain a list of focusable
|
||||
rows in the original canvas.
|
||||
"""
|
||||
if not any(s in widget.sizing() for s in (FIXED, FLOW)):
|
||||
raise ValueError('Not a fixed or flow widget: %r' % widget)
|
||||
self._trim_top = 0
|
||||
self._scroll_action = None
|
||||
self._forward_keypress = None
|
||||
self._old_cursor_coords = None
|
||||
self._rows_max_cached = 0
|
||||
self.__super.__init__(widget)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol, maxrow = size
|
||||
|
||||
# Render complete original widget
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
canv_full = ow.render(ow_size, focus)
|
||||
|
||||
# Make full canvas editable
|
||||
canv = urwid.CompositeCanvas(canv_full)
|
||||
canv_cols, canv_rows = canv.cols(), canv.rows()
|
||||
|
||||
if canv_cols <= maxcol:
|
||||
pad_width = maxcol - canv_cols
|
||||
if pad_width > 0:
|
||||
# Canvas is narrower than available horizontal space
|
||||
canv.pad_trim_left_right(0, pad_width)
|
||||
|
||||
if canv_rows <= maxrow:
|
||||
fill_height = maxrow - canv_rows
|
||||
if fill_height > 0:
|
||||
# Canvas is lower than available vertical space
|
||||
canv.pad_trim_top_bottom(0, fill_height)
|
||||
|
||||
if canv_cols <= maxcol and canv_rows <= maxrow:
|
||||
# Canvas is small enough to fit without trimming
|
||||
return canv
|
||||
|
||||
self._adjust_trim_top(canv, size)
|
||||
|
||||
# Trim canvas if necessary
|
||||
trim_top = self._trim_top
|
||||
trim_end = canv_rows - maxrow - trim_top
|
||||
trim_right = canv_cols - maxcol
|
||||
if trim_top > 0:
|
||||
canv.trim(trim_top)
|
||||
if trim_end > 0:
|
||||
canv.trim_end(trim_end)
|
||||
if trim_right > 0:
|
||||
canv.pad_trim_left_right(0, -trim_right)
|
||||
|
||||
# Disable cursor display if cursor is outside of visible canvas parts
|
||||
if canv.cursor is not None:
|
||||
curscol, cursrow = canv.cursor
|
||||
if cursrow >= maxrow or cursrow < 0:
|
||||
canv.cursor = None
|
||||
|
||||
# Figure out whether we should forward keypresses to original widget
|
||||
if canv.cursor is not None:
|
||||
# Trimmed canvas contains the cursor, e.g. in an Edit widget
|
||||
self._forward_keypress = True
|
||||
else:
|
||||
if canv_full.cursor is not None:
|
||||
# Full canvas contains the cursor, but scrolled out of view
|
||||
self._forward_keypress = False
|
||||
else:
|
||||
# Original widget does not have a cursor, but may be selectable
|
||||
|
||||
# FIXME: Using ow.selectable() is bad because the original
|
||||
# widget may be selectable because it's a container widget with
|
||||
# a key-grabbing widget that is scrolled out of view.
|
||||
# ow.selectable() returns True anyway because it doesn't know
|
||||
# how we trimmed our canvas.
|
||||
#
|
||||
# To fix this, we need to resolve ow.focus and somehow
|
||||
# ask canv whether it contains bits of the focused widget. I
|
||||
# can't see a way to do that.
|
||||
if ow.selectable():
|
||||
self._forward_keypress = True
|
||||
else:
|
||||
self._forward_keypress = False
|
||||
|
||||
return canv
|
||||
|
||||
def keypress(self, size, key):
|
||||
# Maybe offer key to original widget
|
||||
if self._forward_keypress:
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
|
||||
# Remember previous cursor position if possible
|
||||
if hasattr(ow, 'get_cursor_coords'):
|
||||
self._old_cursor_coords = ow.get_cursor_coords(ow_size)
|
||||
|
||||
key = ow.keypress(ow_size, key)
|
||||
if key is None:
|
||||
return None
|
||||
|
||||
# Handle up/down, page up/down, etc
|
||||
command_map = self._command_map
|
||||
if command_map[key] == urwid.CURSOR_UP:
|
||||
self._scroll_action = SCROLL_LINE_UP
|
||||
elif command_map[key] == urwid.CURSOR_DOWN:
|
||||
self._scroll_action = SCROLL_LINE_DOWN
|
||||
|
||||
elif command_map[key] == urwid.CURSOR_PAGE_UP:
|
||||
self._scroll_action = SCROLL_PAGE_UP
|
||||
elif command_map[key] == urwid.CURSOR_PAGE_DOWN:
|
||||
self._scroll_action = SCROLL_PAGE_DOWN
|
||||
|
||||
elif command_map[key] == urwid.CURSOR_MAX_LEFT: # 'home'
|
||||
self._scroll_action = SCROLL_TO_TOP
|
||||
elif command_map[key] == urwid.CURSOR_MAX_RIGHT: # 'end'
|
||||
self._scroll_action = SCROLL_TO_END
|
||||
|
||||
else:
|
||||
return key
|
||||
|
||||
self._invalidate()
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
ow = self._original_widget
|
||||
if hasattr(ow, 'mouse_event'):
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
row += self._trim_top
|
||||
return ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _adjust_trim_top(self, canv, size):
|
||||
"""Adjust self._trim_top according to self._scroll_action"""
|
||||
action = self._scroll_action
|
||||
self._scroll_action = None
|
||||
|
||||
maxcol, maxrow = size
|
||||
trim_top = self._trim_top
|
||||
canv_rows = canv.rows()
|
||||
|
||||
if trim_top < 0:
|
||||
# Negative trim_top values use bottom of canvas as reference
|
||||
trim_top = canv_rows - maxrow + trim_top + 1
|
||||
|
||||
if canv_rows <= maxrow:
|
||||
self._trim_top = 0 # Reset scroll position
|
||||
return
|
||||
|
||||
def ensure_bounds(new_trim_top):
|
||||
return max(0, min(canv_rows - maxrow, new_trim_top))
|
||||
|
||||
if action == SCROLL_LINE_UP:
|
||||
self._trim_top = ensure_bounds(trim_top - 1)
|
||||
elif action == SCROLL_LINE_DOWN:
|
||||
self._trim_top = ensure_bounds(trim_top + 1)
|
||||
|
||||
elif action == SCROLL_PAGE_UP:
|
||||
self._trim_top = ensure_bounds(trim_top - maxrow+1)
|
||||
elif action == SCROLL_PAGE_DOWN:
|
||||
self._trim_top = ensure_bounds(trim_top + maxrow-1)
|
||||
|
||||
elif action == SCROLL_TO_TOP:
|
||||
self._trim_top = 0
|
||||
elif action == SCROLL_TO_END:
|
||||
self._trim_top = canv_rows - maxrow
|
||||
|
||||
else:
|
||||
self._trim_top = ensure_bounds(trim_top)
|
||||
|
||||
# If the cursor was moved by the most recent keypress, adjust trim_top
|
||||
# so that the new cursor position is within the displayed canvas part.
|
||||
# But don't do this if the cursor is at the top/bottom edge so we can still scroll out
|
||||
if self._old_cursor_coords is not None and self._old_cursor_coords != canv.cursor:
|
||||
self._old_cursor_coords = None
|
||||
curscol, cursrow = canv.cursor
|
||||
if cursrow < self._trim_top:
|
||||
self._trim_top = cursrow
|
||||
elif cursrow >= self._trim_top + maxrow:
|
||||
self._trim_top = max(0, cursrow - maxrow + 1)
|
||||
|
||||
def _get_original_widget_size(self, size):
|
||||
ow = self._original_widget
|
||||
sizing = ow.sizing()
|
||||
if FIXED in sizing:
|
||||
return ()
|
||||
elif FLOW in sizing:
|
||||
return (size[0],)
|
||||
|
||||
def get_scrollpos(self, size=None, focus=False):
|
||||
"""Current scrolling position
|
||||
|
||||
Lower limit is 0, upper limit is the maximum number of rows with the
|
||||
given maxcol minus maxrow.
|
||||
|
||||
NOTE: The returned value may be too low or too high if the position has
|
||||
changed but the widget wasn't rendered yet.
|
||||
"""
|
||||
return self._trim_top
|
||||
|
||||
def set_scrollpos(self, position):
|
||||
"""Set scrolling position
|
||||
|
||||
If `position` is positive it is interpreted as lines from the top.
|
||||
If `position` is negative it is interpreted as lines from the bottom.
|
||||
|
||||
Values that are too high or too low values are automatically adjusted
|
||||
during rendering.
|
||||
"""
|
||||
self._trim_top = int(position)
|
||||
self._invalidate()
|
||||
|
||||
def rows_max(self, size=None, focus=False):
|
||||
"""Return the number of rows for `size`
|
||||
|
||||
If `size` is not given, the currently rendered number of rows is returned.
|
||||
"""
|
||||
if size is not None:
|
||||
ow = self._original_widget
|
||||
ow_size = self._get_original_widget_size(size)
|
||||
sizing = ow.sizing()
|
||||
if FIXED in sizing:
|
||||
self._rows_max_cached = ow.pack(ow_size, focus)[1]
|
||||
elif FLOW in sizing:
|
||||
self._rows_max_cached = ow.rows(ow_size, focus)
|
||||
else:
|
||||
raise RuntimeError('Not a flow/box widget: %r' % self._original_widget)
|
||||
return self._rows_max_cached
|
||||
|
||||
|
||||
DEFAULT_THUMB_CHAR = '\u2588'
|
||||
DEFAULT_TROUGH_CHAR = " "
|
||||
DEFAULT_SIDE = SCROLLBAR_RIGHT
|
||||
|
||||
|
||||
class ScrollBar(urwid.WidgetDecoration):
|
||||
|
||||
_thumb_char = DEFAULT_THUMB_CHAR
|
||||
_trough_char = DEFAULT_TROUGH_CHAR
|
||||
_thumb_indicator_top = None
|
||||
_thumb_indicator_bottom = None
|
||||
_scroll_bar_side = DEFAULT_SIDE
|
||||
|
||||
def sizing(self):
|
||||
return frozenset((BOX,))
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, widget,
|
||||
thumb_char=None, trough_char=None,
|
||||
thumb_indicator_top=None, thumb_indicator_bottom=None,
|
||||
side=DEFAULT_SIDE, width=1,
|
||||
always_visible=False):
|
||||
"""Box widget that adds a scrollbar to `widget`
|
||||
|
||||
`widget` must be a box widget with the following methods:
|
||||
- `get_scrollpos` takes the arguments `size` and `focus` and returns
|
||||
the index of the first visible row.
|
||||
- `set_scrollpos` (optional; needed for mouse click support) takes the
|
||||
index of the first visible row.
|
||||
- `rows_max` takes `size` and `focus` and returns the total number of
|
||||
rows `widget` can render.
|
||||
|
||||
`thumb_char` is the character used for the scrollbar handle.
|
||||
`trough_char` is used for the space above and below the handle.
|
||||
`side` must be 'left' or 'right'.
|
||||
`width` specifies the number of columns the scrollbar uses.
|
||||
`always_visible` will always draw the scrollbar, even when unnecessary.
|
||||
"""
|
||||
if BOX not in widget.sizing():
|
||||
raise ValueError('Not a box widget: %r' % widget)
|
||||
self.__super.__init__(widget)
|
||||
if thumb_char is not None:
|
||||
self._thumb_char = thumb_char
|
||||
if trough_char is not None:
|
||||
self._trough_char = trough_char
|
||||
if thumb_indicator_top is not None:
|
||||
self._thumb_indicator_top = thumb_indicator_top
|
||||
if thumb_indicator_bottom is not None:
|
||||
self._thumb_indicator_bottom = thumb_indicator_bottom
|
||||
|
||||
self.scrollbar_side = side
|
||||
self.scrollbar_width = max(1, width)
|
||||
self.always_visible = always_visible
|
||||
self._original_widget_size = (0, 0)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol, maxrow = size
|
||||
|
||||
sb_width = self._scrollbar_width
|
||||
ow_size = (max(0, maxcol - sb_width), maxrow)
|
||||
sb_width = maxcol - ow_size[0]
|
||||
|
||||
ow = self._original_widget
|
||||
ow_base = self.scrolling_base_widget
|
||||
if not self.always_visible:
|
||||
ow_rows_max = ow_base.rows_max(size, focus)
|
||||
if ow_rows_max <= maxrow:
|
||||
# Canvas fits without scrolling - no scrollbar needed
|
||||
self._original_widget_size = size
|
||||
return ow.render(size, focus)
|
||||
ow_rows_max = ow_base.rows_max(ow_size, focus)
|
||||
|
||||
ow_canv = ow.render(ow_size, focus)
|
||||
self._original_widget_size = ow_size
|
||||
|
||||
pos = ow_base.get_scrollpos(ow_size, focus)
|
||||
posmax = ow_rows_max - maxrow
|
||||
|
||||
# Thumb shrinks/grows according to the ratio of
|
||||
# <number of visible lines> / <number of total lines>
|
||||
thumb_weight = min(1, maxrow / max(1, ow_rows_max))
|
||||
thumb_height = max(1, round(thumb_weight * maxrow))
|
||||
|
||||
# Thumb may only touch top/bottom if the first/last row is visible
|
||||
top_weight = float(pos) / max(1, posmax)
|
||||
top_height = int((maxrow-thumb_height) * top_weight)
|
||||
if top_height == 0 and top_weight > 0:
|
||||
top_height = 1
|
||||
|
||||
# Bottom part is remaining space
|
||||
bottom_height = maxrow - thumb_height - top_height
|
||||
assert thumb_height + top_height + bottom_height == maxrow
|
||||
|
||||
# Create scrollbar canvas
|
||||
# Creating SolidCanvases of correct height may result in "cviews do not
|
||||
# fill gaps in shard_tail!" or "cviews overflow gaps in shard_tail!"
|
||||
# exceptions. Stacking the same SolidCanvas is a workaround.
|
||||
# https://github.com/urwid/urwid/issues/226#issuecomment-437176837
|
||||
|
||||
thumb_top = thumb_bottom = None
|
||||
if (self._thumb_indicator_top
|
||||
or self._thumb_indicator_bottom) and hasattr(ow.body, "positions"):
|
||||
if hasattr(ow.body, "focus"):
|
||||
pos = ow.body.focus
|
||||
elif hasattr(ow.body, "get_focus"):
|
||||
pos = ow.body.get_focus()[1]
|
||||
|
||||
try:
|
||||
head = next(iter(ow.body.positions()))
|
||||
except StopIteration:
|
||||
head = None
|
||||
if pos == head:
|
||||
if isinstance(self._thumb_indicator_top, tuple):
|
||||
attr, char = self._thumb_indicator_top
|
||||
else:
|
||||
attr, char = None, self._thumb_indicator_top
|
||||
|
||||
if char:
|
||||
thumb_top = urwid.Text(
|
||||
(attr, char * sb_width),
|
||||
wrap="any"
|
||||
).render((sb_width,))
|
||||
if thumb_height:
|
||||
thumb_height -= 1
|
||||
try:
|
||||
tail = next(iter(ow.body.positions(reverse=True)))
|
||||
except StopIteration:
|
||||
tail = None
|
||||
if pos == tail:
|
||||
if isinstance(self._thumb_indicator_bottom, tuple):
|
||||
attr, char = self._thumb_indicator_bottom
|
||||
else:
|
||||
attr, char = None, self._thumb_indicator_bottom
|
||||
|
||||
if char:
|
||||
thumb_bottom = urwid.Text(
|
||||
(attr, char * sb_width),
|
||||
wrap="any"
|
||||
).render((sb_width,))
|
||||
if thumb_height:
|
||||
thumb_height -= 1
|
||||
|
||||
if isinstance(self._trough_char, tuple):
|
||||
trough_attr, trough_char = self._trough_char
|
||||
else:
|
||||
trough_attr, trough_char = None, self._trough_char
|
||||
|
||||
top = urwid.Text(
|
||||
(trough_attr, trough_char * top_height * sb_width),
|
||||
wrap="any"
|
||||
).render((sb_width,))
|
||||
|
||||
if isinstance(self._thumb_char, tuple):
|
||||
thumb_attr, thumb_char = self._thumb_char
|
||||
else:
|
||||
thumb_attr, thumb_char = (None, self._thumb_char)
|
||||
thumb = urwid.Text(
|
||||
(thumb_attr, thumb_char * thumb_height * sb_width),
|
||||
wrap="any"
|
||||
).render((sb_width,))
|
||||
|
||||
bottom = urwid.Text(
|
||||
(trough_attr, trough_char * bottom_height * sb_width),
|
||||
wrap="any"
|
||||
).render((sb_width,))
|
||||
|
||||
|
||||
sb_canv = urwid.CanvasCombine(
|
||||
[ (top, None, False)] * (1 if top_height else 0) +
|
||||
[ (thumb_top, None, False)] * (1 if thumb_top else 0) +
|
||||
[ (thumb, None, False)] * (1 if thumb_height else 0) +
|
||||
[ (thumb_bottom, None, False)] * (1 if thumb_bottom else 0) +
|
||||
[ (bottom, None, False)] * (1 if bottom_height else 0)
|
||||
)
|
||||
|
||||
combinelist = [(ow_canv, None, True, ow_size[0]),
|
||||
(sb_canv, None, False, sb_width)]
|
||||
if self._scrollbar_side != SCROLLBAR_LEFT:
|
||||
return urwid.CanvasJoin(combinelist)
|
||||
else:
|
||||
return urwid.CanvasJoin(reversed(combinelist))
|
||||
|
||||
@property
|
||||
def scrollbar_width(self):
|
||||
"""Columns the scrollbar uses"""
|
||||
return max(1, self._scrollbar_width)
|
||||
|
||||
@scrollbar_width.setter
|
||||
def scrollbar_width(self, width):
|
||||
self._scrollbar_width = max(1, int(width))
|
||||
self._invalidate()
|
||||
|
||||
@property
|
||||
def scrollbar_side(self):
|
||||
"""Where to display the scrollbar; must be 'left' or 'right'"""
|
||||
return self._scrollbar_side
|
||||
|
||||
@scrollbar_side.setter
|
||||
def scrollbar_side(self, side):
|
||||
if side not in (SCROLLBAR_LEFT, SCROLLBAR_RIGHT):
|
||||
raise ValueError('scrollbar_side must be "left" or "right", not %r' % side)
|
||||
self._scrollbar_side = side
|
||||
self._invalidate()
|
||||
|
||||
@property
|
||||
def scrolling_base_widget(self):
|
||||
"""Nearest `original_widget` that is compatible with the scrolling API"""
|
||||
def orig_iter(w):
|
||||
while hasattr(w, 'original_widget'):
|
||||
w = w.original_widget
|
||||
yield w
|
||||
yield w
|
||||
|
||||
def is_scrolling_widget(w):
|
||||
return hasattr(w, 'get_scrollpos') and hasattr(w, 'rows_max')
|
||||
|
||||
for w in orig_iter(self):
|
||||
if is_scrolling_widget(w):
|
||||
return w
|
||||
raise ValueError('Not compatible to be wrapped by ScrollBar: %r' % w)
|
||||
|
||||
def keypress(self, size, key):
|
||||
return self._original_widget.keypress(self._original_widget_size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
ow = self._original_widget
|
||||
ow_size = self._original_widget_size
|
||||
handled = False
|
||||
if hasattr(ow, 'mouse_event'):
|
||||
handled = ow.mouse_event(ow_size, event, button, col, row, focus)
|
||||
|
||||
if not handled and hasattr(ow, 'set_scrollpos'):
|
||||
if button == 4: # scroll wheel up
|
||||
pos = ow.get_scrollpos(ow_size)
|
||||
ow.set_scrollpos(pos - 1)
|
||||
return True
|
||||
elif button == 5: # scroll wheel down
|
||||
pos = ow.get_scrollpos(ow_size)
|
||||
ow.set_scrollpos(pos + 1)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ["Scrollable", "ScrollBar"]
|
||||
67
bin/deg/src/util.py
Normal file
67
bin/deg/src/util.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import toml
|
||||
import platform
|
||||
|
||||
def get_os():
|
||||
if sys.platform.startswith('java'):
|
||||
os_name = platform.java_ver()[3][0]
|
||||
if os_name.startswith('Windows'):
|
||||
system = 'win32'
|
||||
elif os_name.startswith('Mac'):
|
||||
system = 'macOS'
|
||||
else:
|
||||
system = 'linux'
|
||||
else:
|
||||
system = sys.platform
|
||||
return system
|
||||
|
||||
def user_config_dir(appname, system):
|
||||
if system == "win32":
|
||||
path = windows_dir(appname)
|
||||
elif system == 'macOS':
|
||||
path = os.path.expanduser('~/Library/Preferences/')
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
|
||||
path = os.path.join(path, appname)
|
||||
return path
|
||||
|
||||
def windows_dir(appname):
|
||||
appauthor = appname
|
||||
const = "CSIDL_APPDATA"
|
||||
path = os.path.normpath(_get_win_folder(const))
|
||||
path = os.path.join(path, appname)
|
||||
return path
|
||||
|
||||
def spawn_config(path):
|
||||
file_exists = os.path.exists(path)
|
||||
if file_exists:
|
||||
with open(path) as f:
|
||||
cfg = toml.load(f)
|
||||
return cfg
|
||||
else:
|
||||
with open('deg_config.toml') as f:
|
||||
cfg = toml.load(f)
|
||||
with open(path, 'w') as f:
|
||||
toml.dump(cfg, f)
|
||||
print(f"Config file created in {path}. Please review it and try again.")
|
||||
sys.exit(0)
|
||||
|
||||
273
bin/deg/src/view.py
Normal file
273
bin/deg/src/view.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dyne.org foundation
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import urwid
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from src.scroll import ScrollBar, Scrollable
|
||||
|
||||
class DegWidget(urwid.WidgetWrap):
|
||||
def __init__(self, node_name, session):
|
||||
self.node_name = node_name
|
||||
self.session = session
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
def update(self, txt):
|
||||
super().__init__(txt)
|
||||
self._w = urwid.AttrWrap(self._w, None)
|
||||
self._w.focus_attr = 'line'
|
||||
|
||||
|
||||
class Node(DegWidget):
|
||||
def set_txt(self, is_empty: bool):
|
||||
if is_empty:
|
||||
txt = urwid.Text(f"{self.node_name} (offline)")
|
||||
super().update(txt)
|
||||
else:
|
||||
txt = urwid.Text(f"{self.node_name}")
|
||||
super().update(txt)
|
||||
|
||||
|
||||
class Session(DegWidget):
|
||||
def set_txt(self):
|
||||
txt = urwid.Text(f" {self.session}")
|
||||
super().update(txt)
|
||||
|
||||
|
||||
class View():
|
||||
palette = [
|
||||
('body','light gray','default', 'standout'),
|
||||
('line','dark cyan','default','standout'),
|
||||
]
|
||||
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
info_text = urwid.Text("")
|
||||
self.pile = urwid.Pile([info_text])
|
||||
scroll = ScrollBar(Scrollable(self.pile))
|
||||
rightbox = urwid.LineBox(scroll)
|
||||
self.listbox_content = []
|
||||
self.listwalker = urwid.SimpleListWalker(self.listbox_content)
|
||||
self.listw = self.listwalker.contents
|
||||
self.list = urwid.ListBox(self.listwalker)
|
||||
leftbox = urwid.LineBox(self.list)
|
||||
columns = urwid.Columns([leftbox, rightbox], focus_column=0)
|
||||
self.ui = urwid.Frame(urwid.AttrWrap( columns, 'body' ))
|
||||
self.known_nodes = []
|
||||
self.live_nodes = []
|
||||
self.dead_nodes = []
|
||||
self.refresh = False
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Render deg.get_info() RPC call
|
||||
#-----------------------------------------------------------------
|
||||
def draw_info(self, node_name, info):
|
||||
logging.debug(f'info {info}')
|
||||
# info = info['result']
|
||||
# if 'eventgraph_info' in info:
|
||||
# logging.debug(f'info {info}')
|
||||
node = Node(node_name, "node")
|
||||
node.set_txt(False)
|
||||
self.listw.append(node)
|
||||
|
||||
# info = info['eventgraph_info']
|
||||
|
||||
if 'current_genesis' in info and info['current_genesis']:
|
||||
session = Session(node_name, "current_genesis")
|
||||
session.set_txt()
|
||||
self.listw.append(session)
|
||||
|
||||
if 'broadcasted_ids' in info and info['broadcasted_ids']:
|
||||
session = Session(node_name, "broadcasted_ids")
|
||||
session.set_txt()
|
||||
self.listw.append(session)
|
||||
|
||||
if 'synced' in info and info['synced']:
|
||||
session = Session(node_name, "synced")
|
||||
session.set_txt()
|
||||
self.listw.append(session)
|
||||
|
||||
if 'unreferenced_tips' in info and info['unreferenced_tips']:
|
||||
session = Session(node_name, "unreferenced_tips")
|
||||
session.set_txt()
|
||||
self.listw.append(session)
|
||||
|
||||
|
||||
def draw_empty(self, node_name, info):
|
||||
node = Node(node_name, "node")
|
||||
node.set_txt(True)
|
||||
self.listw.append(node)
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Render deg.subscribe_events() RPC call
|
||||
# Right hand menu only
|
||||
#-----------------------------------------------------------------
|
||||
def fill_right_box(self):
|
||||
self.pile.contents.clear()
|
||||
focus_w = self.list.get_focus()
|
||||
if focus_w[0] is None:
|
||||
return
|
||||
session = focus_w[0].session
|
||||
|
||||
if session == "node":
|
||||
node_name = focus_w[0].node_name
|
||||
info = self.model.nodes.get(node_name)
|
||||
if info['msgs']:
|
||||
msg = info['msgs'].get(node_name)
|
||||
for m in msg:
|
||||
time = m[0]
|
||||
event = m[1]
|
||||
event_info = m[2]
|
||||
msg = m[3]
|
||||
self.pile.contents.append((urwid.Text(
|
||||
f"{time}: {event} {event_info}: {msg}"),
|
||||
self.pile.options()))
|
||||
|
||||
if session == "current_genesis":
|
||||
key = "current_genesis"
|
||||
node_name = focus_w[0].node_name
|
||||
info = self.model.nodes.get(node_name)
|
||||
|
||||
if key in info:
|
||||
ev = info.get(key)
|
||||
self.pile.contents.append((
|
||||
urwid.Text(f" {ev}"),
|
||||
self.pile.options()))
|
||||
|
||||
if session == "broadcasted_ids":
|
||||
key = "broadcasted_ids"
|
||||
node_name = focus_w[0].node_name
|
||||
info = self.model.nodes.get(node_name)
|
||||
|
||||
if key in info:
|
||||
ev = list(info.get(key))
|
||||
if info['msgs']:
|
||||
msg = info['msgs'].get(node_name)
|
||||
for m in msg:
|
||||
event = m[1]
|
||||
event_info = m[2]
|
||||
msg = m[3]
|
||||
if event_info == "EventPut" and event == "send" and msg not in ev:
|
||||
ev.extend(msg)
|
||||
self.pile.contents.append((
|
||||
urwid.Text(f" {ev}"),
|
||||
self.pile.options()))
|
||||
|
||||
if session == "synced":
|
||||
key = "synced"
|
||||
node_name = focus_w[0].node_name
|
||||
info = self.model.nodes.get(node_name)
|
||||
|
||||
if key in info:
|
||||
ev = info.get(key)
|
||||
self.pile.contents.append((
|
||||
urwid.Text(f" {ev}"),
|
||||
self.pile.options()))
|
||||
|
||||
if session == "unreferenced_tips":
|
||||
key = "unreferenced_tips"
|
||||
node_name = focus_w[0].node_name
|
||||
info = self.model.nodes.get(node_name)
|
||||
|
||||
if key in info:
|
||||
ev = info.get(key)
|
||||
self.pile.contents.append((
|
||||
urwid.Text(f" {ev}"),
|
||||
self.pile.options()))
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Sort through node info, checking whether we are already
|
||||
# tracking this node or if the node's state has changed.
|
||||
#-----------------------------------------------------------------
|
||||
def sort(self, nodes):
|
||||
for name, info in nodes:
|
||||
if bool(info) and name not in self.live_nodes:
|
||||
self.live_nodes.append(name)
|
||||
if not bool(info) and name not in self.dead_nodes:
|
||||
self.dead_nodes.append(name)
|
||||
if bool(info) and name in self.dead_nodes:
|
||||
logging.debug("Refresh: dead node online.")
|
||||
self.refresh = True
|
||||
if not bool(info) and name in self.live_nodes:
|
||||
logging.debug("Refresh: online node offline.")
|
||||
self.refresh = True
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Checks whether we are already displaying this node, and draw
|
||||
# it if not.
|
||||
#-----------------------------------------------------------------
|
||||
async def display(self, nodes):
|
||||
for name, info in nodes:
|
||||
if name in self.live_nodes and name not in self.known_nodes:
|
||||
self.draw_info(name, info)
|
||||
if name in self.dead_nodes and name not in self.known_nodes:
|
||||
self.draw_empty(name, info)
|
||||
if self.refresh:
|
||||
logging.debug("Refresh initiated.")
|
||||
await asyncio.sleep(0.1)
|
||||
self.known_nodes.clear()
|
||||
self.live_nodes.clear()
|
||||
self.dead_nodes.clear()
|
||||
self.refresh = False
|
||||
self.listw.clear()
|
||||
logging.debug("Refresh complete.")
|
||||
#-----------------------------------------------------------------
|
||||
# Handle events.
|
||||
#-----------------------------------------------------------------
|
||||
def draw_events(self, nodes):
|
||||
for name, info in nodes:
|
||||
if bool(info) and name in self.known_nodes:
|
||||
# info = info ['result']
|
||||
self.fill_right_box()
|
||||
|
||||
# logging.debug(f"info: {info}")
|
||||
|
||||
if 'unreferenced_tips' in info:
|
||||
val = info['unreferenced_tips']
|
||||
if not bool(val) or not val == None:
|
||||
continue
|
||||
logging.debug(f"Refresh: unreferenced_tips online")
|
||||
self.refresh = True
|
||||
|
||||
async def update_view(self, evloop: asyncio.AbstractEventLoop,
|
||||
loop: urwid.MainLoop):
|
||||
while True:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
nodes = self.model.nodes.items()
|
||||
evloop.call_soon(loop.draw_screen)
|
||||
|
||||
# We first ensure that we are keeping track
|
||||
# of all the displayed widgets.
|
||||
for index, item in enumerate(self.listw):
|
||||
# Keep track of known nodes.
|
||||
if item.node_name not in self.known_nodes:
|
||||
self.known_nodes.append(item.node_name)
|
||||
|
||||
self.sort(nodes)
|
||||
|
||||
await self.display(nodes)
|
||||
|
||||
# logging.debug(f"nodes: {nodes}")
|
||||
|
||||
self.draw_events(nodes)
|
||||
Reference in New Issue
Block a user