Explorar el Código

script/dnetview: commit dummy TUI

most of the widgets we need for dnetview are now working, but it has yet
to be plugged into real data.
lunar-mining hace 2 años
padre
commit
7d57ce77d2
Se han modificado 3 ficheros con 680 adiciones y 0 borrados
  1. 1 0
      script/dnet/requirements.txt
  2. 573 0
      script/dnet/scroll.py
  3. 106 0
      script/dnet/view.py

+ 1 - 0
script/dnet/requirements.txt

@@ -0,0 +1 @@
+urwid==2.1.2

+ 573 - 0
script/dnet/scroll.py

@@ -0,0 +1,573 @@
+# This file is part of DarkFi (https://dark.fi)
+#
+# Copyright (C) 2020-2023 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"]

+ 106 - 0
script/dnet/view.py

@@ -0,0 +1,106 @@
+# This file is part of DarkFi (https://dark.fi)
+#
+# Copyright (C) 2020-2023 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 asyncio
+import time
+from scroll import ScrollBar, Scrollable
+
+loop = asyncio.get_event_loop()
+
+class ServiceWidget(urwid.TreeWidget):
+    def get_display_text(self):
+        return self.get_node().get_value()['name']
+
+class ChildNode(urwid.TreeNode):
+    def load_widget(self):
+        return ServiceWidget(self)
+
+# Service/ session
+class ServiceNode(urwid.ParentNode):
+    def load_widget(self):
+        return ServiceWidget(self)
+
+    def load_child_keys(self):
+        data = self.get_value()
+        return range(len(data['session']))
+
+    def load_child_node(self, key):
+        childdata = self.get_value()['session'][key]
+        childdepth = self.get_depth() + 1
+        if 'session' in childdata:
+            childclass = ServiceNode
+        else:
+            childclass = ChildNode
+        return childclass(childdata, parent=self, key=key, depth=childdepth)
+
+class Dnetview:
+    palette = [
+              ('body','light gray','black', 'standout'),
+              ("line","dark cyan","black","standout"),
+              ]
+
+    def __init__(self, data=None):
+        self.topnode = ServiceNode(data)
+
+        self.listbox = urwid.Columns([urwid.TreeListBox(urwid.TreeWalker(self.topnode))])
+        self.listbox.offset_rows = 1
+        
+        list_frame = urwid.LineBox(self.listbox)
+
+        pile = urwid.Pile([])
+        loop.create_task(get_info(pile))
+
+        scroll = ScrollBar(Scrollable(pile))
+        scroll_frame = urwid.LineBox(scroll)
+
+        columns = urwid.Columns([list_frame, scroll_frame], focus_column=0)
+        self.view = urwid.Frame(urwid.AttrWrap( columns, 'body' ))
+
+    def main(self):
+        self.loop = urwid.MainLoop(self.view, self.palette,
+            event_loop=urwid.AsyncioEventLoop(loop=loop),
+            unhandled_input=self.unhandled_input)
+        self.loop.run()
+
+    def unhandled_input(self, k):
+        if k in ('q','Q'):
+            raise urwid.ExitMainLoop()
+
+
+def get_example_tree():
+    tree = {"name":"service","session":[]}
+    for i in range(2):
+        tree['session'].append({"name":f"session{str(i)}"})
+        tree['session'][i]['session']=[]
+        for j in range(2):
+            tree['session'][i]['session'].append({"name":"connection"+
+                                                      str(i) + "." + str(j)})
+    return tree
+
+async def get_info(pile):
+    while True:
+        await asyncio.sleep(0.5)
+        t = time.localtime()
+        current_time = time.strftime("%H:%M:%S", t)
+        text = urwid.Text(f"{current_time}: recv ping-pong")
+        pile.contents.append((text, pile.options()))
+
+if __name__ == '__main__':
+    sample = get_example_tree()
+    Dnetview(sample).main()
+