Compare commits

...

21 Commits
0.3.4 ... 0.3.9

Author SHA1 Message Date
Mark Qvist
2aa5c6f665 Updated version and dependencies 2023-10-07 16:44:09 +02:00
Mark Qvist
72a5ed9c8e Updated version 2023-10-03 11:54:09 +02:00
Mark Qvist
c411ce703f Fixed incorrect checkbox display in UI 2023-10-03 11:53:52 +02:00
Mark Qvist
c670aa82ad Updated RNS dependency 2023-09-22 12:03:00 +02:00
Mark Qvist
c52efbfb75 Temporarily fixed urwid at 2.1.2 due to breaking changes in 2.2.0 2023-09-21 22:58:00 +02:00
Mark Qvist
da348c3b42 Improved propagation peer list display 2023-09-19 14:46:52 +02:00
Mark Qvist
92c3c55e03 Updated version and dependencies 2023-09-19 11:56:24 +02:00
Mark Qvist
8d9f4956db Improved request failure handling 2023-09-19 11:56:04 +02:00
Mark Qvist
062e31964a Updated install section 2023-08-15 11:26:48 +02:00
Mark Qvist
f36018632f Updated install section 2023-08-15 11:25:46 +02:00
Mark Qvist
81f65e3453 Updated install section 2023-08-15 11:13:31 +02:00
Mark Qvist
34b3987ded Added error handling to micron parser mouse event translator. Fixes #32. 2023-08-14 18:02:39 +02:00
Mark Qvist
22a7acf259 Added compact announce stream option 2023-08-13 21:54:35 +02:00
Mark Qvist
919a146da1 Added alternative save node hotkey. Added save hotkey to hotkeys list. Closes #31. 2023-08-13 20:55:35 +02:00
Mark Qvist
f0a4efa28b Added alternative save node hotkey. Added save hotkey to hotkeys list. 2023-08-13 20:54:29 +02:00
Mark Qvist
3d0043499c Added error handling to lxmf.delivery announce handler. Fixes #30. Hopefully. 2023-08-13 20:37:44 +02:00
Mark Qvist
b6e6c4bd3d Updated version and dependencies 2023-06-13 19:57:49 +02:00
markqvist
d06e1d3f1b Merge pull request #27 from Swissbandit/patch-1
Update Guide.py
2023-04-02 10:21:37 +02:00
Swissbandit
02f9a5a760 Update Guide.py
fixed typo redundant .mu
2023-03-29 16:44:37 +02:00
Mark Qvist
bf7004fd0f Updated version 2023-02-17 23:09:13 +01:00
Mark Qvist
8109bce5a3 Fixed missing path in env 2023-02-17 23:04:44 +01:00
11 changed files with 182 additions and 55 deletions

View File

@@ -30,7 +30,36 @@ The easiest way to install Nomad Network is via pip:
```bash ```bash
# Install Nomad Network and dependencies # Install Nomad Network and dependencies
pip3 install nomadnet pip install nomadnet
# Run the client
nomadnet
# Or alternatively run as a daemon, with no user interface
nomadnet --daemon
# List options
nomadnet --help
```
If you are using an operating system that blocks normal user package installation via `pip`, you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file, and adding the following directive in the `[global]` section:
```text
[global]
break-system-packages = true
```
Alternatively, you can use the `pipx` tool to install Nomad Network in an isolated environment:
```bash
# Install Nomad Network
pipx install nomadnet
# Optionally install Reticulum utilities
pipx install rns
# Optionally install standalone LXMF utilities
pipx install lxmf
# Run the client # Run the client
nomadnet nomadnet

View File

@@ -34,16 +34,21 @@ class Directory:
aspect_filter = "nomadnetwork.node" aspect_filter = "nomadnetwork.node"
@staticmethod @staticmethod
def received_announce(destination_hash, announced_identity, app_data): def received_announce(destination_hash, announced_identity, app_data):
app = nomadnet.NomadNetworkApp.get_shared_instance() try:
app = nomadnet.NomadNetworkApp.get_shared_instance()
if not destination_hash in app.ignored_list: if not destination_hash in app.ignored_list:
associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity) associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity)
app.directory.node_announce_received(destination_hash, app_data, associated_peer) app.directory.node_announce_received(destination_hash, app_data, associated_peer)
app.autoselect_propagation_node() app.autoselect_propagation_node()
else: else:
RNS.log("Ignored announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) RNS.log("Ignored announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
except Exception as e:
RNS.log("Error while evaluating LXMF destination announce, ignoring announce.", RNS.LOG_DEBUG)
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
def __init__(self, app): def __init__(self, app):
@@ -113,6 +118,19 @@ class Directory:
def lxmf_announce_received(self, source_hash, app_data): def lxmf_announce_received(self, source_hash, app_data):
if app_data != None: if app_data != None:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer")) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "peer"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
@@ -123,6 +141,19 @@ class Directory:
def node_announce_received(self, source_hash, app_data, associated_peer): def node_announce_received(self, source_hash, app_data, associated_peer):
if app_data != None: if app_data != None:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node")) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "node"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:
@@ -150,6 +181,19 @@ class Directory:
break break
if not found_node: if not found_node:
if self.app.compact_stream:
try:
remove_announces = []
for announce in self.announce_stream:
if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces:
self.announce_stream.remove(a)
except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR)
timestamp = time.time() timestamp = time.time()
self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn")) self.announce_stream.insert(0, (timestamp, source_hash, app_data, "pn"))
while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH:

View File

@@ -153,6 +153,8 @@ class Node:
RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE) RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE)
if os.access(file_path, os.X_OK): if os.access(file_path, os.X_OK):
env_map = {} env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
if link_id != None: if link_id != None:
env_map["link_id"] = RNS.hexrep(link_id, delimit=False) env_map["link_id"] = RNS.hexrep(link_id, delimit=False)
if remote_identity != None: if remote_identity != None:
@@ -248,4 +250,4 @@ If you are the node operator, you can define your own home page by creating a fi
DEFAULT_NOTALLOWED = '''>Request Not Allowed DEFAULT_NOTALLOWED = '''>Request Not Allowed
You are not authorised to carry out the request. You are not authorised to carry out the request.
''' '''

View File

@@ -126,6 +126,7 @@ class NomadNetworkApp:
self.periodic_lxmf_sync = True self.periodic_lxmf_sync = True
self.lxmf_sync_interval = 360*60 self.lxmf_sync_interval = 360*60
self.lxmf_sync_limit = 8 self.lxmf_sync_limit = 8
self.compact_stream = False
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@@ -698,6 +699,10 @@ class NomadNetworkApp:
else: else:
self.lxmf_sync_limit = None self.lxmf_sync_limit = None
if option == "compact_announce_stream":
value = self.config["client"].as_bool(option)
self.compact_stream = value
if option == "user_interface": if option == "user_interface":
value = value.lower() value = value.lower()
if value == "none": if value == "none":
@@ -929,6 +934,12 @@ lxmf_sync_interval = 360
# the limit, and download everything every time. # the limit, and download everything every time.
lxmf_sync_limit = 8 lxmf_sync_limit = 8
# The announce stream will only show one entry
# per destination or node by default. You can
# change this to show as many announces as have
# been received, for every destination.
compact_announce_stream = yes
[textui] [textui]
# Amount of time to show intro screen # Amount of time to show intro screen

View File

@@ -1 +1 @@
__version__ = "0.3.4" __version__ = "0.3.9"

View File

@@ -45,6 +45,8 @@ THEMES = {
("list_normal", "dark gray", "default", "default", "#bbb", "default"), ("list_normal", "dark gray", "default", "default", "#bbb", "default"),
("list_untrusted", "dark red", "default", "default", "#a22", "default"), ("list_untrusted", "dark red", "default", "default", "#a22", "default"),
("list_focus_untrusted", "black", "light gray", "standout", "#810", "#aaa"), ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#aaa"),
("list_unresponsive", "yellow", "default", "default", "#b92", "default"),
("list_focus_unresponsive", "black", "light gray", "standout", "#530", "#aaa"),
("topic_list_normal", "light gray", "default", "default", "#ddd", "default"), ("topic_list_normal", "light gray", "default", "default", "#ddd", "default"),
("browser_controls", "light gray", "default", "default", "#bbb", "default"), ("browser_controls", "light gray", "default", "default", "#bbb", "default"),
("progress_full", "black", "light gray", "standout", "#111", "#bbb"), ("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
@@ -78,6 +80,8 @@ THEMES = {
("list_normal", "dark gray", "default", "default", "#444", "default"), ("list_normal", "dark gray", "default", "default", "#444", "default"),
("list_untrusted", "dark red", "default", "default", "#a22", "default"), ("list_untrusted", "dark red", "default", "default", "#a22", "default"),
("list_focus_untrusted", "black", "dark gray", "standout", "#810", "#aaa"), ("list_focus_untrusted", "black", "dark gray", "standout", "#810", "#aaa"),
("list_unresponsive", "yellow", "default", "default", "#b92", "default"),
("list_focus_unresponsive", "black", "light gray", "standout", "#530", "#aaa"),
("topic_list_normal", "dark gray", "default", "default", "#222", "default"), ("topic_list_normal", "dark gray", "default", "default", "#222", "default"),
("browser_controls", "dark gray", "default", "default", "#444", "default"), ("browser_controls", "dark gray", "default", "default", "#444", "default"),
("progress_full", "black", "dark gray", "standout", "#111", "#bbb"), ("progress_full", "black", "dark gray", "standout", "#111", "#bbb"),

View File

@@ -23,6 +23,8 @@ class BrowserFrame(urwid.Frame):
self.delegate.url_dialog() self.delegate.url_dialog()
elif key == "ctrl s": elif key == "ctrl s":
self.delegate.save_node_dialog() self.delegate.save_node_dialog()
elif key == "ctrl b":
self.delegate.save_node_dialog()
elif key == "ctrl g": elif key == "ctrl g":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen() nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen()
elif self.get_focus() == "body": elif self.get_focus() == "body":
@@ -719,6 +721,10 @@ class Browser:
env_map = self.request_data env_map = self.request_data
else: else:
env_map = {} env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
generated = subprocess.run([page_path], stdout=subprocess.PIPE, env=env_map) generated = subprocess.run([page_path], stdout=subprocess.PIPE, env=env_map)
page_data = generated.stdout page_data = generated.stdout
else: else:
@@ -750,7 +756,7 @@ class Browser:
def __load(self): def __load(self):
# If an established link exists, but it doesn't match the target # If an established link exists, but it doesn't match the target
# destination, we close and clear it. # destination, we close and clear it.
if self.link != None and self.link.destination.hash != self.destination_hash: if self.link != None and (self.link.destination.hash != self.destination_hash or self.link.status != RNS.Link.ACTIVE):
self.link.teardown() self.link.teardown()
self.link = None self.link = None
@@ -999,6 +1005,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
else: else:
self.status = Browser.REQUEST_FAILED self.status = Browser.REQUEST_FAILED
self.response_progress = 0 self.response_progress = 0
@@ -1006,6 +1017,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
def request_timeout(self, request_receipt=None): def request_timeout(self, request_receipt=None):
@@ -1015,6 +1031,11 @@ class Browser:
self.response_transfer_size = None self.response_transfer_size = None
self.update_display() self.update_display()
if self.link != None:
try:
self.link.teardown()
except Exception as e:
pass
def response_progressed(self, request_receipt): def response_progressed(self, request_receipt):
@@ -1108,4 +1129,4 @@ class UrlEdit(urwid.Edit):
if key == "enter": if key == "enter":
self.confirmed(self) self.confirmed(self)
else: else:
return super(UrlEdit, self).keypress(size, key) return super(UrlEdit, self).keypress(size, key)

View File

@@ -335,7 +335,7 @@ By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you bu
Sometimes, you don't want everyone to be able to view certain pages or execute certain scripts. In such cases, you can use `*authentication`* to control who gets to run certain requests. Sometimes, you don't want everyone to be able to view certain pages or execute certain scripts. In such cases, you can use `*authentication`* to control who gets to run certain requests.
To enable authentication for any page, simply add a new file to your pages directory with ".allowed" added to the file-name of the page. If your page is named "secret_page.mu", just add a file named "secret_page.mu.allowed". To enable authentication for any page, simply add a new file to your pages directory with ".allowed" added to the file-name of the page. If your page is named "secret_page.mu", just add a file named "secret_page.allowed".
For each user allowed to access the page, add a line to this file, containing the hash of that users primary identity. Users can find their own identity hash in the `![ Network ]`! part of the program, under `!Local Peer Info`!. If you want to allow access for three different users, your file would look like this: For each user allowed to access the page, add a line to this file, containing the hash of that users primary identity. Users can find their own identity hash in the `![ Network ]`! part of the program, under `!Local Peer Info`!. If you want to allow access for three different users, your file would look like this:
@@ -506,6 +506,12 @@ The number of minutes between each automatic sync. The default is equal to 6 hou
On low-bandwidth networks, it can be useful to limit the amount of messages downloaded in each sync. The default is 8. Set to 0 to download all available messages every time a sync occurs. On low-bandwidth networks, it can be useful to limit the amount of messages downloaded in each sync. The default is 8. Set to 0 to download all available messages every time a sync occurs.
< <
>>>
`!compact_announce_stream = yes`!
>>>>
With this option enabled, Nomad Network will only display one entry in the announce stream per destination. Older announces are culled when a new one arrives.
<
>> Text UI Section >> Text UI Section
This section hold configuration directives related to the look and feel of the text-based user interface of the program. It is delimited by the `![textui]`! header in the configuration file. Available directives, along with their default values, are as follows: This section hold configuration directives related to the look and feel of the text-based user interface of the program. It is delimited by the `![textui]`! header in the configuration file. Available directives, along with their default values, are as follows:

View File

@@ -751,41 +751,45 @@ class LinkableText(urwid.Text):
return x, y return x, y
def mouse_event(self, size, event, button, x, y, focus): def mouse_event(self, size, event, button, x, y, focus):
if button != 1 or not is_mouse_press(event): try:
return False if button != 1 or not is_mouse_press(event):
else: return False
(maxcol,) = size
translation = self.get_line_translation(maxcol)
line_offset = 0
if self.align == "center":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
if x > translation[y][1][0]+translation[y][0][0]:
x = translation[y][1][0]+translation[y][0][0]
elif self.align == "right":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
else: else:
line_offset = translation[y][0][1] (maxcol,) = size
if x > translation[y][0][0]: translation = self.get_line_translation(maxcol)
x = translation[y][0][0] line_offset = 0
pos = line_offset+x if self.align == "center":
line_offset = translation[y][1][1]-translation[y][0][0]
if x < translation[y][0][0]:
x = translation[y][0][0]
self._cursor_position = pos if x > translation[y][1][0]+translation[y][0][0]:
item = self.find_item_at_pos(self._cursor_position) x = translation[y][1][0]+translation[y][0][0]
if item != None: elif self.align == "right":
if isinstance(item, LinkSpec): line_offset = translation[y][1][1]-translation[y][0][0]
self.handle_link(item.link_target, item.link_fields) if x < translation[y][0][0]:
x = translation[y][0][0]
self._invalidate() else:
self._emit("change") line_offset = translation[y][0][1]
if x > translation[y][0][0]:
x = translation[y][0][0]
pos = line_offset+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, item.link_fields)
self._invalidate()
self._emit("change")
return True
return True except Exception as e:
return False

View File

@@ -13,9 +13,7 @@ class NetworkDisplayShortcuts():
self.app = app self.app = app
g = app.ui.glyphs g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Text("[C-l] Nodes/Announces [C-x] Remove [C-w] Disconnect [C-d] Back [C-f] Forward [C-r] Reload [C-u] URL [C-g] Fullscreen"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-l] Nodes/Announces [C-x] Remove [C-w] Disconnect [C-d] Back [C-f] Forward [C-r] Reload [C-u] URL [C-g] Fullscreen [C-s / C-b] Save Node"), "shortcutbar")
# "[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"
class DialogLineBox(urwid.LineBox): class DialogLineBox(urwid.LineBox):
def keypress(self, size, key): def keypress(self, size, key):
@@ -588,7 +586,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
def ident_change(sender, userdata): def ident_change(sender, userdata):
pass pass
propagation_node_checkbox = urwid.CheckBox("Use as default propagation node", state=(self.app.get_user_selected_propagation_node() == source_hash), on_state_change=pn_change) propagation_node_checkbox = urwid.CheckBox("Use as default propagation node", state=(self.app.get_user_selected_propagation_node() == pn_hash), on_state_change=pn_change)
connect_identify_checkbox = urwid.CheckBox("Identify when connecting", state=self.app.directory.should_identify_on_connect(source_hash), on_state_change=ident_change) connect_identify_checkbox = urwid.CheckBox("Identify when connecting", state=self.app.directory.should_identify_on_connect(source_hash), on_state_change=ident_change)
def save_node(sender): def save_node(sender):
@@ -1578,7 +1576,11 @@ class LXMFPeers(urwid.WidgetWrap):
self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no LXMF nodes are peered\n\n"), align="center")]) self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no LXMF nodes are peered\n\n"), align="center")])
self.display_widget = urwid.Filler(self.pile, valign="top", height="pack") self.display_widget = urwid.Filler(self.pile, valign="top", height="pack")
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="LXMF Propagation Peers"), widget_style)) if hasattr(self, "peer_list") and self.peer_list:
pl = len(self.peer_list)
else:
pl = 0
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title=f"LXMF Propagation Peers ({pl})"), widget_style))
def keypress(self, size, key): def keypress(self, size, key):
if key == "up" and (self.no_content or self.ilb.first_item_is_selected()): if key == "up" and (self.no_content or self.ilb.first_item_is_selected()):
@@ -1613,13 +1615,13 @@ class LXMFPeers(urwid.WidgetWrap):
def make_peer_widgets(self): def make_peer_widgets(self):
widget_list = [] widget_list = []
for peer_id in self.peer_list: sorted_peers = sorted(self.peer_list, key=lambda pid: self.peer_list[pid].link_establishment_rate, reverse=True)
for peer_id in sorted_peers:
peer = self.peer_list[peer_id] peer = self.peer_list[peer_id]
pe = LXMFPeerEntry(self.app, peer, self) pe = LXMFPeerEntry(self.app, peer, self)
pe.destination_hash = peer.destination_hash pe.destination_hash = peer.destination_hash
widget_list.append(pe) widget_list.append(pe)
# TODO: Sort list
return widget_list return widget_list
class LXMFPeerEntry(urwid.WidgetWrap): class LXMFPeerEntry(urwid.WidgetWrap):
@@ -1635,7 +1637,7 @@ class LXMFPeerEntry(urwid.WidgetWrap):
node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", node_identity) node_hash = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", node_identity)
display_name = self.app.directory.alleged_display_str(node_hash) display_name = self.app.directory.alleged_display_str(node_hash)
if display_name != None: if display_name != None:
display_str += " "+str(display_name) display_str = str(display_name)+"\n "+display_str
sym = g["sent"] sym = g["sent"]
style = "list_unknown" style = "list_unknown"
@@ -1645,8 +1647,12 @@ class LXMFPeerEntry(urwid.WidgetWrap):
if hasattr(peer, "alive"): if hasattr(peer, "alive"):
if peer.alive: if peer.alive:
alive_string = "Available" alive_string = "Available"
style = "list_normal"
focus_style = "list_focus"
else: else:
alive_string = "Unresponsive" alive_string = "Unresponsive"
style = "list_unresponsive"
focus_style = "list_focus_unresponsive"
widget = ListEntry(sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs, "+RNS.prettysize(peer.link_establishment_rate/8, "b")+"/s LER") widget = ListEntry(sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))+"\n "+str(len(peer.unhandled_messages))+" unhandled LXMs, "+RNS.prettysize(peer.link_establishment_rate/8, "b")+"/s LER")
# urwid.connect_signal(widget, "click", delegate.connect_node, node) # urwid.connect_signal(widget, "click", delegate.connect_node, node)

View File

@@ -30,6 +30,6 @@ setuptools.setup(
entry_points= { entry_points= {
'console_scripts': ['nomadnet=nomadnet.nomadnet:main'] 'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
}, },
install_requires=["rns>=0.4.9", "lxmf>=0.3.1", "urwid>=2.1.2", "qrcode"], install_requires=["rns>=0.6.2", "lxmf>=0.3.4", "urwid==2.1.2", "qrcode"],
python_requires=">=3.6", python_requires=">=3.6",
) )