Implemented node browser and micron parser link handling.

This commit is contained in:
Mark Qvist 2021-08-27 19:58:14 +02:00
parent 6a36786c4d
commit 6e4baf3731
7 changed files with 635 additions and 30 deletions

View File

@ -70,22 +70,24 @@ class Directory:
RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR)
def lxmf_announce_received(self, source_hash, app_data):
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, False))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
if app_data != None:
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, False))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def node_announce_received(self, source_hash, app_data, associated_peer):
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, True))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if app_data != None:
timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, True))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
self.announce_stream.pop()
if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED:
node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True)
self.remember(node_entry)
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED:
node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True)
self.remember(node_entry)
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def remove_announce_with_timestamp(self, timestamp):
selected_announce = None

View File

@ -116,7 +116,7 @@ class Node:
DEFAULT_INDEX = '''>Default Home Page
This node is serving pages, but no home page file (index.mu) was found in the page storage directory. This is an auto-generated placeholder.
This node is serving pages, but the home page file (index.mu) was not found in the page storage directory. This is an auto-generated placeholder.
If you are the node operator, you can define your own home page by creating a file named `*index.mu`* in the page storage directory.
'''

View File

@ -133,6 +133,7 @@ class TextUI:
self.loop.run()
def set_colormode(self, colormode):
self.colormode = colormode
self.screen.set_terminal_properties(colormode)
self.screen.reset_default_terminal_palette()

View File

@ -0,0 +1,377 @@
import RNS
import time
import urwid
import nomadnet
import threading
from .MicronParser import markup_to_attrmaps
from nomadnet.vendor.Scrollable import *
# TODO: REMOVE
import os
class BrowserFrame(urwid.Frame):
def keypress(self, size, key):
if key == "ctrl w":
self.delegate.disconnect()
elif self.get_focus() == "body":
return super(BrowserFrame, self).keypress(size, key)
# if key == "up" and self.delegate.messagelist.top_is_visible:
# nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
# elif key == "down" and self.delegate.messagelist.bottom_is_visible:
# self.set_focus("footer")
# else:
# return super(ConversationFrame, self).keypress(size, key)
else:
return super(BrowserFrame, self).keypress(size, key)
class Browser:
DEFAULT_PATH = "/page/index.mu"
DEFAULT_TIMEOUT = 5
NO_PATH = 0x00
PATH_REQUESTED = 0x01
ESTABLISHING_LINK = 0x02
LINK_ESTABLISHED = 0x03
REQUESTING = 0x04
REQUEST_SENT = 0x05
REQUEST_FAILED = 0x06
REQUEST_TIMEOUT = 0x07
RECEIVING_RESPONSE = 0x08
DONE = 0xFF
DISCONECTED = 0xFE
def __init__(self, app, app_name, aspects, destination_hash = None, path = None, auth_identity = None, delegate = None):
self.app = app
self.g = self.app.ui.glyphs
self.delegate = delegate
self.app_name = app_name
self.aspects = aspects
self.destination_hash = destination_hash
self.path = path
self.timeout = Browser.DEFAULT_TIMEOUT
self.last_keypress = None
self.link = None
self.status = Browser.DISCONECTED
self.page_data = None
self.displayed_page_data = None
self.auth_identity = auth_identity
self.display_widget = None
self.frame = None
self.attr_maps = []
self.build_display()
if self.path == None:
self.path = Browser.DEFAULT_PATH
if self.destination_hash != None:
self.load_page()
def current_url(self):
if self.destination_hash == None:
return ""
else:
if self.path == None:
path = ""
else:
path = self.path
return RNS.hexrep(self.destination_hash, delimit=False)+":"+path
def handle_link(self, link_target):
RNS.log("Browser handling link to: "+str(link_target))
try:
self.retrieve_url(link_target)
except Exception as e:
self.browser_footer = urwid.Text("Could not open link: "+str(e))
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
def micron_released_focus(self):
if self.delegate != None:
self.delegate.focus_lists()
def build_display(self):
self.browser_header = urwid.Text("")
self.browser_footer = urwid.Text("")
self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle")
self.frame = BrowserFrame(self.browser_body, header=self.browser_header, footer=self.browser_footer)
self.frame.delegate = self
self.display_widget = urwid.AttrMap(urwid.LineBox(self.frame, title="Remote Node"), "inactive_text")
def make_status_widget(self):
return urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text(self.status_text())])
def make_control_widget(self):
return urwid.Pile([urwid.Text(self.g["node"]+" "+self.current_url()), urwid.Divider(self.g["divider1"])])
def make_request_failed_widget(self):
def back_action(sender):
self.status = Browser.DONE
self.destination_hash = self.previous_destination_hash
self.path = self.previous_path
self.update_display()
columns = urwid.Columns([
("weight", 0.5, urwid.Text(" ")),
(8, urwid.Button("Back", on_press=back_action)),
("weight", 0.5, urwid.Text(" "))
])
pile = urwid.Pile([
urwid.Text("!\n\n"+self.status_text()+"\n", align="center"),
columns
])
return urwid.Filler(pile, "middle")
def update_display(self):
if self.status == Browser.DISCONECTED:
self.display_widget.set_attr_map({None: "inactive_text"})
self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle")
self.browser_footer = urwid.Text("")
self.browser_header = urwid.Text("")
else:
self.display_widget.set_attr_map({None: "body_text"})
self.browser_header = self.make_control_widget()
if self.status == Browser.DONE:
self.browser_footer = self.make_status_widget()
self.update_page_display()
elif self.status <= Browser.REQUEST_SENT:
if len(self.attr_maps) == 0:
self.browser_body = urwid.Filler(urwid.Text("Retrieving\n["+self.current_url()+"]", align="center"), "middle")
self.browser_footer = self.make_status_widget()
elif self.status == Browser.REQUEST_FAILED:
self.browser_body = self.make_request_failed_widget()
self.browser_footer = urwid.Text("")
elif self.status == Browser.REQUEST_TIMEOUT:
self.browser_body = self.make_request_failed_widget()
self.browser_footer = urwid.Text("")
else:
pass
self.frame.contents["body"] = (self.browser_body, self.frame.options())
self.frame.contents["header"] = (self.browser_header, self.frame.options())
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
def update_page_display(self):
pile = urwid.Pile(self.attr_maps)
self.browser_body = urwid.AttrMap(ScrollBar(Scrollable(pile), thumb_char="\u2503", trough_char=" "), "scrollbar")
def identify(self):
if self.link != None:
if self.link.status == RNS.Link.ACTIVE:
self.link.identify(self.auth_identity)
def disconnect(self):
if self.link != None:
self.link.teardown()
self.attr_maps = []
self.status = Browser.DISCONECTED
self.update_display()
def retrieve_url(self, url):
self.previous_destination_hash = self.destination_hash
self.previous_path = self.path
destination_hash = None
path = None
components = url.split(":")
if len(components) == 1:
if len(components[0]) == 20:
try:
destination_hash = bytes.fromhex(components[0])
except Exception as e:
raise ValueError("Malformed URL")
path = Browser.DEFAULT_PATH
else:
raise ValueError("Malformed URL")
elif len(components) == 2:
if len(components[0]) == 20:
try:
destination_hash = bytes.fromhex(components[0])
except Exception as e:
raise ValueError("Malformed URL")
path = components[1]
if len(path) == 0:
path = Browser.DEFAULT_PATH
else:
if len(components[0]) == 0:
if self.destination_hash != None:
destination_hash = self.destination_hash
path = components[1]
if len(path) == 0:
path = Browser.DEFAULT_PATH
else:
raise ValueError("Malformed URL")
else:
raise ValueError("Malformed URL")
else:
raise ValueError("Malformed URL")
if destination_hash != None and path != None:
self.set_destination_hash(destination_hash)
self.set_path(path)
self.load_page()
def set_destination_hash(self, destination_hash):
if len(destination_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
self.destination_hash = destination_hash
return True
else:
return False
def set_path(self, path):
self.path = path
def set_timeout(self, timeout):
self.timeout = timeout
def load_page(self):
load_thread = threading.Thread(target=self.__load)
load_thread.setDaemon(True)
load_thread.start()
def __load(self):
# If an established link exists, but it doesn't match the target
# destination, we close and clear it.
if self.link != None and self.link.destination.hash != self.destination_hash:
self.link.close()
self.link = None
# If no link to the destination exists, we create one.
if self.link == None:
if not RNS.Transport.has_path(self.destination_hash):
self.status = Browser.NO_PATH
self.update_display()
RNS.Transport.request_path(self.destination_hash)
self.status = Browser.PATH_REQUESTED
self.update_display()
pr_time = time.time()
while not RNS.Transport.has_path(self.destination_hash):
now = time.time()
if now > pr_time+self.timeout:
self.request_timeout()
time.sleep(0.25)
self.status = Browser.ESTABLISHING_LINK
self.update_display()
identity = RNS.Identity.recall(self.destination_hash)
destination = RNS.Destination(
identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
self.app_name,
self.aspects
)
self.link = RNS.Link(destination, established_callback = self.link_established, closed_callback = self.link_closed)
l_time = time.time()
while not self.status == Browser.LINK_ESTABLISHED:
now = time.time()
if now > l_time+self.timeout:
self.request_timeout()
time.sleep(0.25)
self.update_display()
# Send the request
self.status = Browser.REQUESTING
self.update_display()
receipt = self.link.request(
self.path,
data = None,
response_callback = self.response_received,
failed_callback = self.request_failed,
timeout = self.timeout
)
self.last_request_receipt = receipt
self.last_request_id = receipt.request_id
self.status = Browser.REQUEST_SENT
self.update_display()
def link_established(self, link):
self.status = Browser.LINK_ESTABLISHED
def link_closed(self, link):
if self.status == Browser.DISCONECTED or self.status == Browser.DONE:
self.link = None
else:
self.link = None
self.status = Browser.REQUEST_FAILED
self.update_display()
def response_received(self, request_receipt):
try:
self.status = Browser.DONE
self.page_data = request_receipt.response
self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.update_display()
except Exception as e:
RNS.log("An error occurred while handling response. The contained exception was: "+str(e))
def request_failed(self, request_receipt=None):
if request_receipt != None:
if request_receipt.request_id == self.last_request_id:
self.status = Browser.REQUEST_FAILED
self.update_display()
else:
self.status = Browser.REQUEST_FAILED
self.update_display()
def request_timeout(self, request_receipt=None):
self.status = Browser.REQUEST_TIMEOUT
self.update_display()
def status_text(self):
if self.status == Browser.NO_PATH:
return "No path to destination known"
elif self.status == Browser.PATH_REQUESTED:
return "Path requested, waiting for path..."
elif self.status == Browser.ESTABLISHING_LINK:
return "Establishing link..."
elif self.status == Browser.LINK_ESTABLISHED:
return "Link established"
elif self.status == Browser.REQUESTING:
return "Sending request..."
elif self.status == Browser.REQUEST_SENT:
return "Request sent, awaiting response..."
elif self.status == Browser.REQUEST_FAILED:
return "Request failed"
elif self.status == Browser.REQUEST_TIMEOUT:
return "Request timed out"
elif self.status == Browser.RECEIVING_RESPONSE:
return "Receiving response..."
elif self.status == Browser.DONE:
return "Done"
elif self.status == Browser.DISCONECTED:
return "Disconnected"
else:
return "Browser Status Unknown"

View File

@ -64,6 +64,7 @@ class GuideEntry(urwid.WidgetWrap):
def __init__(self, app, reader, topic_name):
self.app = app
self.reader = reader
self.last_keypress = None
g = self.app.ui.glyphs
widget = ListEntry(topic_name)
@ -76,10 +77,13 @@ class GuideEntry(urwid.WidgetWrap):
def display_topic(self, event, topic):
markup = TOPICS[topic]
attrmaps = markup_to_attrmaps(markup)
attrmaps = markup_to_attrmaps(markup, url_delegate=None)
self.reader.set_content_widgets(attrmaps)
def micron_released_focus(self):
self.reader.focus_topics()
class TopicList(urwid.WidgetWrap):
def __init__(self, app, guide_display):
self.app = app
@ -142,12 +146,21 @@ class GuideDisplay():
def shortcuts(self):
return self.shortcuts_display
def focus_topics(self):
self.columns.focus_position = 0
TOPIC_INTRODUCTION = '''>Nomad Network
`c`*Communicate Freely.`*
`a
TODO: REMOVE
This is a `F07flink `[With a label`344858860838a8d9f8ed:/page/test] to some resource`f.
This is a link `*`[With a label`:/page/test]`* to some resource.
This is a link `[With a label`:] to some resource.
This is a link `*`[With a label`344858860838a8d9f8ed] to some`* resource.
The intention with this program is to provide a tool to that allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them.
Nomad Network is build on LXMF and Reticulum, which together provides the cryptographic mesh functionality and peer-to-peer message routing that Nomad Network relies on. This foundation also makes it possible to use the program over a very wide variety of communication mediums, from packet radio to gigabit fiber.

View File

@ -1,5 +1,8 @@
import nomadnet
import urwid
import time
from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords
import re
STYLES = {
@ -10,11 +13,12 @@ STYLES = {
}
SYNTH_STYLES = []
SYNTH_SPECS = {}
SECTION_INDENT = 2
INDENT_RIGHT = 1
def markup_to_attrmaps(markup):
def markup_to_attrmaps(markup, url_delegate = None):
attrmaps = []
state = {
@ -39,7 +43,7 @@ def markup_to_attrmaps(markup):
for line in lines:
if len(line) > 0:
display_widget = parse_line(line, state)
display_widget = parse_line(line, state, url_delegate)
else:
display_widget = urwid.Text("")
@ -50,7 +54,7 @@ def markup_to_attrmaps(markup):
return attrmaps
def parse_line(line, state):
def parse_line(line, state, url_delegate):
if len(line) > 0:
first_char = line[0]
@ -68,7 +72,7 @@ def parse_line(line, state):
# Check for section heading reset
elif first_char == "<":
state["depth"] = 0
return parse_line(line[1:], state)
return parse_line(line[1:], state, url_delegate)
# Check for section headings
elif first_char == ">":
@ -88,7 +92,7 @@ def parse_line(line, state):
style_to_state(style, state)
heading_style = make_style(state)
output = make_output(state, line)
output = make_output(state, line, url_delegate)
style_to_state(latched_style, state)
@ -114,13 +118,18 @@ def parse_line(line, state):
else:
return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))
output = make_output(state, line)
output = make_output(state, line, url_delegate)
if output != None:
if state["depth"] == 0:
return urwid.Text(output, align=state["align"])
if url_delegate != None:
text_widget = LinkableText(output, align=state["align"], delegate=url_delegate)
else:
return urwid.Padding(urwid.Text(output, align=state["align"]), left=left_indent(state), right=right_indent(state))
text_widget = urwid.Text(output, align=state["align"])
if state["depth"] == 0:
return text_widget
else:
return urwid.Padding(text_widget, left=left_indent(state), right=right_indent(state))
else:
return None
else:
@ -180,11 +189,15 @@ def make_style(state):
if not name in SYNTH_STYLES:
screen = nomadnet.NomadNetworkApp.get_shared_instance().ui.screen
screen.register_palette_entry(name, low_color(fg)+format_string,low_color(bg),mono_color(fg, bg)+format_string,high_color(fg)+format_string,high_color(bg))
synth_spec = screen._palette[name]
SYNTH_STYLES.append(name)
if not name in SYNTH_SPECS:
SYNTH_SPECS[name] = synth_spec
return name
def make_output(state, line):
def make_output(state, line, url_delegate):
output = []
if state["literal"]:
if line == "\\`=":
@ -246,6 +259,58 @@ def make_output(state, line):
elif c == "a":
state["align"] = state["default_align"]
elif c == "[":
endpos = line[i:].find("]")
if endpos == -1:
pass
else:
link_data = line[i+1:i+endpos]
skip = endpos
link_components = link_data.split("`")
if len(link_components) == 1:
link_label = ""
link_url = link_data
elif len(link_components) == 2:
link_label = link_components[0]
link_url = link_components[1]
else:
link_url = ""
link_label = ""
if len(link_url) != 0:
if link_label == "":
link_label = link_url
# First generate output until now
if len(part) > 0:
output.append(make_part(state, part))
cm = nomadnet.NomadNetworkApp.get_shared_instance().ui.colormode
specname = make_style(state)
speclist = SYNTH_SPECS[specname]
orig_spec = urwid.AttrSpec('underline', 'default', cm)
if cm == 1:
orig_spec = speclist[0]
elif cm == 16:
orig_spec = speclist[1]
elif cm == 88:
orig_spec = speclist[2]
elif cm == 256:
orig_spec = speclist[3]
elif cm == 2**24:
orig_spec = speclist[4]
if url_delegate != None:
linkspec = LinkSpec(link_url, orig_spec)
output.append((linkspec, link_label))
else:
output.append(make_part(state, link_label))
mode = "text"
if len(part) > 0:
output.append(make_part(state, part))
@ -272,4 +337,141 @@ def make_output(state, line):
if len(output) > 0:
return output
else:
return None
return None
class LinkSpec(urwid.AttrSpec):
def __init__(self, link_target, orig_spec):
self.link_target = link_target
urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background)
class LinkableText(urwid.Text):
ignore_focus = False
_selectable = True
signals = ["click", "change"]
def __init__(self, text, align=None, cursor_position=0, delegate=None):
self.__super.__init__(text, align=align)
self.delegate = delegate
self._cursor_position = 0
self.key_timeout = 3
if self.delegate != None:
self.delegate.last_keypress = 0
def handle_link(self, link_target):
if self.delegate != None:
self.delegate.handle_link(link_target)
def find_next_part_pos(self, pos, part_positions):
for position in part_positions:
if position > pos:
return position
return pos
def find_prev_part_pos(self, pos, part_positions):
nextpos = pos
for position in part_positions:
if position < pos:
nextpos = position
return nextpos
def find_item_at_pos(self, pos):
total = 0
text, parts = self.get_text()
for i, info in enumerate(parts):
style, length = info
if total <= pos < length+total:
return style
total += length
return None
def keypress(self, size, key):
part_positions = [0]
parts = []
total = 0
text, parts = self.get_text()
for i, info in enumerate(parts):
style_name, length = info
part_positions.append(length+total)
total += length
if self.delegate != None:
self.delegate.last_keypress = time.time()
self._invalidate()
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(self.key_timeout, self.kt_event)
if self._command_map[key] == urwid.ACTIVATE:
item = self.find_item_at_pos(self._cursor_position)
if item != None:
if isinstance(item, LinkSpec):
self.handle_link(item.link_target)
elif key == "up":
self._cursor_position = 0
return key
elif key == "down":
self._cursor_position = 0
return key
elif key == "right":
self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions)
self._invalidate()
elif key == "left":
if self._cursor_position > 0:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions)
self._invalidate()
else:
if self.delegate != None:
self.delegate.micron_released_focus()
else:
return key
def kt_event(self, loop, user_data):
self._invalidate()
def render(self, size, focus=False):
now = time.time()
c = self.__super.render(size, focus)
if focus and (self.delegate == None or now < self.delegate.last_keypress+self.key_timeout):
c = urwid.CompositeCanvas(c)
c.cursor = self.get_cursor_coords(size)
return c
def get_cursor_coords(self, size):
if self._cursor_position > len(self.text):
return None
(maxcol,) = size
trans = self.get_line_translation(maxcol)
x, y = calc_coords(self.text, trans, self._cursor_position)
if maxcol <= x:
return None
return x, y
def mouse_event(self, size, event, button, x, y, focus):
if button != 1 or not is_mouse_press(event):
return False
else:
pos = (y * size[0]) + x
self._cursor_position = pos
item = self.find_item_at_pos(self._cursor_position)
if item != None:
if isinstance(item, LinkSpec):
self.handle_link(item.link_target)
self._invalidate()
self._emit("change")
return True

View File

@ -6,12 +6,14 @@ from datetime import datetime
from nomadnet.Directory import DirectoryEntry
from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY
from .Browser import Browser
class NetworkDisplayShortcuts():
def __init__(self, app):
self.app = app
g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Text("[C-l] Toggle Nodes/Announces View [C-x] Remove entry"), "shortcutbar")
self.widget = urwid.AttrMap(urwid.Text("[C-l] Toggle Nodes/Announces view [C-x] Remove entry [C-w] Disconnect remote"), "shortcutbar")
# "[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"
@ -392,6 +394,7 @@ class KnownNodes(urwid.WidgetWrap):
self.delegate.close_list_dialogs()
def confirmed(sender):
self.delegate.browser.retrieve_url(RNS.hexrep(source_hash, delimit=False))
self.delegate.close_list_dialogs()
@ -714,6 +717,8 @@ class NetworkLeftPile(urwid.Pile):
def keypress(self, size, key):
if key == "ctrl l":
self.parent.toggle_list()
elif key == "ctrl w":
self.parent.browser.disconnect()
else:
return super(NetworkLeftPile, self).keypress(size, key)
@ -725,6 +730,8 @@ class NetworkDisplay():
self.app = app
g = self.app.ui.glyphs
self.browser = Browser(self.app, "nomadnetwork", "node", auth_identity = self.app.identity, delegate = self)
self.known_nodes_display = KnownNodes(self.app)
self.network_stats_display = NetworkStats(self.app, self)
self.announce_stream_display = AnnounceStream(self.app, self)
@ -733,9 +740,9 @@ class NetworkDisplay():
self.known_nodes_display.delegate = self
self.list_display = 0
self.list_display = 1
self.left_pile = NetworkLeftPile([
("weight", 1, self.announce_stream_display),
("weight", 1, self.known_nodes_display),
("pack", self.network_stats_display),
("pack", self.local_peer_display),
])
@ -743,7 +750,7 @@ class NetworkDisplay():
self.left_pile.parent = self
self.left_area = self.left_pile
self.right_area = urwid.AttrMap(urwid.LineBox(urwid.Filler(urwid.Text("Disconnected\n"+g["arrow_l"]+" "+g["arrow_r"], align="center"), "middle"), title="Remote Node"), "inactive_text")
self.right_area = self.browser.display_widget
self.columns = urwid.Columns(
[
@ -766,6 +773,9 @@ class NetworkDisplay():
self.left_pile.contents[0] = (self.known_nodes_display, options)
self.list_display = 1
def focus_lists(self):
self.columns.focus_position = 0
def reinit_known_nodes(self):
self.known_nodes_display = KnownNodes(self.app)
self.known_nodes_display.delegate = self