From 6e4baf37313e3eb56c16c3d815adeda5c5227cf6 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 27 Aug 2021 19:58:14 +0200 Subject: [PATCH] Implemented node browser and micron parser link handling. --- nomadnet/Directory.py | 28 ++- nomadnet/Node.py | 2 +- nomadnet/ui/TextUI.py | 1 + nomadnet/ui/textui/Browser.py | 377 +++++++++++++++++++++++++++++ nomadnet/ui/textui/Guide.py | 15 +- nomadnet/ui/textui/MicronParser.py | 224 ++++++++++++++++- nomadnet/ui/textui/Network.py | 18 +- 7 files changed, 635 insertions(+), 30 deletions(-) create mode 100644 nomadnet/ui/textui/Browser.py diff --git a/nomadnet/Directory.py b/nomadnet/Directory.py index f29075a..c7f1741 100644 --- a/nomadnet/Directory.py +++ b/nomadnet/Directory.py @@ -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 diff --git a/nomadnet/Node.py b/nomadnet/Node.py index 620a480..155d4d7 100644 --- a/nomadnet/Node.py +++ b/nomadnet/Node.py @@ -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. ''' \ No newline at end of file diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py index 961c93a..c6700b9 100644 --- a/nomadnet/ui/TextUI.py +++ b/nomadnet/ui/TextUI.py @@ -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() diff --git a/nomadnet/ui/textui/Browser.py b/nomadnet/ui/textui/Browser.py new file mode 100644 index 0000000..4e506b6 --- /dev/null +++ b/nomadnet/ui/textui/Browser.py @@ -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" + + \ No newline at end of file diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py index 997d0b3..78352a8 100644 --- a/nomadnet/ui/textui/Guide.py +++ b/nomadnet/ui/textui/Guide.py @@ -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. diff --git a/nomadnet/ui/textui/MicronParser.py b/nomadnet/ui/textui/MicronParser.py index 79569e4..8ee451f 100644 --- a/nomadnet/ui/textui/MicronParser.py +++ b/nomadnet/ui/textui/MicronParser.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/nomadnet/ui/textui/Network.py b/nomadnet/ui/textui/Network.py index b6ba9de..f3e02ea 100644 --- a/nomadnet/ui/textui/Network.py +++ b/nomadnet/ui/textui/Network.py @@ -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