Compare commits

...

46 Commits
0.3.3 ... 0.4.0

Author SHA1 Message Date
Mark Qvist
5a7c84d2a4 Updated LXMF version 2023-10-16 01:50:56 +02:00
Mark Qvist
a3bf538afe Added propagation node list sorting and manual delivery sync initiation 2023-10-16 01:46:50 +02:00
Mark Qvist
8d72a83411 Added option to disable propagation node for pageserving-only nodes 2023-10-16 00:51:23 +02:00
Mark Qvist
0fb9e180ba Added node list sorting 2023-10-16 00:16:04 +02:00
Mark Qvist
2322a254a8 LXM URI file output 2023-10-15 22:53:56 +02:00
Mark Qvist
37ad5cc5d3 Updated dependencies 2023-10-15 21:01:39 +02:00
Mark Qvist
a921adbdbe Cleanup 2023-10-15 21:00:47 +02:00
Mark Qvist
5c1a2c3485 Updated version 2023-10-15 21:00:42 +02:00
Mark Qvist
0cd1c9cf37 Added paper message output options 2023-10-15 20:35:21 +02:00
Mark Qvist
cfc0f4f9c1 Updated readme 2023-10-15 20:35:08 +02:00
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
Mark Qvist
2fe2f6bb49 Updated dependencies 2023-02-17 22:31:21 +01:00
Mark Qvist
10d6d737b9 Added LER metric to propagation node list 2023-02-17 19:37:10 +01:00
Mark Qvist
1b561a88ba Updated version 2023-02-15 12:28:16 +01:00
Mark Qvist
1f76897855 Added fields and request links to guide 2023-02-15 12:26:37 +01:00
Mark Qvist
e266719e2c Keyboard navigation for fields 2023-02-15 11:53:34 +01:00
Mark Qvist
1a84e0c019 Added link variables 2023-02-15 10:09:25 +01:00
Mark Qvist
c134ee180a Added input fields example 2023-02-14 19:46:39 +01:00
Mark Qvist
d30f405fd5 Added input field data handling 2023-02-14 19:46:21 +01:00
Mark Qvist
1378a3f6dd Field width and mask control 2023-02-14 16:07:30 +01:00
Mark Qvist
eb6dd1729b Input field rendering 2023-02-14 15:26:25 +01:00
Mark Qvist
4f483e482a Merge branch 'master' of https://git.unsigned.io/markqvist/NomadNet 2023-02-09 21:18:37 +01:00
Mark Qvist
1d3538a604 Field parsing 2023-02-09 21:18:14 +01:00
markqvist
307c522074 Merge pull request #23 from Erethon/fix-crash
Fix bug where Nomadnet crashes on empty lists
2023-02-09 13:40:22 +01:00
Dionysis Grigoropoulos
4ec1a4979f Fix bug where Nomadnet crashes on empty lists 2023-02-09 13:45:37 +02:00
Mark Qvist
2d6cd8e33f Catch unhandled Ctrl-E before passing to terminal 2023-02-09 12:40:35 +01:00
14 changed files with 815 additions and 132 deletions

View File

@@ -4,7 +4,7 @@ Off-grid, resilient mesh communication with strong encryption, forward secrecy a
![Screenshot](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/1.png) ![Screenshot](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/1.png)
Nomad Network Allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them. No signups, no agreements, no handover of any data, no permissions and gatekeepers. Nomad Network allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them. No signups, no agreements, no handover of any data, no permissions and gatekeepers.
Nomad Network is build on [LXMF](https://github.com/markqvist/LXMF) and [Reticulum](https://github.com/markqvist/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 fiber optics. Nomad Network is build on [LXMF](https://github.com/markqvist/LXMF) and [Reticulum](https://github.com/markqvist/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 fiber optics.
@@ -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

@@ -227,7 +227,7 @@ class Conversation:
RNS.log("Destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE) RNS.log("Destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE)
return False return False
def paper_output(self, content="", title=""): def paper_output(self, content="", title="", mode="print_qr"):
if self.send_destination: if self.send_destination:
try: try:
dest = self.send_destination dest = self.send_destination
@@ -235,6 +235,8 @@ class Conversation:
desired_method = LXMF.LXMessage.PAPER desired_method = LXMF.LXMessage.PAPER
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method) lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method)
if mode == "print_qr":
qr_code = lxm.as_qr() qr_code = lxm.as_qr()
qr_tmp_path = self.app.tmpfilespath+"/"+str(RNS.hexrep(lxm.hash, delimit=False)) qr_tmp_path = self.app.tmpfilespath+"/"+str(RNS.hexrep(lxm.hash, delimit=False))
qr_code.save(qr_tmp_path) qr_code.save(qr_tmp_path)
@@ -248,6 +250,27 @@ class Conversation:
return print_result return print_result
elif mode == "save_qr":
qr_code = lxm.as_qr()
qr_save_path = self.app.downloads_path+"/LXM_"+str(RNS.hexrep(lxm.hash, delimit=False)+".png")
qr_code.save(qr_save_path)
message_path = Conversation.ingest(lxm, self.app, originator=True)
self.messages.append(ConversationMessage(message_path))
return qr_save_path
elif mode == "save_uri":
lxm_uri = lxm.as_uri()+"\n"
uri_save_path = self.app.downloads_path+"/LXM_"+str(RNS.hexrep(lxm.hash, delimit=False)+".txt")
with open(uri_save_path, "wb") as f:
f.write(lxm_uri.encode("utf-8"))
message_path = Conversation.ingest(lxm, self.app, originator=True)
self.messages.append(ConversationMessage(message_path))
return uri_save_path
elif mode == "return_uri":
return lxm.as_uri()
except Exception as e: except Exception as e:
RNS.log("An error occurred while generating paper message, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("An error occurred while generating paper message, the contained exception was: "+str(e), RNS.LOG_ERROR)
return False return False

View File

@@ -34,6 +34,7 @@ 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):
try:
app = nomadnet.NomadNetworkApp.get_shared_instance() app = nomadnet.NomadNetworkApp.get_shared_instance()
if not destination_hash in app.ignored_list: if not destination_hash in app.ignored_list:
@@ -45,6 +46,10 @@ class Directory:
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):
self.directory_entries = {} self.directory_entries = {}
@@ -61,7 +66,7 @@ class Directory:
packed_list = [] packed_list = []
for source_hash in self.directory_entries: for source_hash in self.directory_entries:
e = self.directory_entries[source_hash] e = self.directory_entries[source_hash]
packed_list.append((e.source_hash, e.display_name, e.trust_level, e.hosts_node, e.preferred_delivery, e.identify)) packed_list.append((e.source_hash, e.display_name, e.trust_level, e.hosts_node, e.preferred_delivery, e.identify, e.sort_rank))
directory = { directory = {
"entry_list": packed_list, "entry_list": packed_list,
@@ -101,18 +106,35 @@ class Directory:
else: else:
identify = False identify = False
entries[e[0]] = DirectoryEntry(e[0], e[1], e[2], hosts_node, preferred_delivery=preferred_delivery, identify_on_connect=identify) if len(e) > 6:
sort_rank = e[6]
else:
sort_rank = None
entries[e[0]] = DirectoryEntry(e[0], e[1], e[2], hosts_node, preferred_delivery=preferred_delivery, identify_on_connect=identify, sort_rank=sort_rank)
self.directory_entries = entries self.directory_entries = entries
self.announce_stream = unpacked_directory["announce_stream"] self.announce_stream = unpacked_directory["announce_stream"]
except Exception as e: except Exception as e:
RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) 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): 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 +145,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 +185,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:
@@ -176,6 +224,13 @@ class Directory:
def simplest_display_str(self, source_hash): def simplest_display_str(self, source_hash):
trust_level = self.trust_level(source_hash) trust_level = self.trust_level(source_hash)
if trust_level == DirectoryEntry.WARNING or trust_level == DirectoryEntry.UNTRUSTED: if trust_level == DirectoryEntry.WARNING or trust_level == DirectoryEntry.UNTRUSTED:
if source_hash in self.directory_entries:
dn = self.directory_entries[source_hash].display_name
if dn == None:
return RNS.prettyhexrep(source_hash)
else:
return dn+" <"+RNS.hexrep(source_hash, delimit=False)+">"
else:
return "<"+RNS.hexrep(source_hash, delimit=False)+">" return "<"+RNS.hexrep(source_hash, delimit=False)+">"
else: else:
if source_hash in self.directory_entries: if source_hash in self.directory_entries:
@@ -209,6 +264,18 @@ class Directory:
else: else:
return DirectoryEntry.UNKNOWN return DirectoryEntry.UNKNOWN
def pn_trust_level(self, source_hash):
recalled_identity = RNS.Identity.recall(source_hash)
if recalled_identity != None:
associated_node = RNS.Destination.hash_from_name_and_identity("nomadnetwork.node", recalled_identity)
return self.trust_level(associated_node)
def sort_rank(self, source_hash):
if source_hash in self.directory_entries:
return self.directory_entries[source_hash].sort_rank
else:
return None
def preferred_delivery(self, source_hash): def preferred_delivery(self, source_hash):
if source_hash in self.directory_entries: if source_hash in self.directory_entries:
return self.directory_entries[source_hash].preferred_delivery return self.directory_entries[source_hash].preferred_delivery
@@ -268,6 +335,7 @@ class Directory:
if e.hosts_node: if e.hosts_node:
node_list.append(e) node_list.append(e)
node_list.sort(key = lambda e: (e.sort_rank if e.sort_rank != None else 2^32, DirectoryEntry.TRUSTED-e.trust_level, e.display_name))
return node_list return node_list
def number_of_known_nodes(self): def number_of_known_nodes(self):
@@ -292,10 +360,11 @@ class DirectoryEntry:
DIRECT = 0x01 DIRECT = 0x01
PROPAGATED = 0x02 PROPAGATED = 0x02
def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None, identify_on_connect=False): def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None, identify_on_connect=False, sort_rank=None):
if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8: if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
self.source_hash = source_hash self.source_hash = source_hash
self.display_name = display_name self.display_name = display_name
self.sort_rank = sort_rank
if preferred_delivery == None: if preferred_delivery == None:
self.preferred_delivery = DirectoryEntry.DIRECT self.preferred_delivery = DirectoryEntry.DIRECT

View File

@@ -97,7 +97,7 @@ class Node:
for directory in directories: for directory in directories:
self.scan_files(base_path+"/"+directory) self.scan_files(base_path+"/"+directory)
def serve_page(self, path, data, request_id, remote_identity, requested_at): def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
RNS.log("Page request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE) RNS.log("Page request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE)
try: try:
self.app.peer_settings["served_page_requests"] += 1 self.app.peer_settings["served_page_requests"] += 1
@@ -152,7 +152,20 @@ class Node:
if request_allowed: if request_allowed:
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):
generated = subprocess.run([file_path], stdout=subprocess.PIPE) env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
if link_id != None:
env_map["link_id"] = RNS.hexrep(link_id, delimit=False)
if remote_identity != None:
env_map["remote_identity"] = RNS.hexrep(remote_identity.hash, delimit=False)
if data != None and isinstance(data, dict):
for e in data:
if isinstance(e, str) and (e.startswith("field_") or e.startswith("var_")):
env_map[e] = data[e]
generated = subprocess.run([file_path], stdout=subprocess.PIPE, env=env_map)
return generated.stdout return generated.stdout
else: else:
fh = open(file_path, "rb") fh = open(file_path, "rb")

View File

@@ -122,10 +122,12 @@ class NomadNetworkApp:
self.peer_announce_at_start = True self.peer_announce_at_start = True
self.try_propagation_on_fail = True self.try_propagation_on_fail = True
self.disable_propagation = False
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)
@@ -307,15 +309,27 @@ class NomadNetworkApp:
except Exception as e: except Exception as e:
RNS.log("Cannot prioritise "+str(dest_str)+", it is not a valid destination hash", RNS.LOG_ERROR) RNS.log("Cannot prioritise "+str(dest_str)+", it is not a valid destination hash", RNS.LOG_ERROR)
if self.disable_propagation:
if os.path.isfile(self.pnannouncedpath):
try:
RNS.log("Sending indication to peered LXMF Propagation Node that this node is no longer participating", RNS.LOG_DEBUG)
self.message_router.disable_propagation()
os.unlink(self.pnannouncedpath)
except Exception as e:
RNS.log("An error ocurred while indicating that this LXMF Propagation Node is no longer participating. The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
self.message_router.enable_propagation() self.message_router.enable_propagation()
try: try:
with open(self.pnannouncedpath, "wb") as pnf: with open(self.pnannouncedpath, "wb") as pnf:
pnf.write(msgpack.packb(time.time())) pnf.write(msgpack.packb(time.time()))
pnf.close() pnf.close()
except Exception as e: except Exception as e:
RNS.log("An error ocurred while writing Propagation Node announce timestamp. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("An error ocurred while writing Propagation Node announce timestamp. The contained exception was: "+str(e), RNS.LOG_ERROR)
if not self.disable_propagation:
RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash)) RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash))
self.node = nomadnet.Node(self) self.node = nomadnet.Node(self)
else: else:
self.node = None self.node = None
@@ -698,6 +712,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":
@@ -783,6 +801,11 @@ class NomadNetworkApp:
else: else:
self.node_name = self.config["node"]["node_name"] self.node_name = self.config["node"]["node_name"]
if not "disable_propagation" in self.config["node"]:
self.disable_propagation = False
else:
self.disable_propagation = self.config["node"].as_bool("disable_propagation")
if not "announce_at_start" in self.config["node"]: if not "announce_at_start" in self.config["node"]:
self.node_announce_at_start = False self.node_announce_at_start = False
else: else:
@@ -929,6 +952,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
@@ -992,6 +1021,14 @@ announce_interval = 360
# Whether to announce when the node starts. # Whether to announce when the node starts.
announce_at_start = Yes announce_at_start = Yes
# By default, when Nomad Network is hosting a
# node, it will also act as an LXMF propagation
# node. If there is already a large amount of
# propagation nodes on the network, or you
# simply want to run a pageserving-only node,
# you can disable running a propagation node.
# disable_propagation = False
# The maximum amount of storage to use for # The maximum amount of storage to use for
# the LXMF Propagation Node message store, # the LXMF Propagation Node message store,
# specified in megabytes. When this limit # specified in megabytes. When this limit

View File

@@ -1 +1 @@
__version__ = "0.3.3" __version__ = "0.4.0"

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import os
env_string = ""
for e in os.environ:
env_string += "{}={}\n".format(e, os.environ[e])
template = """>Fields and Submitting Data
Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables. This page contains a few examples.
>> Read Environment Variables
{@ENV}
>>Examples of Fields and Submissions
The following section contains a simple set of fields, and a few different links that submit the field data in different ways.
-=
An input field : `B444`<username`Entered data>`b
An masked field : `B444`<!|password`Value of Field>`b
An small field : `B444`<8|small`test>`b, and some more text.
Two fields : `B444`<8|one`One>`b `B444`<8|two`Two>`b
The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!.
You can `!`[submit`:/page/input_fields.mu`one|password|small]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`!
Or simply `!`[submit them all`:/page/input_fields.mu`*]`!.
Submission links can also `!`[include pre-configured variables`:/page/input_fields.mu`username|two|entitiy_id=4611|action=view]`!.
Or take all fields and `!`[pre-configured variables`:/page/input_fields.mu`*|entitiy_id=4611|action=view]`!.
Or only `!`[pre-configured variables`:/page/input_fields.mu`entitiy_id=4688|task=something]`!
-=
"""
print(template.replace("{@ENV}", env_string))

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"),
@@ -217,6 +221,8 @@ class TextUI:
def unhandled_input(self, key): def unhandled_input(self, key):
if key == "ctrl q": if key == "ctrl q":
raise urwid.ExitMainLoop raise urwid.ExitMainLoop
elif key == "ctrl e":
pass
def display_main(self, loop, user_data): def display_main(self, loop, user_data):
self.loop.widget = self.main_display.widget self.loop.widget = self.main_display.widget

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":
@@ -81,6 +83,7 @@ class Browser:
self.aspects = aspects self.aspects = aspects
self.destination_hash = destination_hash self.destination_hash = destination_hash
self.path = path self.path = path
self.request_data = None
self.timeout = Browser.DEFAULT_TIMEOUT self.timeout = Browser.DEFAULT_TIMEOUT
self.last_keypress = None self.last_keypress = None
@@ -161,7 +164,42 @@ class Browser:
else: else:
return destination_type return destination_type
def handle_link(self, link_target): def handle_link(self, link_target, link_data = None):
request_data = None
if link_data != None:
link_fields = []
request_data = {}
all_fields = True if "*" in link_data else False
for e in link_data:
if "=" in e:
c = e.split("=")
if len(c) == 2:
request_data["var_"+str(c[0])] = str(c[1])
else:
link_fields.append(e)
def recurse_down(w):
target = None
if isinstance(w, list):
for t in w:
recurse_down(t)
elif isinstance(w, tuple):
for t in w:
recurse_down(t)
elif hasattr(w, "contents"):
recurse_down(w.contents)
elif hasattr(w, "original_widget"):
recurse_down(w.original_widget)
elif hasattr(w, "_original_widget"):
recurse_down(w._original_widget)
else:
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
request_data["field_"+w.field_name] = w.get_edit_text()
recurse_down(self.attr_maps)
RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG)
components = link_target.split("@") components = link_target.split("@")
destination_type = None destination_type = None
@@ -177,7 +215,7 @@ class Browser:
RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG) RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG)
self.browser_footer = urwid.Text("Opening link to: "+str(link_target)) self.browser_footer = urwid.Text("Opening link to: "+str(link_target))
try: try:
self.retrieve_url(link_target) self.retrieve_url(link_target, request_data)
except Exception as e: except Exception as e:
self.browser_footer = urwid.Text("Could not open link: "+str(e)) self.browser_footer = urwid.Text("Could not open link: "+str(e))
self.frame.contents["footer"] = (self.browser_footer, self.frame.options()) self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
@@ -351,6 +389,7 @@ class Browser:
if self.link != None: if self.link != None:
self.link.teardown() self.link.teardown()
self.request_data = None
self.attr_maps = [] self.attr_maps = []
self.status = Browser.DISCONECTED self.status = Browser.DISCONECTED
self.response_progress = 0 self.response_progress = 0
@@ -365,7 +404,7 @@ class Browser:
self.update_display() self.update_display()
def retrieve_url(self, url): def retrieve_url(self, url, request_data = None):
self.previous_destination_hash = self.destination_hash self.previous_destination_hash = self.destination_hash
self.previous_path = self.path self.previous_path = self.path
@@ -418,6 +457,7 @@ class Browser:
else: else:
self.set_destination_hash(destination_hash) self.set_destination_hash(destination_hash)
self.set_path(path) self.set_path(path)
self.set_request_data(request_data)
self.load_page() self.load_page()
def set_destination_hash(self, destination_hash): def set_destination_hash(self, destination_hash):
@@ -431,6 +471,8 @@ class Browser:
def set_path(self, path): def set_path(self, path):
self.path = path self.path = path
def set_request_data(self, request_data):
self.request_data = request_data
def set_timeout(self, timeout): def set_timeout(self, timeout):
self.timeout = timeout self.timeout = timeout
@@ -637,7 +679,11 @@ class Browser:
def load_page(self): def load_page(self):
if self.request_data == None:
cached = self.get_cached(self.current_url()) cached = self.get_cached(self.current_url())
else:
cached = None
if cached: if cached:
self.status = Browser.DONE self.status = Browser.DONE
self.page_data = cached self.page_data = cached
@@ -671,7 +717,15 @@ class Browser:
page_data = b"The requested local page did not exist in the file system" page_data = b"The requested local page did not exist in the file system"
if os.path.isfile(page_path): if os.path.isfile(page_path):
if os.access(page_path, os.X_OK): if os.access(page_path, os.X_OK):
generated = subprocess.run([page_path], stdout=subprocess.PIPE) if self.request_data != None:
env_map = self.request_data
else:
env_map = {}
if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"]
generated = subprocess.run([page_path], stdout=subprocess.PIPE, env=env_map)
page_data = generated.stdout page_data = generated.stdout
else: else:
file = open(page_path, "rb") file = open(page_path, "rb")
@@ -702,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
@@ -758,7 +812,7 @@ class Browser:
self.update_display() self.update_display()
receipt = self.link.request( receipt = self.link.request(
self.path, self.path,
data = None, data = self.request_data,
response_callback = self.response_received, response_callback = self.response_received,
failed_callback = self.request_failed, failed_callback = self.request_failed,
progress_callback = self.response_progressed progress_callback = self.response_progressed
@@ -951,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
@@ -958,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):
@@ -967,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):

View File

@@ -110,7 +110,10 @@ class ConversationsDisplay():
def delete_selected_conversation(self): def delete_selected_conversation(self):
self.dialog_open = True self.dialog_open = True
source_hash = self.ilb.get_selected_item().source_hash item = self.ilb.get_selected_item()
if item == None:
return
source_hash = item.source_hash
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.update_conversation_list() self.update_conversation_list()
@@ -140,7 +143,10 @@ class ConversationsDisplay():
def edit_selected_in_directory(self): def edit_selected_in_directory(self):
g = self.app.ui.glyphs g = self.app.ui.glyphs
self.dialog_open = True self.dialog_open = True
source_hash_text = self.ilb.get_selected_item().source_hash item = self.ilb.get_selected_item()
if item == None:
return
source_hash_text = item.source_hash
display_name = self.ilb.get_selected_item().display_name display_name = self.ilb.get_selected_item().display_name
if display_name == None: if display_name == None:
display_name = "" display_name = ""
@@ -968,7 +974,27 @@ class ConversationWidget(urwid.WidgetWrap):
else: else:
pass pass
def paper_message(self): def paper_message_saved(self, path):
g = self.app.ui.glyphs
def dismiss_dialog(sender):
self.dialog_open = False
self.conversation_changed(None)
dialog = DialogLineBox(
urwid.Pile([
urwid.Text("The paper message was saved to:\n\n"+str(path)+"\n", align="center"),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))])
]), title=g["papermsg"].replace(" ", "")
)
dialog.delegate = self
bottom = self.messagelist
overlay = urwid.Overlay(dialog, bottom, align="center", width=60, valign="middle", height="pack", left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.set_focus("body")
def print_paper_message_qr(self):
content = self.content_editor.get_edit_text() content = self.content_editor.get_edit_text()
title = self.title_editor.get_edit_text() title = self.title_editor.get_edit_text()
if not content == "": if not content == "":
@@ -977,6 +1003,67 @@ class ConversationWidget(urwid.WidgetWrap):
else: else:
self.paper_message_failed() self.paper_message_failed()
def save_paper_message_qr(self):
content = self.content_editor.get_edit_text()
title = self.title_editor.get_edit_text()
if not content == "":
output_result = self.conversation.paper_output(content, title, mode="save_qr")
if output_result != False:
self.clear_editor()
self.paper_message_saved(output_result)
else:
self.paper_message_failed()
def save_paper_message_uri(self):
content = self.content_editor.get_edit_text()
title = self.title_editor.get_edit_text()
if not content == "":
output_result = self.conversation.paper_output(content, title, mode="save_uri")
if output_result != False:
self.clear_editor()
self.paper_message_saved(output_result)
else:
self.paper_message_failed()
def paper_message(self):
def dismiss_dialog(sender):
self.dialog_open = False
self.conversation_changed(None)
def print_qr(sender):
dismiss_dialog(self)
self.print_paper_message_qr()
def save_qr(sender):
dismiss_dialog(self)
self.save_paper_message_qr()
def save_uri(sender):
dismiss_dialog(self)
self.save_paper_message_uri()
dialog = DialogLineBox(
urwid.Pile([
urwid.Text("Select the desired paper message output method.\nSaved files will be written to:\n\n"+str(self.app.downloads_path)+"\n", align="center"),
urwid.Columns([
("weight", 0.5, urwid.Button("Print QR", on_press=print_qr)),
("weight", 0.1, urwid.Text("")),
("weight", 0.5, urwid.Button("Save QR", on_press=save_qr)),
("weight", 0.1, urwid.Text("")),
("weight", 0.5, urwid.Button("Save URI", on_press=save_uri)),
("weight", 0.1, urwid.Text("")),
("weight", 0.5, urwid.Button("Cancel", on_press=dismiss_dialog))
])
]), title="Create Paper Message"
)
dialog.delegate = self
bottom = self.messagelist
overlay = urwid.Overlay(dialog, bottom, align="center", width=60, valign="middle", height="pack", left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.set_focus("body")
def paper_message_failed(self): def paper_message_failed(self):
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.dialog_open = False self.dialog_open = False

View File

@@ -217,6 +217,15 @@ The different sections of the program has a number of keyboard shortcuts mapped,
- Ctrl-W Close conversation - Ctrl-W Close conversation
>>`!Network Window`! >>`!Network Window`!
>>>Browser
- Ctrl-D Back
- Ctrl-F Forward
- Ctrl-R Reload page
- Ctrl-U Open URL entry dialog
- Ctrl-S Save connected node
- Ctrl-G Toggle fullscreen browser window
- Ctrl-W Disconnect from node
>>>Announce Stream >>>Announce Stream
- Ctrl-L Switch to Known Nodes list - Ctrl-L Switch to Known Nodes list
- Ctrl-X Delete selected announce - Ctrl-X Delete selected announce
@@ -227,14 +236,10 @@ The different sections of the program has a number of keyboard shortcuts mapped,
- Ctrl-X Delete selected node entry - Ctrl-X Delete selected node entry
- Ctrl-P Display peered LXMF Propagation Nodes - Ctrl-P Display peered LXMF Propagation Nodes
>>>Browser >>>Peered LXMF Propagation Nodes
- Ctrl-D Back - Ctrl-L Switch to Announce Stream or Known Nodes
- Ctrl-F Forward - Ctrl-X Break peering with selected node entry
- Ctrl-R Reload page - Ctrl-R Request immediate delivery sync of unhandled LXMs
- Ctrl-U Open URL entry dialog
- Ctrl-S Save connected node
- Ctrl-G Toggle fullscreen browser window
- Ctrl-W Disconnect from node
''' '''
TOPIC_CONCEPTS = '''>Concepts and Terminology TOPIC_CONCEPTS = '''>Concepts and Terminology
@@ -302,11 +307,13 @@ By default, no content is defined, apart from a short placeholder home page. To
>>Distributed Message Store >>Distributed Message Store
All nodes on the network automatically form a distributed message store that allows users to exchange messages, even when they are not available at the same time. All nodes on the network will automatically participate in a distributed message store that allows users to exchange messages, even when they are not connected to the network at the same time.
When Nomad Network is configured to host a node, it also configures itself as an LXMF Propagation Node, and automatically discovers and peers with other propagation nodes on the network. This process is completely automatic and requires no configuration from the node operator. When Nomad Network is configured to host a node, by default it also configures itself as an LXMF Propagation Node, and automatically discovers and peers with other propagation nodes on the network. This process is completely automatic and requires no configuration from the node operator.
To view LXMF Propagation nodes that are currently peered with your node, go to the `![ Network ]`! part of the program and press `!Ctrl-P`!. If there is already an abundance of Propagation Nodes on the network, or the operator simply wishes to host a pageserving-only node, Propagation Node hosting can be disabled in the configuration file.
To view LXMF Propagation nodes that are currently peered with your node, go to the `![ Network ]`! part of the program and press `!Ctrl-P`!. In the list of peered Propagation Nodes, it is possible to break peering with a node by pressing `!Ctrl-X`!. It is also possible to request an immediate delivery sync of all unhandled messages for a node, by pressing `!Ctrl-R`!.
The distributed message store is resilient to intermittency, and will remain functional as long as at least one node remains on the network. Nodes that were offline for a time will automatically be synced up to date when they regain connectivity. The distributed message store is resilient to intermittency, and will remain functional as long as at least one node remains on the network. Nodes that were offline for a time will automatically be synced up to date when they regain connectivity.
@@ -322,9 +329,12 @@ You can control how long a peer will cache your pages by including the cache hea
You can use a preprocessor such as PHP, bash, Python (or whatever you prefer) to generate dynamic pages. To do so, just set executable permissions on the relevant page file, and be sure to include the interpreter at the beginning of the file, for example `!#!/usr/bin/python3`!. You can use a preprocessor such as PHP, bash, Python (or whatever you prefer) to generate dynamic pages. To do so, just set executable permissions on the relevant page file, and be sure to include the interpreter at the beginning of the file, for example `!#!/usr/bin/python3`!.
Data from fields and link variables will be passed to these scipts or programs as environment variables, and can simply be read by any method for acessing such.
In the `!examples`! directory, you can find various small examples for the use of this feature. The currently included examples are: In the `!examples`! directory, you can find various small examples for the use of this feature. The currently included examples are:
- A messageboard that receives messages over LXMF, contributed by trippcheng - A messageboard that receives messages over LXMF, contributed by trippcheng
- A simple demonstration on how to create fields and read entered data in node-side scripts
By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you build something neat, that you feel would fit here, you are more than welcome to contribute it. By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you build something neat, that you feel would fit here, you are more than welcome to contribute it.
@@ -332,7 +342,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:
@@ -503,6 +513,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:
@@ -611,6 +627,12 @@ Determines where the node server will look for hosted pages. Must be a readable
Determines where the node server will look for downloadable files. Must be a readable filesystem path. Determines where the node server will look for downloadable files. Must be a readable filesystem path.
< <
>>>
`!disable_propagation = no`!
>>>>
By default, when Nomad Network is hosting a node, it will also run an LXMF propagation node. If there is already a large amount of propagation nodes on the network, or you simply want to run a pageserving-only node, you can disable running a propagation node.
<
>>> >>>
`!message_storage_limit = 2000`! `!message_storage_limit = 2000`!
>>>> >>>>
@@ -1002,6 +1024,94 @@ Here is `F00f`_`[a more visible link`72914442a3689add83a09a767963f57c:/page/inde
When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL. When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL.
>Fields & Requests
Nomad Network let's you use simple input fields for submitting data to node-side applications. Submitted data, along with other session variables will be available to the node-side script / program as environment variables.
>>Request Links
Links can contain request variables and a list of fields to submit to the node-side application. You can include all fields on the page, only specific ones, and any number of request variables. To simply submit all fields on a page to a specified node-side page, create a link like this:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`*]
`=
``
Note the `!*`! following the extra `!\``! at the end of the path. This `!*`! denotes `*all fields`*. You can also specify a list of fields to include:
`Faaa
`=
`[Submit Fields`:/page/fields.mu`username|auth_token]
`=
``
If you want to include pre-set variables, you can do it like this:
`Faaa
`=
`[Query the System`:/page/fields.mu`username|auth_token|action=view|amount=64]
`=
``
>> Fields
Here's an example of creating a field. We'll create a field named `!user_input`! and fill it with the text `!Pre-defined data`!. Note that we are using background color tags to make the field more visible to the user:
`Faaa
`=
A simple input field: `B444`<user_input`Pre-defined data>`b
`=
``
You must always set a field `*name`*, but you can of course omit the pre-defined value of the field:
`Faaa
`=
An empty input field: `B444`<demo_empty`>`b
`=
``
You can set the size of the field like this:
`Faaa
`=
A sized input field: `B444`<16|with_size`>`b
`=
``
It is possible to mask fields, for example for use with passwords and similar:
`Faaa
`=
A masked input field: `B444`<!|masked_demo`hidden text>`b
`=
``
And you can of course control all parameters at the same time:
`Faaa
`=
Full control: `B444`<!32|all_options`hidden text>`b
`=
``
Collecting the above markup produces the following output:
`Faaa`B333
A simple input field: `B444`<user_input`Pre-defined data>`B333
An empty input field: `B444`<demo_empty`>`B333
A sized input field: `B444`<16|with_size`>`B333
A masked input field: `B444`<!|masked_demo`hidden text>`B333
Full control: `B444`<!32|all_options`hidden text>`B333
``
>Comments >Comments
You can insert comments that will not be displayed in the output by starting a line with the # character. You can insert comments that will not be displayed in the output by starting a line with the # character.
@@ -1034,7 +1144,7 @@ To display literal content, for example source-code, or blocks of text that shou
`= `=
''' '''
TOPIC_MARKUP += TOPIC_MARKUP.replace("`=", "\\`=") + "[ micron source for document goes here, we don't want infinite recursion now, do we? ]\n\\`=" TOPIC_MARKUP += TOPIC_MARKUP.replace("`=", "\\`=") + "[ micron source for document goes here, we don't want infinite recursion now, do we? ]\n\\`="
TOPIC_MARKUP += "\n`=\n\n>Closing Remarks\n\nIf you made it all the way here, you should be well equipped to write documents and pages using micron. Thank you for staying with me.\n\n`c\U0001F332\n" TOPIC_MARKUP += "\n`=\n\n>Closing Remarks\n\nIf you made it all the way here, you should be well equipped to write documents, pages and applications using micron and Nomad Network. Thank you for staying with me.\n\n`c\U0001F332\n"
TOPICS = { TOPICS = {

View File

@@ -1,5 +1,6 @@
import nomadnet import nomadnet
import urwid import urwid
import random
import time import time
from urwid.util import is_mouse_press from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords from urwid.text_layout import calc_coords
@@ -62,11 +63,12 @@ def markup_to_attrmaps(markup, url_delegate = None):
for line in lines: for line in lines:
if len(line) > 0: if len(line) > 0:
display_widget = parse_line(line, state, url_delegate) display_widgets = parse_line(line, state, url_delegate)
else: else:
display_widget = urwid.Text("") display_widgets = [urwid.Text("")]
if display_widget != None: if display_widgets != None and len(display_widgets) != 0:
for display_widget in display_widgets:
attrmap = urwid.AttrMap(display_widget, make_style(state)) attrmap = urwid.AttrMap(display_widget, make_style(state))
attrmaps.append(attrmap) attrmaps.append(attrmap)
@@ -124,7 +126,7 @@ def parse_line(line, state, url_delegate):
heading_style = first_style heading_style = first_style
output.insert(0, " "*left_indent(state)) output.insert(0, " "*left_indent(state))
return urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style) return [urwid.AttrMap(urwid.Text(output, align=state["align"]), heading_style)]
else: else:
return None return None
else: else:
@@ -137,22 +139,56 @@ def parse_line(line, state, url_delegate):
else: else:
divider_char = "\u2500" divider_char = "\u2500"
if state["depth"] == 0: if state["depth"] == 0:
return urwid.Divider(divider_char) return [urwid.Divider(divider_char)]
else: else:
return urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state)) return [urwid.Padding(urwid.Divider(divider_char), left=left_indent(state), right=right_indent(state))]
output = make_output(state, line, url_delegate) output = make_output(state, line, url_delegate)
if output != None: if output != None:
text_only = True
for o in output:
if not isinstance(o, tuple):
text_only = False
break
if not text_only:
widgets = []
for o in output:
if isinstance(o, tuple):
if url_delegate != None:
tw = LinkableText(o, align=state["align"], delegate=url_delegate)
tw.in_columns = True
else:
tw = urwid.Text(o, align=state["align"])
widgets.append(("pack", tw))
else:
if o["type"] == "field":
fw = o["width"]
fd = o["data"]
fn = o["name"]
fs = o["style"]
fmask = "*" if o["masked"] else None
f = urwid.Edit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask)
f.field_name = fn
fa = urwid.AttrMap(f, fs)
widgets.append((fw, fa))
columns_widget = urwid.Columns(widgets, dividechars=0)
text_widget = columns_widget
# text_widget = urwid.Text("<"+output+">", align=state["align"])
else:
if url_delegate != None: if url_delegate != None:
text_widget = LinkableText(output, align=state["align"], delegate=url_delegate) text_widget = LinkableText(output, align=state["align"], delegate=url_delegate)
else: else:
text_widget = urwid.Text(output, align=state["align"]) text_widget = urwid.Text(output, align=state["align"])
if state["depth"] == 0: if state["depth"] == 0:
return text_widget return [text_widget]
else: else:
return urwid.Padding(text_widget, left=left_indent(state), right=right_indent(state)) return [urwid.Padding(text_widget, left=left_indent(state), right=right_indent(state))]
else: else:
return None return None
else: else:
@@ -423,6 +459,54 @@ def make_output(state, line, url_delegate):
elif c == "a": elif c == "a":
state["align"] = state["default_align"] state["align"] = state["default_align"]
elif c == "<":
try:
field_name = None
field_name_end = line[i:].find("`")
if field_name_end == -1:
pass
else:
field_name = line[i+1:i+field_name_end]
field_name_skip = len(field_name)
field_masked = False
field_width = 24
if "|" in field_name:
f_components = field_name.split("|")
field_flags = f_components[0]
field_name = f_components[1]
if "!" in field_flags:
field_flags = field_flags.replace("!", "")
field_masked = True
if len(field_flags) > 0:
field_width = min(int(field_flags), 256)
def sr():
return "@{"+str(random.randint(1000,9999))+"}"
rsg = sr()
while rsg in line[i+field_name_end:]:
rsg = sr()
lr = line[i+field_name_end:].replace("\\>", rsg)
endpos = lr.find(">")
if endpos == -1:
pass
else:
field_data = lr[1:endpos].replace(rsg, "\\>")
skip = len(field_data)+field_name_skip+2
field_data = field_data.replace("\\>", ">")
output.append({
"type":"field",
"name": field_name,
"width": field_width,
"masked": field_masked,
"data": field_data,
"style": make_style(state)
})
except Exception as e:
pass
elif c == "[": elif c == "[":
endpos = line[i:].find("]") endpos = line[i:].find("]")
if endpos == -1: if endpos == -1:
@@ -434,13 +518,20 @@ def make_output(state, line, url_delegate):
link_components = link_data.split("`") link_components = link_data.split("`")
if len(link_components) == 1: if len(link_components) == 1:
link_label = "" link_label = ""
link_fields = ""
link_url = link_data link_url = link_data
elif len(link_components) == 2: elif len(link_components) == 2:
link_label = link_components[0] link_label = link_components[0]
link_url = link_components[1] link_url = link_components[1]
link_fields = ""
elif len(link_components) == 3:
link_label = link_components[0]
link_url = link_components[1]
link_fields = link_components[2]
else: else:
link_url = "" link_url = ""
link_label = "" link_label = ""
link_fields = ""
if len(link_url) != 0: if len(link_url) != 0:
if link_label == "": if link_label == "":
@@ -468,6 +559,11 @@ def make_output(state, line, url_delegate):
if url_delegate != None: if url_delegate != None:
linkspec = LinkSpec(link_url, orig_spec) linkspec = LinkSpec(link_url, orig_spec)
if link_fields != "":
lf = link_fields.split("|")
if len(lf) > 0:
linkspec.link_fields = lf
output.append((linkspec, link_label)) output.append((linkspec, link_label))
else: else:
output.append(make_part(state, link_label)) output.append(make_part(state, link_label))
@@ -510,6 +606,7 @@ def make_output(state, line, url_delegate):
class LinkSpec(urwid.AttrSpec): class LinkSpec(urwid.AttrSpec):
def __init__(self, link_target, orig_spec): def __init__(self, link_target, orig_spec):
self.link_target = link_target self.link_target = link_target
self.link_fields = None
urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background) urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background)
@@ -525,12 +622,13 @@ class LinkableText(urwid.Text):
self.delegate = delegate self.delegate = delegate
self._cursor_position = 0 self._cursor_position = 0
self.key_timeout = 3 self.key_timeout = 3
self.in_columns = False
if self.delegate != None: if self.delegate != None:
self.delegate.last_keypress = 0 self.delegate.last_keypress = 0
def handle_link(self, link_target): def handle_link(self, link_target, link_fields):
if self.delegate != None: if self.delegate != None:
self.delegate.handle_link(link_target) self.delegate.handle_link(link_target, link_fields)
def find_next_part_pos(self, pos, part_positions): def find_next_part_pos(self, pos, part_positions):
for position in part_positions: for position in part_positions:
@@ -588,7 +686,7 @@ class LinkableText(urwid.Text):
item = self.find_item_at_pos(self._cursor_position) item = self.find_item_at_pos(self._cursor_position)
if item != None: if item != None:
if isinstance(item, LinkSpec): if isinstance(item, LinkSpec):
self.handle_link(item.link_target) self.handle_link(item.link_target, item.link_fields)
elif key == "up": elif key == "up":
self._cursor_position = 0 self._cursor_position = 0
@@ -601,7 +699,11 @@ class LinkableText(urwid.Text):
elif key == "right": elif key == "right":
old = self._cursor_position old = self._cursor_position
self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions) self._cursor_position = self.find_next_part_pos(self._cursor_position, part_positions)
if self._cursor_position == old: if self._cursor_position == old:
if self.in_columns:
return "right"
else:
self._cursor_position = 0 self._cursor_position = 0
return "down" return "down"
@@ -609,6 +711,9 @@ class LinkableText(urwid.Text):
elif key == "left": elif key == "left":
if self._cursor_position > 0: if self._cursor_position > 0:
if self.in_columns:
return "left"
else:
self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions) self._cursor_position = self.find_prev_part_pos(self._cursor_position, part_positions)
self._invalidate() self._invalidate()
@@ -646,6 +751,7 @@ 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):
try:
if button != 1 or not is_mouse_press(event): if button != 1 or not is_mouse_press(event):
return False return False
else: else:
@@ -678,9 +784,12 @@ class LinkableText(urwid.Text):
if item != None: if item != None:
if isinstance(item, LinkSpec): if isinstance(item, LinkSpec):
self.handle_link(item.link_target) self.handle_link(item.link_target, item.link_fields)
self._invalidate() self._invalidate()
self._emit("change") self._emit("change")
return True return True
except Exception as e:
return False

View File

@@ -2,6 +2,7 @@ import RNS
import urwid import urwid
import nomadnet import nomadnet
import time import time
import threading
from datetime import datetime from datetime import datetime
from nomadnet.Directory import DirectoryEntry from nomadnet.Directory import DirectoryEntry
from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY
@@ -13,9 +14,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):
@@ -482,6 +481,12 @@ class KnownNodeInfo(urwid.WidgetWrap):
trust_level = self.app.directory.trust_level(source_hash) trust_level = self.app.directory.trust_level(source_hash)
trust_str = "" trust_str = ""
node_entry = self.app.directory.find(source_hash) node_entry = self.app.directory.find(source_hash)
sort_str = self.app.directory.sort_rank(source_hash)
if sort_str == None:
sort_str = "None"
else:
sort_str = str(sort_str)
if node_entry == None: if node_entry == None:
display_str = self.app.directory.simplest_display_str(source_hash) display_str = self.app.directory.simplest_display_str(source_hash)
else: else:
@@ -542,6 +547,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected) r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected)
e_name = urwid.Edit(caption="Name : ",edit_text=display_str) e_name = urwid.Edit(caption="Name : ",edit_text=display_str)
e_sort = urwid.Edit(caption="Sort Rank : ",edit_text=sort_str)
node_ident = RNS.Identity.recall(source_hash) node_ident = RNS.Identity.recall(source_hash)
op_hash = None op_hash = None
@@ -588,7 +594,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):
@@ -606,8 +612,16 @@ class KnownNodeInfo(urwid.WidgetWrap):
trust_level = DirectoryEntry.TRUSTED trust_level = DirectoryEntry.TRUSTED
display_str = e_name.get_edit_text() display_str = e_name.get_edit_text()
sort_rank = e_sort.get_edit_text()
try:
if int(sort_rank) >= 0:
sort_rank = int(sort_rank)
else:
sort_rank = None
except:
sort_rank = None
node_entry = DirectoryEntry(source_hash, display_name=display_str, trust_level=trust_level, hosts_node=True, identify_on_connect=connect_identify_checkbox.get_state()) node_entry = DirectoryEntry(source_hash, display_name=display_str, trust_level=trust_level, hosts_node=True, identify_on_connect=connect_identify_checkbox.get_state(), sort_rank=sort_rank)
self.app.directory.remember(node_entry) self.app.directory.remember(node_entry)
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
@@ -628,6 +642,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
urwid.Text("Type : "+type_string, align="left"), urwid.Text("Type : "+type_string, align="left"),
e_name, e_name,
urwid.Text("Node Addr : "+addr_str, align="left"), urwid.Text("Node Addr : "+addr_str, align="left"),
e_sort,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Text(lxmf_addr_str, align="center"), urwid.Text(lxmf_addr_str, align="center"),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
@@ -963,12 +978,12 @@ class NodeStorageStats(urwid.WidgetWrap):
def update_stat(self): def update_stat(self):
self.stat_string = "None" self.stat_string = "None"
if self.app.node != None: if self.app.node != None and not self.app.disable_propagation:
limit = self.app.message_router.message_storage_limit limit = self.app.message_router.message_storage_limit
used = self.app.message_router.message_storage_size() used = self.app.message_router.message_storage_size()
if limit != None: if limit != None and used != None:
pct = round((used/limit)*100, 1) pct = round((used/limit)*100, 1)
pct_str = str(pct)+"%, " pct_str = str(pct)+"%, "
limit_str = " of "+RNS.prettysize(limit) limit_str = " of "+RNS.prettysize(limit)
@@ -1299,6 +1314,7 @@ class NodeInfo(urwid.WidgetWrap):
connect_button = urwid.Button("Browse", on_press=connect_query) connect_button = urwid.Button("Browse", on_press=connect_query)
reset_button = urwid.Button("Rst Stats", on_press=stats_query) reset_button = urwid.Button("Rst Stats", on_press=stats_query)
if not self.app.disable_propagation:
pile = urwid.Pile([ pile = urwid.Pile([
t_id, t_id,
e_name, e_name,
@@ -1322,6 +1338,28 @@ class NodeInfo(urwid.WidgetWrap):
("weight", 7, announce_button), ("weight", 7, announce_button),
]) ])
]) ])
else:
pile = urwid.Pile([
t_id,
e_name,
urwid.Divider(g["divider1"]),
self.t_last_announce,
self.t_storage_stats,
self.t_active_links,
self.t_total_connections,
self.t_total_pages,
self.t_total_files,
urwid.Divider(g["divider1"]),
urwid.Columns([
("weight", 5, urwid.Button("Back", on_press=show_peer_info)),
("weight", 0.5, urwid.Text("")),
("weight", 6, connect_button),
("weight", 0.5, urwid.Text("")),
("weight", 8, reset_button),
("weight", 0.5, urwid.Text("")),
("weight", 7, announce_button),
])
])
else: else:
pile = urwid.Pile([ pile = urwid.Pile([
urwid.Text("\n"+g["info"], align="center"), urwid.Text("\n"+g["info"], align="center"),
@@ -1578,13 +1616,19 @@ 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()):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
elif key == "ctrl x": elif key == "ctrl x":
self.delete_selected_entry() self.delete_selected_entry()
elif key == "ctrl r":
self.sync_selected_entry()
return super(LXMFPeers, self).keypress(size, key) return super(LXMFPeers, self).keypress(size, key)
@@ -1600,6 +1644,43 @@ class LXMFPeers(urwid.WidgetWrap):
self.delegate.reinit_lxmf_peers() self.delegate.reinit_lxmf_peers()
self.delegate.show_peers() self.delegate.show_peers()
def sync_selected_entry(self):
sync_grace = 10
si = self.ilb.get_selected_item()
if si != None:
destination_hash = si.original_widget.destination_hash
if destination_hash in self.app.message_router.peers:
peer = self.app.message_router.peers[destination_hash]
if time.time() > peer.last_sync_attempt+sync_grace:
peer.next_sync_attempt = time.time()-1
def job():
peer.sync()
threading.Thread(target=job, daemon=True).start()
time.sleep(0.25)
def dismiss_dialog(sender):
self.close_list_dialogs()
dialog = ListDialogLineBox(
urwid.Pile([
urwid.Text("A delivery sync of all unhandled LXMs was manually requested for the selected node\n", align="center"),
urwid.Columns([("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("OK", on_press=dismiss_dialog))])
]), title="!"
)
dialog.delegate = self.delegate
bottom = self
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
options = self.delegate.left_pile.options("weight", 1)
self.delegate.left_pile.contents[0] = (overlay, options)
def close_list_dialogs(self):
self.delegate.reinit_lxmf_peers()
self.delegate.show_peers()
def rebuild_widget_list(self): def rebuild_widget_list(self):
self.peer_list = self.app.message_router.peers self.peer_list = self.app.message_router.peers
@@ -1613,17 +1694,18 @@ 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.app.directory.pn_trust_level(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) trust_level = self.app.directory.pn_trust_level(peer_id)
pe = LXMFPeerEntry(self.app, peer, self, trust_level)
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):
def __init__(self, app, peer, delegate): def __init__(self, app, peer, delegate, trust_level):
destination_hash = peer.destination_hash destination_hash = peer.destination_hash
self.app = app self.app = app
@@ -1635,7 +1717,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,12 +1727,18 @@ class LXMFPeerEntry(urwid.WidgetWrap):
if hasattr(peer, "alive"): if hasattr(peer, "alive"):
if peer.alive: if peer.alive:
alive_string = "Available" alive_string = "Available"
if trust_level == DirectoryEntry.TRUSTED:
style = "list_trusted"
focus_style = "list_focus_trusted"
else:
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") 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)
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)
self.display_widget.destination_hash = destination_hash self.display_widget.destination_hash = destination_hash
urwid.WidgetWrap.__init__(self, self.display_widget) urwid.WidgetWrap.__init__(self, self.display_widget)

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.8", "lxmf>=0.3.0", "urwid>=2.1.2", "qrcode"], install_requires=["rns>=0.6.2", "lxmf>=0.3.6", "urwid==2.1.2", "qrcode"],
python_requires=">=3.6", python_requires=">=3.6",
) )