Compare commits

...

158 commits

Author SHA1 Message Date
Mark Qvist
a20b4c9bc3 Added funding
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-05-17 10:26:59 +02:00
Mark Qvist
8e0f782df0 Updated LXMF dependency
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-05-15 22:20:03 +02:00
Mark Qvist
58407b9ec4 Mask IFAC passphrase in interface view
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-05-12 11:51:12 +02:00
Mark Qvist
2c7b35033b Fixed legacy mode download when file exists
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
2025-05-11 21:27:32 +02:00
Mark Qvist
c9de9244f4 Updated version 2025-05-11 20:59:43 +02:00
Mark Qvist
96823c8546 Added page background and foreground headers to micron 2025-05-11 20:59:26 +02:00
Mark Qvist
b6e4d8441a Handle first-char escape properly. Fixes #67. 2025-05-11 18:43:13 +02:00
Mark Qvist
c35f6418a2 Added formatted bitrate values to interface charts 2025-05-11 18:11:35 +02:00
Mark Qvist
3ace328918 Scale interface graphs according to terminal size 2025-05-11 17:45:49 +02:00
Mark Qvist
b022631382 Fixed multiple urwid screen init calls 2025-05-11 17:38:28 +02:00
Mark Qvist
0889d4e5e3 Merge commit 'refs/pull/70/head' of github.com:markqvist/NomadNet into ifconf 2025-05-11 17:23:27 +02:00
Mark Qvist
d1c7f3dcfe Added transfer speed to progress bar 2025-05-11 17:21:43 +02:00
Mark Qvist
068fa10990 Merge branch 'master' of github.com:markqvist/NomadNet 2025-05-11 16:43:52 +02:00
Mark Qvist
dfb234c3a9 Updated RNS dependency 2025-05-11 16:43:42 +02:00
Mark Qvist
8d464fcfe4 Updated version 2025-05-11 16:43:19 +02:00
Mark Qvist
f107a9ea30 Use RNS file responses for file downloads instead of reading entire file into memory 2025-05-11 16:42:50 +02:00
markqvist
d1118e5dfe
Merge pull request #73 from Sudo-Ivan/upstream-main-snapshot
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
Fix Scrollable.py
2025-05-09 11:20:01 +02:00
Ivan
b9ac735308 update to use super().__init__(widget) 2025-05-08 18:40:04 -05:00
Mark Qvist
4eed5bab48 Updated readme
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-04-29 11:43:46 +02:00
RFNexus
ab8b30999a fixes for TCPServer, TCPClient, and UDPInterface field definitions 2025-04-22 18:33:33 -04:00
RFNexus
a0d6b752a3 Fixes for RNodeMultiInterface 2025-04-20 23:17:24 -04:00
RFNexus
8e21c26c2f Merge remote-tracking branch 'origin/interface-management' into interface-management 2025-04-20 09:21:27 -04:00
RFNexus
dfa7adc21e Interface Management initial (2/2) 2025-04-20 09:18:56 -04:00
Zenith
7a8ece77f0
Merge branch 'markqvist:master' into interface-management 2025-04-15 14:34:19 -04:00
Mark Qvist
aa4feeb29c Updated version
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-04-15 20:24:48 +02:00
Mark Qvist
ad985a2552 Updated dependencies 2025-04-15 20:23:24 +02:00
Zenith
c415345f59
Merge branch 'markqvist:master' into interface-management 2025-04-13 22:51:41 -04:00
Mark Qvist
42fdc30cf8 Cleanup configobj
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-04-08 02:05:02 +02:00
Mark Qvist
e79689010e Fixed directory sorting bug 2025-04-08 02:03:34 +02:00
Mark Qvist
e4f57d6260 Updated dependencies 2025-03-31 15:03:44 +02:00
RFNexus
c280e36a84 Interface Management (1/2) 2025-03-09 15:20:36 -04:00
Mark Qvist
03d1b22b8f Updated dependencies
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-02-18 21:20:50 +01:00
Mark Qvist
f8775adbab Increased announce stream length 2025-02-18 20:27:33 +01:00
markqvist
c0dac0eadb
Merge pull request #68 from byte-diver/network_tabs
Add network tabs
2025-02-18 20:10:40 +01:00
byte-diver
c198d516da cleaning and sort 2025-02-07 00:23:43 +01:00
byte-diver
d9886980fa fix docker, add tabs 2025-02-07 00:10:44 +01:00
Mark Qvist
7ced4c3659 Fixed missing attribute check
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-01-29 14:45:19 +01:00
Mark Qvist
c3c8f99131 Updated logging
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
2025-01-29 01:41:25 +01:00
Mark Qvist
9f00ffbae6 Updated version 2025-01-29 01:25:33 +01:00
Mark Qvist
a26edd21db Added acceptance rate to PN peer stats 2025-01-29 01:24:29 +01:00
Mark Qvist
46f3a4127c Updated dependencies 2025-01-29 01:24:08 +01:00
Mark Qvist
eeb15dcb43 Use lock on announce stream updates 2025-01-28 23:04:22 +01:00
Mark Qvist
9ef34fc774 Maintain PN list position 2025-01-28 22:45:32 +01:00
Mark Qvist
704a4ea828 Updated issue template
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-01-27 10:26:55 +01:00
Mark Qvist
5f31aeb3c1 Fixed missing none check 2025-01-27 10:26:39 +01:00
Mark Qvist
f000073594 Updated versions
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-01-22 12:06:05 +01:00
Mark Qvist
e332be6211 Added ability configure max_peers and static_peers 2025-01-22 12:05:08 +01:00
Mark Qvist
9ee052a122 Sort PNs according to STR 2025-01-22 12:04:34 +01:00
Mark Qvist
11fda51017 Updated guide 2025-01-22 12:03:49 +01:00
Mark Qvist
d5cf34f9d9 Get PN message counts faster
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
2025-01-21 17:01:06 +01:00
Mark Qvist
d13c8af88e Updated dependencies
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-01-19 21:47:22 +01:00
Mark Qvist
7cd025e236 Updated dependencies
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
2025-01-18 17:20:29 +01:00
Mark Qvist
c48a8d2a09 Updated version
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-01-13 15:25:12 +01:00
Mark Qvist
4fb97eecc5 Updated dependencies 2025-01-13 15:24:23 +01:00
Mark Qvist
74e7e4e806 Updated default config 2025-01-13 15:22:13 +01:00
Mark Qvist
01a5c21016 Revert NF glyphs 2025-01-13 15:17:03 +01:00
Mark Qvist
5e8a14d7a6 Merge branch 'master' of github.com:markqvist/NomadNet 2025-01-13 15:10:06 +01:00
markqvist
6edf531fbf
Merge pull request #64 from kc1awv/patch-1
Update TextUI.py
2025-01-13 15:09:53 +01:00
Mark Qvist
eb3ff558c0 Use built-in platform check 2025-01-13 15:09:39 +01:00
markqvist
b5bf730e07
Merge pull request #65 from liamcottle/master
Fix serving NomadNet pages from Windows
2025-01-13 15:06:33 +01:00
markqvist
c52c49371b
Merge pull request #66 from penguinolog/urwid_deprecations
Handle urwid API deprecations
2025-01-13 15:04:46 +01:00
Mark Qvist
46eee79da2 Added sync transfer rate to PN list display 2025-01-13 14:40:45 +01:00
Mark Qvist
bd909c9f58 Updated version 2025-01-13 14:40:31 +01:00
Aleksei Stepanov
c95fd83400 Handle urwid API deprecations
* `_get_base_widget` method is going to be removed
2025-01-01 13:37:16 +01:00
liamcottle
7d18a103cf don't try to execute nomadnet pages when running on windows 2024-12-20 23:03:31 +13:00
Steve Miller
00855c32a7
Update TextUI.py
Replace some missing or deprecated NF icons with new / updated icons.
2024-12-17 09:07:14 -05:00
Mark Qvist
d8cfc69ac6 Updated dependencies
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-12-12 08:54:59 +01:00
Mark Qvist
ccc41a5789 Updated version 2024-12-12 08:54:05 +01:00
Mark Qvist
7b38d4f80e Fix multiline prop 2024-12-12 08:53:20 +01:00
Mark Qvist
bec7612428 Updated dependencies
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-12-09 22:49:55 +01:00
Mark Qvist
11fd305959 Updated versions 2024-12-09 22:11:49 +01:00
markqvist
c2fc7039fd
Merge pull request #62 from RFnexus/micron
Add Checkbox and Radio Group fields to Micron
2024-11-27 14:15:02 +01:00
zenith
912c510ab2 Add Checkbox and Radio Button fields to Micron format 2024-11-26 19:58:53 -05:00
probability
a5aa2097bd Add Checkbox and Radio Button fields to Micron format 2024-11-25 18:18:33 -05:00
Mark Qvist
dc43bc6a22 Fix invalid LXMF link handling in browser 2024-11-15 16:29:46 +01:00
Mark Qvist
289136a632 Updated version 2024-10-06 11:26:05 +02:00
Mark Qvist
0e79c3299c Added opportunistic delivery if destination ratchets are available 2024-10-06 11:25:08 +02:00
Mark Qvist
c61da069f2 Updated dependencies 2024-10-06 11:14:17 +02:00
Mark Qvist
0df8b56d58 Updated versions
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-09-17 14:53:15 +02:00
Mark Qvist
112a45f270 Fixed invalid dict key. Fixes #59.
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-09-17 14:50:32 +02:00
Mark Qvist
03a02a9ebc Fixed incorrect display name loading in conversation list
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-09-11 13:02:05 +02:00
Mark Qvist
e755641dbe Updated version 2024-09-11 11:49:50 +02:00
Mark Qvist
5392275782 Updated dependencies 2024-09-11 11:49:36 +02:00
Mark Qvist
1bbfacee94 Add stamp configuration options
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
2024-09-11 00:20:35 +02:00
Mark Qvist
0135de3e0e Updated version
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-09-09 16:25:41 +02:00
Mark Qvist
76cb1f73f5 Ratchet, stamp and ticket compatibility
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-09-08 01:23:09 +02:00
Mark Qvist
77c9e6c9eb Updated version and dependencies
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-08-29 15:35:46 +02:00
Mark Qvist
ecb6ca6553 Cleanup
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-08-17 21:24:11 +02:00
markqvist
18cc588f93
Merge pull request #58 from eddebc/fix-windows-log
Some checks are pending
Create and publish a Docker image / build-and-push-image (push) Waiting to run
Add Windows log static tail
2024-08-17 14:59:31 +02:00
markqvist
ed64837a6c
Merge pull request #56 from donuts-are-good/grammar-its-its
Fix: Grammar, it's -> its
2024-08-17 14:48:02 +02:00
edd
4a1832ae34 Add Windows log static tail 2024-08-17 01:41:51 +03:00
donuts-are-good
648242b99f
Fix: Grammar, it's -> its 2024-07-03 11:47:55 -05:00
Mark Qvist
8ad19cf048 Updated readme 2024-05-29 00:38:40 +02:00
Mark Qvist
7bf577a8c5 Updated guide 2024-05-25 22:54:23 +02:00
Mark Qvist
b14d42a17c Updated versions 2024-05-18 15:15:00 +02:00
Mark Qvist
51f0048e7c Updated nerd font glyphs. Fixes #55. 2024-05-18 14:56:22 +02:00
Mark Qvist
c2fb2ca9f8 Updated version and dependencies 2024-05-05 20:13:00 +02:00
Mark Qvist
6a4f202624 Updated dependencies 2024-03-04 00:31:38 +01:00
Mark Qvist
add8b295ec Updated version 2024-03-04 00:31:25 +01:00
Mark Qvist
f1989cfc6e Fixed inadverdent trust level warning 2024-03-02 09:11:09 +01:00
Mark Qvist
d9bba6fd69 Added LXMF transfer size limit options 2024-03-02 00:11:17 +01:00
Mark Qvist
4a3c987cef Merge branch 'master' of github.com:markqvist/NomadNet 2024-03-01 17:55:26 +01:00
Mark Qvist
026ff7b5c7 Updated versions 2024-03-01 17:55:19 +01:00
Mark Qvist
bcca6be487 Updated guide text 2024-03-01 17:55:13 +01:00
markqvist
3181e2124f
Merge pull request #34 from qwertyuiopzxcvbnmlkjhgfdsa1/feature/dockerize
dozkerize build
2024-03-01 17:54:22 +01:00
Mark Qvist
6026f42f34 Check browser destination before displaying save dialog. Fixes #35. 2024-02-29 18:42:47 +01:00
Mark Qvist
e695cce3ba Added issue templates 2024-02-29 18:07:34 +01:00
Mark Qvist
ff45c597f8 Updated readme 2024-02-16 17:54:07 +01:00
Mark Qvist
a4c348529e Merge branch 'master' of github.com:markqvist/NomadNet 2024-02-16 17:48:53 +01:00
Mark Qvist
641f326be7 Updated readme 2024-02-16 17:48:34 +01:00
markqvist
014b9faea6
Merge pull request #53 from neutralinsomniac/notify_on_new_message
add message notification bell for textui
2024-02-08 17:30:22 +01:00
markqvist
ac7eecbd99
Merge pull request #50 from penguinolog/docker_fix
Fix docker image build
2024-02-08 17:29:17 +01:00
Jeremy O'Brien
04376a9f96 add message notification bell for textui 2024-01-25 10:14:12 -05:00
Aleksei Stepanov
c004adfb8c Fix docker image build
* Use non-rc python
* Use `pip` for package install
  `python setup.py --install` is deprecated by setuptools
2024-01-23 08:45:58 +01:00
Mark Qvist
8b69947098 Updated readme 2024-01-18 16:05:45 +01:00
Mark Qvist
ca730ded3b Updated version 2024-01-18 16:00:28 +01:00
Mark Qvist
6a460d9600 Fixed ILB scrolling 2024-01-18 15:57:49 +01:00
markqvist
92a49c12c0
Merge pull request #48 from penguinolog/urwid_constants
Use urwid constants for widgets instead of string equivalents
2024-01-18 15:38:38 +01:00
markqvist
a876c09b4c
Merge branch 'master' into urwid_constants 2024-01-18 15:38:11 +01:00
markqvist
bf0b08c154
Merge pull request #47 from penguinolog/urwid_regression_fixed
Ban `urwid==2.4.3` as containing UI regression.
2024-01-18 15:34:34 +01:00
Aleksei Stepanov
77eca5d23e Use urwid constants for widgets instead of string equivalents
* Reduce `DeprecationWarning` amount:
  `get_focus` -> `focus_position` for `Frame` and `focus` for `Pile`
  `set_focus` -> use property `focus_position`
2024-01-18 11:58:22 +01:00
Aleksei Stepanov
ed6ba63317 Ban urwid==2.4.3 as containing UI regression.
Fix urwid entities init: use correct `super()`,
hack `__super` is officially deprecated.
Method <Class>.method(self, ...) is only for fallback in corner cases
(normally not recommended to use).
2024-01-18 10:22:38 +01:00
Mark Qvist
d856f3fd28 Pin Urwid to 2.4.2 due to rendering bug in 2.4.3 2024-01-17 23:31:16 +01:00
Mark Qvist
24d850107d Updated LXMF dependency 2024-01-17 23:15:00 +01:00
Mark Qvist
9943cad15f Updated readme 2024-01-17 23:08:52 +01:00
Mark Qvist
dcd5f7df95 Updated readme 2024-01-15 20:26:10 +01:00
Mark Qvist
d840ca32ae Resolve merge 2024-01-15 19:42:48 +01:00
markqvist
37063932bb
Merge pull request #46 from penguinolog/patch-1
Allow newer versions of urwid
2024-01-15 19:40:58 +01:00
Mark Qvist
0ed4e09b82 Updated nomadnet to use Urwid 2.4.2 2024-01-15 19:40:01 +01:00
Mark Qvist
910e527cc7 Terminate log tail when view is inactive 2024-01-15 19:30:26 +01:00
Alexey Stepanov
4eef326d6b
Allow newer versions of urwid
Last urwid versions are backward-compatible with 2.1.2 (but may produce "DeprecationWarning" on legacy logic)
2024-01-15 10:39:35 +01:00
Mark Qvist
691f4df098 Updated versions 2024-01-14 01:08:10 +01:00
Mark Qvist
407cc8fb5f Updated version 2024-01-08 00:12:38 +01:00
Mark Qvist
1f7302903a Fixed missing timestamp update on page/file periodic scan 2024-01-08 00:10:55 +01:00
Mark Qvist
5736012f2c Updated version 2024-01-03 13:07:20 +01:00
Mark Qvist
bb98a512f3 Added page and file refresh intervals to guide 2024-01-03 13:05:38 +01:00
markqvist
ae0d4c6e0c
Merge pull request #41 from faragher/master
Made save paths relative, added page/file refresh
2024-01-03 12:55:19 +01:00
faragher
2449b39f77 Added file and path scans to jobs 2023-11-29 05:43:49 -06:00
faragher
e022d469f8 Made save paths relative 2023-11-29 03:23:35 -06:00
markqvist
a4f665e650
Merge pull request #39 from faragher/master
Possible fix for Directory error
2023-11-28 14:16:04 +01:00
faragher
19d1d8504f Possible fix for Directory error 2023-11-26 15:51:47 -06:00
Mark Qvist
276a5f56e1 Changed text 2023-11-02 18:30:23 +01:00
Mark Qvist
89dd17ece5 Updated version and dependencies 2023-11-02 12:48:40 +01:00
Mark Qvist
82bb479957 Adjusted timeouts 2023-11-02 12:25:36 +01:00
Mark Qvist
082026ca1b Updated dependencies 2023-10-27 20:43:31 +02:00
Mark Qvist
df9fccf199 Updated version 2023-10-27 20:42:03 +02:00
Mark Qvist
efd1d08399 Improved LXMF sync feedback 2023-10-27 20:41:18 +02:00
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
Petr Blaha
a5623978a2 dozkerize build 2023-09-07 10:19:38 +02:00
32 changed files with 6101 additions and 3108 deletions

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: ✨ Feature Request or Idea
url: https://github.com/markqvist/Reticulum/discussions/new?category=ideas
about: Propose and discuss features and ideas
- name: 💬 Questions, Help & Discussion
about: Ask anything, or get help
url: https://github.com/markqvist/Reticulum/discussions/new/choose
- name: 📖 Read the Reticulum Manual
url: https://markqvist.github.io/Reticulum/manual/
about: The complete documentation for Reticulum

View file

@ -0,0 +1,35 @@
---
name: "\U0001F41B Bug Report"
about: Report a reproducible bug
title: ''
labels: ''
assignees: ''
---
**Read the Contribution Guidelines**
Before creating a bug report on this issue tracker, you **must** read the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md). Issues that do not follow the contribution guidelines **will be deleted without comment**.
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
**Describe the Bug**
A clear and concise description of what the bug is.
**To Reproduce**
Describe in detail how to reproduce the bug.
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Logs & Screenshots**
Please include any relevant log output. If applicable, also add screenshots to help explain your problem.
**System Information**
- OS and version
- Python version
- Program version
**Additional context**
Add any other context about the problem here.

View file

@ -1,4 +1,4 @@
FROM python:3.11-rc-alpine3.14 as build FROM python:3.12-alpine as build
RUN apk add --no-cache build-base linux-headers libffi-dev cargo RUN apk add --no-cache build-base linux-headers libffi-dev cargo
@ -8,10 +8,10 @@ ENV PATH="/opt/venv/bin:$PATH"
RUN pip3 install setuptools-rust pyopenssl cryptography RUN pip3 install setuptools-rust pyopenssl cryptography
COPY . /app/ COPY . /app/
RUN cd /app/ && python3 setup.py install RUN cd /app/ && pip3 install .
# Use multi-stage build, as we don't need rust compilation on the final image # Use multi-stage build, as we don't need rust compilation on the final image
FROM python:3.11-rc-alpine3.14 FROM python:3.12-alpine
LABEL org.opencontainers.image.documentation="https://github.com/markqvist/NomadNet#nomad-network-daemon-with-docker" LABEL org.opencontainers.image.documentation="https://github.com/markqvist/NomadNet#nomad-network-daemon-with-docker"

28
Dockerfile.build Normal file
View file

@ -0,0 +1,28 @@
FROM python:alpine
LABEL authors="Petr Blaha petr.blaha@cleverdata.cz"
USER root
RUN apk update
RUN apk add build-base libffi-dev cargo pkgconfig linux-headers py3-virtualenv
RUN addgroup -S myuser && adduser -S -G myuser myuser
USER myuser
WORKDIR /home/myuser
RUN pip install --upgrade pip
RUN pip install setuptools-rust pyopenssl cryptography
ENV PATH="/home/myuser/.local/bin:${PATH}"
################### BEGIN NomadNet ###########################################
COPY --chown=myuser:myuser . .
#Python create virtual environment
RUN virtualenv /home/myuser/NomadNet/venv
RUN source /home/myuser/NomadNet/venv/bin/activate
RUN make all
################### END NomadNet ###########################################

6
Dockerfile.howto Normal file
View file

@ -0,0 +1,6 @@
# Run docker command one by one(all four), it will build NomadNet artifact and copy to dist directory.
# No need to build locally and install dependencies
docker build -t nomadnetdockerimage -f Dockerfile.build .
docker run -d -it --name nomadnetdockercontainer nomadnetdockerimage /bin/sh
docker cp nomadnetdockercontainer:/home/myuser/dist .
docker rm -f nomadnetdockercontainer

3
FUNDING.yml Normal file
View file

@ -0,0 +1,3 @@
liberapay: Reticulum
ko_fi: markqvist
custom: "https://unsigned.io/donate"

View file

@ -4,11 +4,11 @@ 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.
Nomad Network does not need any connections to the public internet to work. In fact, it doesn't even need an IP or Ethernet network. You can use it entirely over packet radio, LoRa or even serial lines. But if you wish, you can bridge islanded networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours. Nomad Network does not need any connections to the public internet to work. In fact, it doesn't even need an IP or Ethernet network. You can use it entirely over packet radio, LoRa or even serial lines. But if you wish, you can bridge islanded networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours. Since Nomad Network uses Reticulum, it is efficient enough to run even over *extremely* low-bandwidth medium, and has been succesfully used over 300bps radio links.
If you'd rather want to use an LXMF client with a graphical user interface, you may want to take a look at [Sideband](https://github.com/markqvist/sideband), which is available for Linux, Android and macOS. If you'd rather want to use an LXMF client with a graphical user interface, you may want to take a look at [Sideband](https://github.com/markqvist/sideband), which is available for Linux, Android and macOS.
@ -22,9 +22,6 @@ If you'd rather want to use an LXMF client with a graphical user interface, you
- An easy to use and bandwidth efficient markup language for writing pages - An easy to use and bandwidth efficient markup language for writing pages
- Page caching in browser - Page caching in browser
## Current Status
The current version of the program should be considered a beta release. The program works well, but there will most probably be bugs and possibly sub-optimal performance in some scenarios. On the other hand, this is the ideal time to have an influence on the direction of the development of Nomad Network. To do so, join the discussion, report bugs and request features here on the GitHub project.
## How do I get started? ## How do I get started?
The easiest way to install Nomad Network is via pip: The easiest way to install Nomad Network is via pip:
@ -121,6 +118,23 @@ $ docker run -d \
$ docker run -i ghcr.io/markqvist/nomadnet:master --daemon --console $ docker run -i ghcr.io/markqvist/nomadnet:master --daemon --console
``` ```
## Tools & Extensions
Nomad Network is a very flexible and extensible platform, and a variety of community-provided tools, utilities and node-side extensions exist:
- [NomadForum](https://codeberg.org/AutumnSpark1226/nomadForum) ([GitHub mirror](https://github.com/AutumnSpark1226/nomadForum))
- [NomadForecast](https://github.com/faragher/NomadForecast)
- [micron-blog](https://github.com/randogoth/micron-blog)
- [md2mu](https://github.com/randogoth/md2mu)
- [Any2MicronConverter](https://github.com/SebastianObi/Any2MicronConverter)
- [Some nomadnet page examples](https://github.com/SebastianObi/NomadNet-Pages)
- [More nomadnet page examples](https://github.com/epenguins/NomadNet_pages)
- [LXMF-Bot](https://github.com/randogoth/lxmf-bot)
- [LXMF Messageboard](https://github.com/chengtripp/lxmf_messageboard)
- [LXMEvent](https://github.com/faragher/LXMEvent)
- [POPR](https://github.com/faragher/POPR)
- [LXMF Tools](https://github.com/SebastianObi/LXMF-Tools)
## Help & Discussion ## Help & Discussion
For help requests, discussion, sharing ideas or anything else related to Nomad Network, please have a look at the [Nomad Network discussions pages](https://github.com/markqvist/Reticulum/discussions/categories/nomad-network). For help requests, discussion, sharing ideas or anything else related to Nomad Network, please have a look at the [Nomad Network discussions pages](https://github.com/markqvist/Reticulum/discussions/categories/nomad-network).
@ -132,13 +146,13 @@ You can help support the continued development of open, free and private communi
``` ```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w 84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
``` ```
- Ethereum
```
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
```
- Bitcoin - Bitcoin
``` ```
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5
```
- Ethereum
```
0xae89F3B94fC4AD6563F0864a55F9a697a90261ff
``` ```
- Ko-Fi: https://ko-fi.com/markqvist - Ko-Fi: https://ko-fi.com/markqvist
@ -147,7 +161,6 @@ You can help support the continued development of open, free and private communi
- New major features - New major features
- Network-wide propagated bulletins and discussion threads - Network-wide propagated bulletins and discussion threads
- Collaborative maps and geospatial information sharing - Collaborative maps and geospatial information sharing
- Facilitation of trade and barter
- Minor improvements and fixes - Minor improvements and fixes
- Link status (RSSI and SNR) in conversation or conv list - Link status (RSSI and SNR) in conversation or conv list
- Ctrl-M shorcut for jumping to menu - Ctrl-M shorcut for jumping to menu

View file

@ -27,6 +27,13 @@ class Conversation:
if Conversation.created_callback != None: if Conversation.created_callback != None:
Conversation.created_callback() Conversation.created_callback()
# This reformats the new v0.5.0 announce data back to the expected format
# for nomadnets storage and other handling functions.
dn = LXMF.display_name_from_app_data(app_data)
app_data = b""
if dn != None:
app_data = dn.encode("utf-8")
# Add the announce to the directory announce # Add the announce to the directory announce
# stream logger # stream logger
app.directory.lxmf_announce_received(destination_hash, app_data) app.directory.lxmf_announce_received(destination_hash, app_data)
@ -95,7 +102,7 @@ class Conversation:
unread = True unread = True
if display_name == None and app_data: if display_name == None and app_data:
display_name = app_data.decode("utf-8") display_name = LXMF.display_name_from_app_data(app_data)
if display_name == None: if display_name == None:
sort_name = "" sort_name = ""
@ -142,7 +149,7 @@ class Conversation:
self.__changed_callback = None self.__changed_callback = None
if not RNS.Transport.has_path(bytes.fromhex(source_hash)): if not RNS.Identity.recall(bytes.fromhex(self.source_hash)):
RNS.Transport.request_path(bytes.fromhex(source_hash)) RNS.Transport.request_path(bytes.fromhex(source_hash))
self.source_identity = RNS.Identity.recall(bytes.fromhex(self.source_hash)) self.source_identity = RNS.Identity.recall(bytes.fromhex(self.source_hash))
@ -209,8 +216,16 @@ class Conversation:
if self.app.directory.preferred_delivery(dest.hash) == DirectoryEntry.PROPAGATED: if self.app.directory.preferred_delivery(dest.hash) == DirectoryEntry.PROPAGATED:
if self.app.message_router.get_outbound_propagation_node() != None: if self.app.message_router.get_outbound_propagation_node() != None:
desired_method = LXMF.LXMessage.PROPAGATED desired_method = LXMF.LXMessage.PROPAGATED
else:
if not self.app.message_router.delivery_link_available(dest.hash) and RNS.Identity.current_ratchet_id(dest.hash) != None:
RNS.log(f"Have ratchet for {RNS.prettyhexrep(dest.hash)}, requesting opportunistic delivery of message", RNS.LOG_DEBUG)
desired_method = LXMF.LXMessage.OPPORTUNISTIC
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method) dest_is_trusted = False
if self.app.directory.trust_level(dest.hash) == DirectoryEntry.TRUSTED:
dest_is_trusted = True
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method, include_ticket=dest_is_trusted)
lxm.register_delivery_callback(self.message_notification) lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification) lxm.register_failed_callback(self.message_notification)
@ -227,7 +242,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,18 +250,41 @@ 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)
qr_code = lxm.as_qr()
qr_tmp_path = self.app.tmpfilespath+"/"+str(RNS.hexrep(lxm.hash, delimit=False))
qr_code.save(qr_tmp_path)
print_result = self.app.print_file(qr_tmp_path) if mode == "print_qr":
os.unlink(qr_tmp_path) qr_code = lxm.as_qr()
qr_tmp_path = self.app.tmpfilespath+"/"+str(RNS.hexrep(lxm.hash, delimit=False))
qr_code.save(qr_tmp_path)
if print_result: print_result = self.app.print_file(qr_tmp_path)
os.unlink(qr_tmp_path)
if print_result:
message_path = Conversation.ingest(lxm, self.app, originator=True)
self.messages.append(ConversationMessage(message_path))
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) message_path = Conversation.ingest(lxm, self.app, originator=True)
self.messages.append(ConversationMessage(message_path)) self.messages.append(ConversationMessage(message_path))
return qr_save_path
return print_result 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)
@ -258,13 +296,17 @@ class Conversation:
def message_notification(self, message): def message_notification(self, message):
if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail:
RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE) if hasattr(message, "stamp_generation_failed") and message.stamp_generation_failed == True:
message.try_propagation_on_fail = None RNS.log(f"Could not send {message} due to a stamp generation failure", RNS.LOG_ERROR)
message.delivery_attempts = 0 else:
del message.next_delivery_attempt RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE)
message.packed = None message.try_propagation_on_fail = None
message.desired_method = LXMF.LXMessage.PROPAGATED message.delivery_attempts = 0
self.app.message_router.handle_outbound(message) if hasattr(message, "next_delivery_attempt"):
del message.next_delivery_attempt
message.packed = None
message.desired_method = LXMF.LXMessage.PROPAGATED
self.app.message_router.handle_outbound(message)
else: else:
message_path = Conversation.ingest(message, self.app, originator=True) message_path = Conversation.ingest(message, self.app, originator=True)
@ -295,12 +337,17 @@ class ConversationMessage:
self.timestamp = self.lxm.timestamp self.timestamp = self.lxm.timestamp
self.sort_timestamp = os.path.getmtime(self.file_path) self.sort_timestamp = os.path.getmtime(self.file_path)
if self.lxm.state > LXMF.LXMessage.DRAFT and self.lxm.state < LXMF.LXMessage.SENT: if self.lxm.state > LXMF.LXMessage.GENERATING and self.lxm.state < LXMF.LXMessage.SENT:
found = False found = False
for pending in nomadnet.NomadNetworkApp.get_shared_instance().message_router.pending_outbound: for pending in nomadnet.NomadNetworkApp.get_shared_instance().message_router.pending_outbound:
if pending.hash == self.lxm.hash: if pending.hash == self.lxm.hash:
found = True found = True
for pending_id in nomadnet.NomadNetworkApp.get_shared_instance().message_router.pending_deferred_stamps:
if pending_id == self.lxm.hash:
found = True
if not found: if not found:
self.lxm.state = LXMF.LXMessage.FAILED self.lxm.state = LXMF.LXMessage.FAILED

View file

@ -3,6 +3,7 @@ import RNS
import LXMF import LXMF
import time import time
import nomadnet import nomadnet
import threading
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
class PNAnnounceHandler: class PNAnnounceHandler:
@ -29,7 +30,7 @@ class PNAnnounceHandler:
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
class Directory: class Directory:
ANNOUNCE_STREAM_MAXLENGTH = 64 ANNOUNCE_STREAM_MAXLENGTH = 256
aspect_filter = "nomadnetwork.node" aspect_filter = "nomadnetwork.node"
@staticmethod @staticmethod
@ -55,6 +56,7 @@ class Directory:
self.directory_entries = {} self.directory_entries = {}
self.announce_stream = [] self.announce_stream = []
self.app = app self.app = app
self.announce_lock = threading.Lock()
self.load_from_disk() self.load_from_disk()
self.pn_announce_handler = PNAnnounceHandler(self) self.pn_announce_handler = PNAnnounceHandler(self)
@ -66,7 +68,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,
@ -91,6 +93,9 @@ class Directory:
entries = {} entries = {}
for e in unpacked_list: for e in unpacked_list:
if e[1] == None:
e[1] = "Undefined"
if len(e) > 3: if len(e) > 3:
hosts_node = e[3] hosts_node = e[3]
else: else:
@ -106,101 +111,109 @@ 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: with self.announce_lock:
if self.app.compact_stream: if app_data != None:
try: if self.app.compact_stream:
remove_announces = [] try:
for announce in self.announce_stream: remove_announces = []
if announce[1] == source_hash: for announce in self.announce_stream:
remove_announces.append(announce) if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) self.announce_stream.remove(a)
except Exception as e: except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR) 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:
self.announce_stream.pop() self.announce_stream.pop()
if hasattr(self.app.ui, "main_display"): if hasattr(self.app, "ui") and self.app.ui != None:
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
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: with self.announce_lock:
if self.app.compact_stream: if app_data != None:
try: if self.app.compact_stream:
remove_announces = [] try:
for announce in self.announce_stream: remove_announces = []
if announce[1] == source_hash: for announce in self.announce_stream:
remove_announces.append(announce) if announce[1] == source_hash:
remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) self.announce_stream.remove(a)
except Exception as e: except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR) 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:
self.announce_stream.pop() self.announce_stream.pop()
if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED: if self.trust_level(associated_peer) == DirectoryEntry.TRUSTED:
existing_entry = self.find(source_hash) existing_entry = self.find(source_hash)
if not existing_entry: if not existing_entry:
node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True) node_entry = DirectoryEntry(source_hash, display_name=app_data.decode("utf-8"), trust_level=DirectoryEntry.TRUSTED, hosts_node=True)
self.remember(node_entry) self.remember(node_entry)
if hasattr(self.app.ui, "main_display"): if hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def pn_announce_received(self, source_hash, app_data, associated_peer, associated_node): def pn_announce_received(self, source_hash, app_data, associated_peer, associated_node):
found_node = None with self.announce_lock:
for sh in self.directory_entries: found_node = None
if sh == associated_node: for sh in self.directory_entries:
found_node = True if sh == associated_node:
break found_node = True
break
for e in self.announce_stream: for e in self.announce_stream:
if e[1] == associated_node: if e[1] == associated_node:
found_node = True found_node = True
break break
if not found_node: if not found_node:
if self.app.compact_stream: if self.app.compact_stream:
try: try:
remove_announces = [] remove_announces = []
for announce in self.announce_stream: for announce in self.announce_stream:
if announce[1] == source_hash: if announce[1] == source_hash:
remove_announces.append(announce) remove_announces.append(announce)
for a in remove_announces: for a in remove_announces:
self.announce_stream.remove(a) self.announce_stream.remove(a)
except Exception as e: except Exception as e:
RNS.log("An error occurred while compacting the announce stream. The contained exception was:"+str(e), RNS.LOG_ERROR) 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:
self.announce_stream.pop() self.announce_stream.pop()
if hasattr(self.app.ui, "main_display"): if hasattr(self.app, "ui") and hasattr(self.app.ui, "main_display"):
self.app.ui.main_display.sub_displays.network_display.directory_change_callback() self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
def remove_announce_with_timestamp(self, timestamp): def remove_announce_with_timestamp(self, timestamp):
selected_announce = None selected_announce = None
@ -220,7 +233,14 @@ 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:
return "<"+RNS.hexrep(source_hash, delimit=False)+">" 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)+">"
else: else:
if source_hash in self.directory_entries: if source_hash in self.directory_entries:
dn = self.directory_entries[source_hash].display_name dn = self.directory_entries[source_hash].display_name
@ -243,16 +263,29 @@ class Directory:
if announced_display_name == None: if announced_display_name == None:
return self.directory_entries[source_hash].trust_level return self.directory_entries[source_hash].trust_level
else: else:
for entry in self.directory_entries: if not self.directory_entries[source_hash].trust_level == DirectoryEntry.TRUSTED:
e = self.directory_entries[entry] for entry in self.directory_entries:
if e.display_name == announced_display_name: e = self.directory_entries[entry]
if e.source_hash != source_hash: if e.display_name == announced_display_name:
return DirectoryEntry.WARNING if e.source_hash != source_hash:
return DirectoryEntry.WARNING
return self.directory_entries[source_hash].trust_level return self.directory_entries[source_hash].trust_level
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
@ -312,6 +345,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 if e.display_name != None else "_"))
return node_list return node_list
def number_of_known_nodes(self): def number_of_known_nodes(self):
@ -336,10 +370,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

@ -1,4 +1,6 @@
import os import os
import sys
import RNS import RNS
import time import time
import threading import threading
@ -15,7 +17,11 @@ class Node:
self.identity = self.app.identity self.identity = self.app.identity
self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node") self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node")
self.last_announce = time.time() self.last_announce = time.time()
self.last_file_refresh = time.time()
self.last_page_refresh = time.time()
self.announce_interval = self.app.node_announce_interval self.announce_interval = self.app.node_announce_interval
self.page_refresh_interval = self.app.page_refresh_interval
self.file_refresh_interval = self.app.file_refresh_interval
self.job_interval = Node.JOB_INTERVAL self.job_interval = Node.JOB_INTERVAL
self.should_run_jobs = True self.should_run_jobs = True
self.app_data = None self.app_data = None
@ -46,6 +52,8 @@ class Node:
def register_pages(self): def register_pages(self):
# TODO: Deregister previously registered pages
# that no longer exist.
self.servedpages = [] self.servedpages = []
self.scan_pages(self.app.pagespath) self.scan_pages(self.app.pagespath)
@ -53,18 +61,18 @@ class Node:
self.destination.register_request_handler( self.destination.register_request_handler(
"/page/index.mu", "/page/index.mu",
response_generator = self.serve_default_index, response_generator = self.serve_default_index,
allow = RNS.Destination.ALLOW_ALL allow = RNS.Destination.ALLOW_ALL)
)
for page in self.servedpages: for page in self.servedpages:
request_path = "/page"+page.replace(self.app.pagespath, "") request_path = "/page"+page.replace(self.app.pagespath, "")
self.destination.register_request_handler( self.destination.register_request_handler(
request_path, request_path,
response_generator = self.serve_page, response_generator = self.serve_page,
allow = RNS.Destination.ALLOW_ALL allow = RNS.Destination.ALLOW_ALL)
)
def register_files(self): def register_files(self):
# TODO: Deregister previously registered files
# that no longer exist.
self.servedfiles = [] self.servedfiles = []
self.scan_files(self.app.filespath) self.scan_files(self.app.filespath)
@ -73,8 +81,8 @@ class Node:
self.destination.register_request_handler( self.destination.register_request_handler(
request_path, request_path,
response_generator = self.serve_file, response_generator = self.serve_file,
allow = RNS.Destination.ALLOW_ALL allow = RNS.Destination.ALLOW_ALL,
) auto_compress = 32_000_000)
def scan_pages(self, base_path): def scan_pages(self, base_path):
files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."] files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."]
@ -151,7 +159,7 @@ class Node:
try: try:
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 not RNS.vendor.platformutils.is_windows() and os.access(file_path, os.X_OK):
env_map = {} env_map = {}
if "PATH" in os.environ: if "PATH" in os.environ:
env_map["PATH"] = os.environ["PATH"] env_map["PATH"] = os.environ["PATH"]
@ -195,10 +203,7 @@ class Node:
file_name = path.replace("/file/", "", 1) file_name = path.replace("/file/", "", 1)
try: try:
RNS.log("Serving file: "+file_path, RNS.LOG_VERBOSE) RNS.log("Serving file: "+file_path, RNS.LOG_VERBOSE)
fh = open(file_path, "rb") return [open(file_path, "rb"), {"name": file_name.encode("utf-8")}]
file_data = fh.read()
fh.close()
return [file_name, file_data]
except Exception as e: except Exception as e:
RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR) RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR)
@ -223,6 +228,16 @@ class Node:
if now > self.last_announce + self.announce_interval*60: if now > self.last_announce + self.announce_interval*60:
self.announce() self.announce()
if self.page_refresh_interval > 0:
if now > self.last_page_refresh + self.page_refresh_interval*60:
self.register_pages()
self.last_page_refresh = time.time()
if self.file_refresh_interval > 0:
if now > self.last_file_refresh + self.file_refresh_interval*60:
self.register_files()
self.last_file_refresh = time.time()
time.sleep(self.job_interval) time.sleep(self.job_interval)
def peer_connected(self, link): def peer_connected(self, link):

View file

@ -19,7 +19,7 @@ from datetime import datetime
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
from ._version import __version__ from ._version import __version__
from .vendor.configobj import ConfigObj from RNS.vendor.configobj import ConfigObj
class NomadNetworkApp: class NomadNetworkApp:
time_format = "%Y-%m-%d %H:%M:%S" time_format = "%Y-%m-%d %H:%M:%S"
@ -115,19 +115,30 @@ class NomadNetworkApp:
self.downloads_path = os.path.expanduser("~/Downloads") self.downloads_path = os.path.expanduser("~/Downloads")
self.firstrun = False self.firstrun = False
self.should_run_jobs = True self.should_run_jobs = True
self.job_interval = 5 self.job_interval = 5
self.defer_jobs = 90 self.defer_jobs = 90
self.page_refresh_interval = 0
self.file_refresh_interval = 0
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.notify_on_new_message = True
self.lxmf_max_propagation_size = None
self.lxmf_max_incoming_size = None
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 self.compact_stream = False
self.required_stamp_cost = None
self.accept_invalid_stamps = False
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@ -279,14 +290,30 @@ class NomadNetworkApp:
self.directory = nomadnet.Directory(self) self.directory = nomadnet.Directory(self)
self.message_router = LXMF.LXMRouter(identity = self.identity, storagepath = self.storagepath, autopeer = True) static_peers = []
for static_peer in self.static_peers:
try:
dh = bytes.fromhex(static_peer)
if len(dh) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
raise ValueError("Invalid destination length")
static_peers.append(dh)
except Exception as e:
RNS.log(f"Could not decode static peer destination hash {static_peer}: {e}", RNS.LOG_ERROR)
self.message_router = LXMF.LXMRouter(
identity = self.identity, storagepath = self.storagepath, autopeer = True,
propagation_limit = self.lxmf_max_propagation_size, delivery_limit = self.lxmf_max_incoming_size,
max_peers = self.max_peers, static_peers = static_peers,
)
self.message_router.register_delivery_callback(self.lxmf_delivery) self.message_router.register_delivery_callback(self.lxmf_delivery)
for destination_hash in self.ignored_list: for destination_hash in self.ignored_list:
self.message_router.ignore_destination(destination_hash) self.message_router.ignore_destination(destination_hash)
self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.peer_settings["display_name"]) self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.peer_settings["display_name"], stamp_cost=self.required_stamp_cost)
self.lxmf_destination.set_default_app_data(self.get_display_name_bytes) if not self.accept_invalid_stamps:
self.message_router.enforce_stamps()
RNS.Identity.remember( RNS.Identity.remember(
packet_hash=None, packet_hash=None,
@ -308,15 +335,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)
self.message_router.enable_propagation() if self.disable_propagation:
try: if os.path.isfile(self.pnannouncedpath):
with open(self.pnannouncedpath, "wb") as pnf: try:
pnf.write(msgpack.packb(time.time())) RNS.log("Sending indication to peered LXMF Propagation Node that this node is no longer participating", RNS.LOG_DEBUG)
pnf.close() self.message_router.disable_propagation()
except Exception as e: os.unlink(self.pnannouncedpath)
RNS.log("An error ocurred while writing Propagation Node announce timestamp. The contained exception was: "+str(e), RNS.LOG_ERROR) 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()
try:
with open(self.pnannouncedpath, "wb") as pnf:
pnf.write(msgpack.packb(time.time()))
pnf.close()
except Exception as e:
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
@ -414,16 +453,24 @@ class NomadNetworkApp:
return "Receiving messages" return "Receiving messages"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RESPONSE_RECEIVED: elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RESPONSE_RECEIVED:
return "Messages received" return "Messages received"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_PATH:
return "No path to node"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_FAILED:
return "Link establisment failed"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_TRANSFER_FAILED:
return "Sync request failed"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_IDENTITY_RCVD:
return "Remote got no identity"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_ACCESS:
return "Node rejected request"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_FAILED:
return "Sync failed"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE:
new_msgs = self.message_router.propagation_transfer_last_result new_msgs = self.message_router.propagation_transfer_last_result
if new_msgs == 0: if new_msgs == 0:
return "Done, no new messages" return "Done, no new messages"
else: else:
return "Downloaded "+str(new_msgs)+" new messages" return "Downloaded "+str(new_msgs)+" new messages"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_IDENTITY_RCVD:
return "Node did not receive identification"
elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_ACCESS:
return "Node did not allow request"
else: else:
return "Unknown" return "Unknown"
@ -461,7 +508,9 @@ class NomadNetworkApp:
self.message_router.cancel_propagation_node_requests() self.message_router.cancel_propagation_node_requests()
def announce_now(self): def announce_now(self):
self.lxmf_destination.announce() self.message_router.set_inbound_stamp_cost(self.lxmf_destination.hash, self.required_stamp_cost)
self.lxmf_destination.display_name = self.peer_settings["display_name"]
self.message_router.announce(self.lxmf_destination.hash)
self.peer_settings["last_announce"] = time.time() self.peer_settings["last_announce"] = time.time()
self.save_peer_settings() self.save_peer_settings()
@ -488,7 +537,7 @@ class NomadNetworkApp:
RNS.log("Could not autoselect a propagation node! LXMF propagation will not be available until a trusted node announces on the network, or a propagation node is manually selected.", RNS.LOG_WARNING) RNS.log("Could not autoselect a propagation node! LXMF propagation will not be available until a trusted node announces on the network, or a propagation node is manually selected.", RNS.LOG_WARNING)
else: else:
pn_name_str = "" pn_name_str = ""
RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_INFO) RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_DEBUG)
self.message_router.set_outbound_propagation_node(selected_node) self.message_router.set_outbound_propagation_node(selected_node)
def get_user_selected_propagation_node(self): def get_user_selected_propagation_node(self):
@ -523,6 +572,9 @@ class NomadNetworkApp:
nomadnet.Conversation.ingest(message, self) nomadnet.Conversation.ingest(message, self)
if self.notify_on_new_message:
self.notify_message_recieved()
if self.should_print(message): if self.should_print(message):
self.print_message(message) self.print_message(message)
@ -626,6 +678,11 @@ class NomadNetworkApp:
if os.path.isfile(self.conversationpath + "/" + source_hash + "/unread"): if os.path.isfile(self.conversationpath + "/" + source_hash + "/unread"):
os.unlink(self.conversationpath + "/" + source_hash + "/unread") os.unlink(self.conversationpath + "/" + source_hash + "/unread")
def notify_message_recieved(self):
if self.uimode == nomadnet.ui.UI_TEXT:
sys.stdout.write("\a")
sys.stdout.flush()
def clear_tmp_dir(self): def clear_tmp_dir(self):
if os.path.isdir(self.tmpfilespath): if os.path.isdir(self.tmpfilespath):
for file in os.listdir(self.tmpfilespath): for file in os.listdir(self.tmpfilespath):
@ -699,10 +756,40 @@ class NomadNetworkApp:
else: else:
self.lxmf_sync_limit = None self.lxmf_sync_limit = None
if option == "required_stamp_cost":
value = self.config["client"][option]
if value.lower() == "none":
self.required_stamp_cost = None
else:
value = self.config["client"].as_int(option)
if value > 0:
if value > 255:
value = 255
self.required_stamp_cost = value
else:
self.required_stamp_cost = None
if option == "accept_invalid_stamps":
value = self.config["client"].as_bool(option)
self.accept_invalid_stamps = value
if option == "max_accepted_size":
value = self.config["client"].as_float(option)
if value > 0:
self.lxmf_max_incoming_size = value
else:
self.lxmf_max_incoming_size = 500
if option == "compact_announce_stream": if option == "compact_announce_stream":
value = self.config["client"].as_bool(option) value = self.config["client"].as_bool(option)
self.compact_stream = value self.compact_stream = value
if option == "notify_on_new_message":
value = self.config["client"].as_bool(option)
self.notify_on_new_message = value
if option == "user_interface": if option == "user_interface":
value = value.lower() value = value.lower()
if value == "none": if value == "none":
@ -788,6 +875,19 @@ 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 "max_transfer_size" in self.config["node"]:
self.lxmf_max_propagation_size = 256
else:
value = self.config["node"].as_float("max_transfer_size")
if value < 1:
value = 1
self.lxmf_max_propagation_size = value
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:
@ -805,14 +905,45 @@ class NomadNetworkApp:
if "pages_path" in self.config["node"]: if "pages_path" in self.config["node"]:
self.pagespath = self.config["node"]["pages_path"] self.pagespath = self.config["node"]["pages_path"]
if not "page_refresh_interval" in self.config["node"]:
self.page_refresh_interval = 0
else:
value = self.config["node"].as_int("page_refresh_interval")
if value < 0:
value = 0
self.page_refresh_interval = value
if "files_path" in self.config["node"]: if "files_path" in self.config["node"]:
self.filespath = self.config["node"]["files_path"] self.filespath = self.config["node"]["files_path"]
if not "file_refresh_interval" in self.config["node"]:
self.file_refresh_interval = 0
else:
value = self.config["node"].as_int("file_refresh_interval")
if value < 0:
value = 0
self.file_refresh_interval = value
if "prioritise_destinations" in self.config["node"]: if "prioritise_destinations" in self.config["node"]:
self.prioritised_lxmf_destinations = self.config["node"].as_list("prioritise_destinations") self.prioritised_lxmf_destinations = self.config["node"].as_list("prioritise_destinations")
else: else:
self.prioritised_lxmf_destinations = [] self.prioritised_lxmf_destinations = []
if "static_peers" in self.config["node"]:
self.static_peers = self.config["node"].as_list("static_peers")
else:
self.static_peers = []
if not "max_peers" in self.config["node"]:
self.max_peers = None
else:
value = self.config["node"].as_int("max_peers")
if value < 0:
value = 0
self.max_peers = value
if not "message_storage_limit" in self.config["node"]: if not "message_storage_limit" in self.config["node"]:
self.message_storage_limit = 2000 self.message_storage_limit = 2000
else: else:
@ -907,6 +1038,7 @@ destination = file
enable_client = yes enable_client = yes
user_interface = text user_interface = text
downloads_path = ~/Downloads downloads_path = ~/Downloads
notify_on_new_message = yes
# By default, the peer is announced at startup # By default, the peer is announced at startup
# to let other peers reach it immediately. # to let other peers reach it immediately.
@ -934,6 +1066,31 @@ 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
# You can specify a required stamp cost for
# inbound messages to be accepted. Specifying
# a stamp cost will require untrusted senders
# that message you to include a cryptographic
# stamp in their messages. Performing this
# operation takes the sender an amount of time
# proportional to the stamp cost. As a rough
# estimate, a stamp cost of 8 will take less
# than a second to compute, and a stamp cost
# of 20 could take several minutes, even on
# a fast computer.
required_stamp_cost = None
# You can signal stamp requirements to senders,
# but still accept messages with invalid stamps
# by setting this option to True.
accept_invalid_stamps = False
# The maximum accepted unpacked size for mes-
# sages received directly from other peers,
# specified in kilobytes. Messages larger than
# this will be rejected before the transfer
# begins.
max_accepted_size = 500
# The announce stream will only show one entry # The announce stream will only show one entry
# per destination or node by default. You can # per destination or node by default. You can
# change this to show as many announces as have # change this to show as many announces as have
@ -1003,6 +1160,16 @@ 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
# When Nomad Network is hosting a page-serving
# node, it can 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.
# Due to lots of propagation nodes being
# available, this is currently the default.
disable_propagation = Yes
# 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
@ -1014,6 +1181,18 @@ announce_at_start = Yes
# and defaults to 2 gigabytes. # and defaults to 2 gigabytes.
# message_storage_limit = 2000 # message_storage_limit = 2000
# The maximum accepted transfer size per in-
# coming propagation transfer, in kilobytes.
# This also sets the upper limit for the size
# of single messages accepted onto this node.
#
# If a node wants to propagate a larger number
# of messages to this node, than what can fit
# within this limit, it will prioritise sending
# the smallest, newest messages first, and try
# with any remaining messages at a later point.
max_transfer_size = 256
# You can tell the LXMF message router to # You can tell the LXMF message router to
# prioritise storage for one or more # prioritise storage for one or more
# destinations. If the message store reaches # destinations. If the message store reaches
@ -1023,6 +1202,29 @@ announce_at_start = Yes
# and generally you do not need to use it. # and generally you do not need to use it.
# prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf # prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf
# You can configure the maximum number of other
# propagation nodes that this node will peer
# with automatically. The default is 50.
# max_peers = 25
# You can configure a list of static propagation
# node peers, that this node will always be
# peered with, by specifying a list of
# destination hashes.
# static_peers = e17f833c4ddf8890dd3a79a6fea8161d, 5a2d0029b6e5ec87020abaea0d746da4
# You can specify the interval in minutes for
# rescanning the hosted pages path. By default,
# this option is disabled, and the pages path
# will only be scanned on startup.
# page_refresh_interval = 0
# You can specify the interval in minutes for
# rescanning the hosted files path. By default,
# this option is disabled, and the files path
# will only be scanned on startup.
# file_refresh_interval = 0
[printing] [printing]
# You can configure Nomad Network to print # You can configure Nomad Network to print

View file

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

View file

@ -17,6 +17,8 @@ The following section contains a simple set of fields, and a few different links
-= -=
>>>Text Fields
An input field : `B444`<username`Entered data>`b An input field : `B444`<username`Entered data>`b
An masked field : `B444`<!|password`Value of Field>`b An masked field : `B444`<!|password`Value of Field>`b
@ -27,7 +29,24 @@ Two fields : `B444`<8|one`One>`b `B444`<8|two`Two>`b
The data can be `!`[submitted`:/page/input_fields.mu`username|two]`!. 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]`! >> Checkbox Fields
`B444`<?|sign_up|1|*`>`b Sign me up
>> Radio group
Select your favorite color:
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
>>> Submitting data
You can `!`[submit`:/page/input_fields.mu`one|password|small|color]`! other fields, or just `!`[a single one`:/page/input_fields.mu`username]`!
Or simply `!`[submit them all`:/page/input_fields.mu`*]`!. Or simply `!`[submit them all`:/page/input_fields.mu`*]`!.

View file

@ -51,6 +51,14 @@ THEMES = {
("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"),
("progress_empty", "light gray", "default", "default", "#ddd", "default"), ("progress_empty", "light gray", "default", "default", "#ddd", "default"),
("interface_title", "", "", "default", "", ""),
("interface_title_selected", "bold", "", "bold", "", ""),
("connected_status", "dark green", "default", "default", "dark green", "default"),
("disconnected_status", "dark red", "default", "default", "dark red", "default"),
("placeholder", "dark gray", "default", "default", "dark gray", "default"),
("placeholder_text", "dark gray", "default", "default", "dark gray", "default"),
("error", "light red,blink", "default", "blink", "#f44,blink", "default"),
], ],
}, },
THEME_LIGHT: { THEME_LIGHT: {
@ -69,6 +77,7 @@ THEMES = {
("msg_header_ok", "black", "dark green", "standout", "#111", "#6b2"), ("msg_header_ok", "black", "dark green", "standout", "#111", "#6b2"),
("msg_header_caution", "black", "yellow", "standout", "#111", "#fd3"), ("msg_header_caution", "black", "yellow", "standout", "#111", "#fd3"),
("msg_header_sent", "black", "dark gray", "standout", "#111", "#ddd"), ("msg_header_sent", "black", "dark gray", "standout", "#111", "#ddd"),
("msg_header_propagated", "black", "light blue", "standout", "#111", "#28b"),
("msg_header_delivered", "black", "light blue", "standout", "#111", "#28b"), ("msg_header_delivered", "black", "light blue", "standout", "#111", "#28b"),
("msg_header_failed", "black", "dark gray", "standout", "#000", "#777"), ("msg_header_failed", "black", "dark gray", "standout", "#000", "#777"),
("msg_warning_untrusted", "black", "dark red", "standout", "#111", "dark red"), ("msg_warning_untrusted", "black", "dark red", "standout", "#111", "dark red"),
@ -86,6 +95,13 @@ THEMES = {
("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"),
("progress_empty", "dark gray", "default", "default", "#ddd", "default"), ("progress_empty", "dark gray", "default", "default", "#ddd", "default"),
("interface_title", "dark gray", "default", "default", "#444", "default"),
("interface_title_selected", "dark gray,bold", "default", "bold", "#444,bold", "default"),
("connected_status", "dark green", "default", "default", "#4a0", "default"),
("disconnected_status", "dark red", "default", "default", "#a22", "default"),
("placeholder", "light gray", "default", "default", "#999", "default"),
("placeholder_text", "light gray", "default", "default", "#999", "default"),
("error", "dark red,blink", "default", "blink", "#a22,blink", "default"),
], ],
} }
} }
@ -97,10 +113,10 @@ GLYPHSETS = {
} }
if platform.system() == "Darwin": if platform.system() == "Darwin":
urm_char = " \uf0e0 " urm_char = " \uf0e0"
ur_char = "\uf0e0 " ur_char = "\uf0e0 "
else: else:
urm_char = " \uf003 " urm_char = " \uf003"
ur_char = "\uf003 " ur_char = "\uf003 "
GLYPHS = { GLYPHS = {
@ -115,19 +131,21 @@ GLYPHS = {
("arrow_u", "/\\", "\u2191", "\u2191"), ("arrow_u", "/\\", "\u2191", "\u2191"),
("arrow_d", "\\/", "\u2193", "\u2193"), ("arrow_d", "\\/", "\u2193", "\u2193"),
("warning", "!", "\u26a0", "\uf12a"), ("warning", "!", "\u26a0", "\uf12a"),
("info", "i", "\u2139", "\ufb4d"), ("info", "i", "\u2139", "\U000f064e"),
("unread", "[!]", "\u2709", ur_char), ("unread", "[!]", "\u2709", ur_char),
("divider1", "-", "\u2504", "\u2504"), ("divider1", "-", "\u2504", "\u2504"),
("peer", "[P]", "\u24c5 ", "\uf415"), ("peer", "[P]", "\u24c5 ", "\uf415"),
("node", "[N]", "\u24c3 ", "\uf502"), ("node", "[N]", "\u24c3 ", "\U000f0002"),
("page", "", "\u25a4 ", "\uf719 "), ("page", "", "\u25a4 ", "\uf719 "),
("speed", "", "\u25F7 ", "\uf9c4"), ("speed", "", "\u25F7 ", "\U000f04c5 "),
("decoration_menu", " +", " +", " \uf93a"), ("decoration_menu", " +", " +", " \U000f043b"),
("unread_menu", " !", " \u2709", urm_char), ("unread_menu", " !", " \u2709", urm_char),
("globe", "", "", "\uf484"), ("globe", "", "", "\uf484"),
("sent", "/\\", "\u2191", "\ufbf4"), ("sent", "/\\", "\u2191", "\U000f0cd8"),
("papermsg", "P", "\u25a4", "\uf719"), ("papermsg", "P", "\u25a4", "\uf719"),
("qrcode", "QR", "\u25a4", "\uf029"), ("qrcode", "QR", "\u25a4", "\uf029"),
("selected", "[*] ", "\u25CF", "\u25CF"),
("unselected", "[ ] ", "\u25CB", "\u25CB"),
} }
class TextUI: class TextUI:
@ -163,7 +181,7 @@ class TextUI:
if self.app.config["textui"]["glyphs"] == "plain": if self.app.config["textui"]["glyphs"] == "plain":
glyphset = "plain" glyphset = "plain"
elif self.app.config["textui"]["glyphs"] == "unicoode": elif self.app.config["textui"]["glyphs"] == "unicode":
glyphset = "unicode" glyphset = "unicode"
elif self.app.config["textui"]["glyphs"] == "nerdfont": elif self.app.config["textui"]["glyphs"] == "nerdfont":
glyphset = "nerdfont" glyphset = "nerdfont"

View file

@ -1,11 +1,14 @@
import RNS import RNS
import LXMF
import io
import os import os
import time import time
import urwid import urwid
import shutil
import nomadnet import nomadnet
import subprocess import subprocess
import threading import threading
from .MicronParser import markup_to_attrmaps from .MicronParser import markup_to_attrmaps, make_style, default_state
from nomadnet.Directory import DirectoryEntry from nomadnet.Directory import DirectoryEntry
from nomadnet.vendor.Scrollable import * from nomadnet.vendor.Scrollable import *
@ -27,13 +30,13 @@ class BrowserFrame(urwid.Frame):
self.delegate.save_node_dialog() 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.focus_position == "body":
if key == "down" or key == "up": if key == "down" or key == "up":
try: try:
if hasattr(self.delegate, "page_pile") and self.delegate.page_pile: if hasattr(self.delegate, "page_pile") and self.delegate.page_pile:
def df(loop, user_data): def df(loop, user_data):
st = None st = None
nf = self.delegate.page_pile.get_focus() nf = self.delegate.page_pile.focus
if hasattr(nf, "key_timeout"): if hasattr(nf, "key_timeout"):
st = nf st = nf
elif hasattr(nf, "original_widget"): elif hasattr(nf, "original_widget"):
@ -90,9 +93,14 @@ class Browser:
self.link = None self.link = None
self.loopback = None self.loopback = None
self.status = Browser.DISCONECTED self.status = Browser.DISCONECTED
self.progress_updated_at = None
self.previous_progress = 0
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.page_background_color = None
self.page_foreground_color = None
self.saved_file_name = None self.saved_file_name = None
self.page_data = None self.page_data = None
self.displayed_page_data = None self.displayed_page_data = None
@ -151,10 +159,16 @@ class Browser:
self.browser_footer = self.make_status_widget() self.browser_footer = self.make_status_widget()
self.frame.contents["footer"] = (self.browser_footer, self.frame.options()) self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
self.link_status_showing = False self.link_status_showing = False
if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
self.browser_footer.set_attr_map({None: style_name})
else: else:
self.link_status_showing = True self.link_status_showing = True
self.browser_footer = urwid.AttrMap(urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text("Link to: "+str(link_target))]), "browser_controls") self.browser_footer = urwid.AttrMap(urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text("Link to: "+str(link_target))]), "browser_controls")
self.frame.contents["footer"] = (self.browser_footer, self.frame.options()) self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
self.browser_footer.set_attr_map({None: style_name})
def expand_shorthands(self, destination_type): def expand_shorthands(self, destination_type):
if destination_type == "nnn": if destination_type == "nnn":
@ -179,23 +193,47 @@ class Browser:
else: else:
link_fields.append(e) link_fields.append(e)
def recurse_down(w): def recurse_down(w):
target = None if isinstance(w, list):
if isinstance(w, list): for t in w:
for t in w: recurse_down(t)
recurse_down(t) elif isinstance(w, tuple):
elif isinstance(w, tuple): for t in w:
for t in w: recurse_down(t)
recurse_down(t) elif hasattr(w, "contents"):
elif hasattr(w, "contents"): recurse_down(w.contents)
recurse_down(w.contents) elif hasattr(w, "original_widget"):
elif hasattr(w, "original_widget"): recurse_down(w.original_widget)
recurse_down(w.original_widget) elif hasattr(w, "_original_widget"):
elif hasattr(w, "_original_widget"): recurse_down(w._original_widget)
recurse_down(w._original_widget) else:
else: if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields): field_key = "field_" + w.field_name
request_data["field_"+w.field_name] = w.get_edit_text() if isinstance(w, urwid.Edit):
request_data[field_key] = w.edit_text
elif isinstance(w, urwid.RadioButton):
if w.state:
user_data = getattr(w, "field_value", None)
if user_data is not None:
request_data[field_key] = user_data
elif isinstance(w, urwid.CheckBox):
user_data = getattr(w, "field_value", "1")
if w.state:
existing_value = request_data.get(field_key, '')
if existing_value:
# Concatenate the new value with the existing one
request_data[field_key] = existing_value + ',' + user_data
else:
# Initialize the field with the current value
request_data[field_key] = user_data
else:
pass # do nothing if checkbox is not check
recurse_down(self.attr_maps) recurse_down(self.attr_maps)
RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG) RNS.log("Including request data: "+str(request_data), RNS.LOG_DEBUG)
@ -252,7 +290,7 @@ class Browser:
display_name = None display_name = None
if display_name_data != None: if display_name_data != None:
display_name = display_name_data.decode("utf-8") display_name = LXMF.display_name_from_app_data(display_name_data)
if not source_hash_text in [c[0] for c in existing_conversations]: if not source_hash_text in [c[0] for c in existing_conversations]:
entry = DirectoryEntry(bytes.fromhex(source_hash_text), display_name=display_name) entry = DirectoryEntry(bytes.fromhex(source_hash_text), display_name=display_name)
@ -279,7 +317,10 @@ class Browser:
self.browser_footer = urwid.Text("") self.browser_footer = urwid.Text("")
self.page_pile = None self.page_pile = None
self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle") self.browser_body = urwid.Filler(
urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align=urwid.CENTER),
urwid.MIDDLE,
)
self.frame = BrowserFrame(self.browser_body, header=self.browser_header, footer=self.browser_footer) self.frame = BrowserFrame(self.browser_body, header=self.browser_header, footer=self.browser_footer)
self.frame.delegate = self self.frame.delegate = self
@ -288,7 +329,14 @@ class Browser:
def make_status_widget(self): def make_status_widget(self):
if self.response_progress > 0: if self.response_progress > 0:
pb = ResponseProgressBar("progress_empty" , "progress_full", current=self.response_progress, done=1.0, satt=None) if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
style_name_inverted = make_style(default_state(bg=self.page_foreground_color, fg=self.page_background_color))
else:
style_name = "progress_empty"
style_name_inverted = "progress_full"
pb = ResponseProgressBar(style_name , style_name_inverted, current=self.response_progress, done=1.0, satt=None, owner=self)
widget = urwid.Pile([urwid.Divider(self.g["divider1"]), pb]) widget = urwid.Pile([urwid.Divider(self.g["divider1"]), pb])
else: else:
widget = urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text(self.status_text())]) widget = urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text(self.status_text())])
@ -306,34 +354,39 @@ class Browser:
self.update_display() self.update_display()
columns = urwid.Columns([ columns = urwid.Columns([
("weight", 0.5, urwid.Text(" ")), (urwid.WEIGHT, 0.5, urwid.Text(" ")),
(8, urwid.Button("Back", on_press=back_action)), (8, urwid.Button("Back", on_press=back_action)),
("weight", 0.5, urwid.Text(" ")) (urwid.WEIGHT, 0.5, urwid.Text(" ")),
]) ])
if len(self.attr_maps) > 0: if len(self.attr_maps) > 0:
pile = urwid.Pile([ pile = urwid.Pile([
urwid.Text("!\n\n"+self.status_text()+"\n", align="center"), urwid.Text("!\n\n"+self.status_text()+"\n", align=urwid.CENTER),
columns columns
]) ])
else: else:
pile = urwid.Pile([ pile = urwid.Pile([urwid.Text("!\n\n"+self.status_text(), align=urwid.CENTER)])
urwid.Text("!\n\n"+self.status_text(), align="center")
])
return urwid.Filler(pile, "middle") return urwid.Filler(pile, urwid.MIDDLE)
def update_display(self): def update_display(self):
if self.status == Browser.DISCONECTED: if self.status == Browser.DISCONECTED:
self.display_widget.set_attr_map({None: "inactive_text"}) self.display_widget.set_attr_map({None: "inactive_text"})
self.page_pile = None self.page_pile = None
self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle") self.browser_body = urwid.Filler(
urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align=urwid.CENTER),
urwid.MIDDLE,
)
self.browser_footer = urwid.Text("") self.browser_footer = urwid.Text("")
self.browser_header = urwid.Text("") self.browser_header = urwid.Text("")
self.linebox.set_title("Remote Node") self.linebox.set_title("Remote Node")
else: else:
self.display_widget.set_attr_map({None: "body_text"}) self.display_widget.set_attr_map({None: "body_text"})
self.browser_header = self.make_control_widget() self.browser_header = self.make_control_widget()
if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
self.browser_header.set_attr_map({None: style_name})
if self.destination_hash != None: if self.destination_hash != None:
remote_display_string = self.app.directory.simplest_display_str(self.destination_hash) remote_display_string = self.app.directory.simplest_display_str(self.destination_hash)
else: else:
@ -348,16 +401,32 @@ class Browser:
self.browser_footer = self.make_status_widget() self.browser_footer = self.make_status_widget()
self.update_page_display() self.update_page_display()
if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
self.browser_body.set_attr_map({None: style_name})
self.browser_footer.set_attr_map({None: style_name})
self.browser_header.set_attr_map({None: style_name})
self.display_widget.set_attr_map({None: style_name})
elif self.status == Browser.LINK_TIMEOUT: elif self.status == Browser.LINK_TIMEOUT:
self.browser_body = self.make_request_failed_widget() self.browser_body = self.make_request_failed_widget()
self.browser_footer = urwid.Text("") self.browser_footer = urwid.Text("")
elif self.status <= Browser.REQUEST_SENT: elif self.status <= Browser.REQUEST_SENT:
if len(self.attr_maps) == 0: if len(self.attr_maps) == 0:
self.browser_body = urwid.Filler(urwid.Text("Retrieving\n["+self.current_url()+"]", align="center"), "middle") self.browser_body = urwid.Filler(
urwid.Text("Retrieving\n["+self.current_url()+"]", align=urwid.CENTER),
urwid.MIDDLE,
)
self.browser_footer = self.make_status_widget() self.browser_footer = self.make_status_widget()
if self.page_background_color != None or self.page_foreground_color != None:
style_name = make_style(default_state(fg=self.page_foreground_color, bg=self.page_background_color))
self.browser_footer.set_attr_map({None: style_name})
self.browser_header.set_attr_map({None: style_name})
self.display_widget.set_attr_map({None: style_name})
elif self.status == Browser.REQUEST_FAILED: elif self.status == Browser.REQUEST_FAILED:
self.browser_body = self.make_request_failed_widget() self.browser_body = self.make_request_failed_widget()
self.browser_footer = urwid.Text("") self.browser_footer = urwid.Text("")
@ -393,6 +462,9 @@ class Browser:
self.attr_maps = [] self.attr_maps = []
self.status = Browser.DISCONECTED self.status = Browser.DISCONECTED
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
@ -503,6 +575,9 @@ class Browser:
self.status = Browser.DONE self.status = Browser.DONE
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
except Exception as e: except Exception as e:
RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -517,7 +592,7 @@ class Browser:
self.status = Browser.PATH_REQUESTED self.status = Browser.PATH_REQUESTED
self.update_display() self.update_display()
pr_time = time.time() pr_time = time.time()+RNS.Transport.first_hop_timeout(self.destination_hash)
while not RNS.Transport.has_path(self.destination_hash): while not RNS.Transport.has_path(self.destination_hash):
now = time.time() now = time.time()
if now > pr_time+self.timeout: if now > pr_time+self.timeout:
@ -552,6 +627,9 @@ class Browser:
# Send the request # Send the request
self.status = Browser.REQUESTING self.status = Browser.REQUESTING
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.saved_file_name = None self.saved_file_name = None
@ -608,7 +686,7 @@ class Browser:
self.load_page() self.load_page()
def close_dialogs(self): def close_dialogs(self):
options = self.delegate.columns.options("weight", self.delegate.right_area_width) options = self.delegate.columns.options(urwid.WEIGHT, self.delegate.right_area_width)
self.delegate.columns.contents[1] = (self.display_widget, options) self.delegate.columns.contents[1] = (self.display_widget, options)
def url_dialog(self): def url_dialog(self):
@ -629,7 +707,11 @@ class Browser:
dialog = UrlDialogLineBox( dialog = UrlDialogLineBox(
urwid.Pile([ urwid.Pile([
e_url, e_url,
urwid.Columns([("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Go", on_press=confirmed))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Cancel", on_press=dismiss_dialog)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Go", on_press=confirmed)),
])
]), title="Enter URL" ]), title="Enter URL"
) )
e_url.confirmed = confirmed e_url.confirmed = confirmed
@ -637,46 +719,72 @@ class Browser:
dialog.delegate = self dialog.delegate = self
bottom = self.display_widget bottom = self.display_widget
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 65), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=(urwid.RELATIVE, 65),
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.delegate.columns.options("weight", self.delegate.right_area_width) options = self.delegate.columns.options(urwid.WEIGHT, self.delegate.right_area_width)
self.delegate.columns.contents[1] = (overlay, options) self.delegate.columns.contents[1] = (overlay, options)
self.delegate.columns.focus_position = 1 self.delegate.columns.focus_position = 1
def save_node_dialog(self): def save_node_dialog(self):
def dismiss_dialog(sender): if self.destination_hash != None:
self.close_dialogs() try:
def dismiss_dialog(sender):
self.close_dialogs()
display_name = RNS.Identity.recall_app_data(self.destination_hash) display_name = RNS.Identity.recall_app_data(self.destination_hash)
disp_str = "" disp_str = ""
if display_name != None: if display_name != None:
display_name = display_name.decode("utf-8") display_name = display_name.decode("utf-8")
disp_str = " \""+display_name+"\"" disp_str = " \""+display_name+"\""
def confirmed(sender): def confirmed(sender):
node_entry = DirectoryEntry(self.destination_hash, display_name=display_name, hosts_node=True) node_entry = DirectoryEntry(self.destination_hash, display_name=display_name, hosts_node=True)
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()
self.close_dialogs() self.close_dialogs()
dialog = UrlDialogLineBox( dialog = UrlDialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Save connected node"+disp_str+" "+RNS.prettyhexrep(self.destination_hash)+" to Known Nodes?\n"), urwid.Text("Save connected node"+disp_str+" "+RNS.prettyhexrep(self.destination_hash)+" to Known Nodes?\n"),
urwid.Columns([("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Save", on_press=confirmed))]) urwid.Columns([
]), title="Save Node" (urwid.WEIGHT, 0.45, urwid.Button("Cancel", on_press=dismiss_dialog)),
) (urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=confirmed)),
])
]), title="Save Node"
)
dialog.confirmed = confirmed dialog.confirmed = confirmed
dialog.delegate = self dialog.delegate = self
bottom = self.display_widget bottom = self.display_widget
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 50), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=(urwid.RELATIVE, 50),
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.delegate.columns.options("weight", self.delegate.right_area_width) options = self.delegate.columns.options(urwid.WEIGHT, self.delegate.right_area_width)
self.delegate.columns.contents[1] = (overlay, options) self.delegate.columns.contents[1] = (overlay, options)
self.delegate.columns.focus_position = 1 self.delegate.columns.focus_position = 1
except Exception as e:
pass
def load_page(self): def load_page(self):
if self.request_data == None: if self.request_data == None:
@ -688,9 +796,29 @@ class Browser:
self.status = Browser.DONE self.status = Browser.DONE
self.page_data = cached self.page_data = cached
self.markup = self.page_data.decode("utf-8") self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.page_background_color = None
bgpos = self.markup.find("#!bg=")
if bgpos:
endpos = self.markup.find("\n", bgpos)
if endpos-(bgpos+5) == 3:
bg = self.markup[bgpos+5:endpos]
self.page_background_color = bg
self.page_foreground_color = None
fgpos = self.markup.find("#!fg=")
if fgpos:
endpos = self.markup.find("\n", fgpos)
if endpos-(fgpos+5) == 3:
fg = self.markup[fgpos+5:endpos]
self.page_foreground_color = fg
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.saved_file_name = None self.saved_file_name = None
@ -735,9 +863,29 @@ class Browser:
self.status = Browser.DONE self.status = Browser.DONE
self.page_data = page_data self.page_data = page_data
self.markup = self.page_data.decode("utf-8") self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.page_background_color = None
bgpos = self.markup.find("#!bg=")
if bgpos:
endpos = self.markup.find("\n", bgpos)
if endpos-(bgpos+5) == 3:
bg = self.markup[bgpos+5:endpos]
self.page_background_color = bg
self.page_foreground_color = None
fgpos = self.markup.find("#!fg=")
if fgpos:
endpos = self.markup.find("\n", fgpos)
if endpos-(fgpos+5) == 3:
fg = self.markup[fgpos+5:endpos]
self.page_foreground_color = fg
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.saved_file_name = None self.saved_file_name = None
@ -770,7 +918,7 @@ class Browser:
self.status = Browser.PATH_REQUESTED self.status = Browser.PATH_REQUESTED
self.update_display() self.update_display()
pr_time = time.time() pr_time = time.time()+RNS.Transport.first_hop_timeout(self.destination_hash)
while not RNS.Transport.has_path(self.destination_hash): while not RNS.Transport.has_path(self.destination_hash):
now = time.time() now = time.time()
if now > pr_time+self.timeout: if now > pr_time+self.timeout:
@ -804,6 +952,9 @@ class Browser:
# Send the request # Send the request
self.status = Browser.REQUESTING self.status = Browser.REQUESTING
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.saved_file_name = None self.saved_file_name = None
@ -849,6 +1000,9 @@ class Browser:
def link_establishment_timeout(self): def link_establishment_timeout(self):
self.status = Browser.LINK_TIMEOUT self.status = Browser.LINK_TIMEOUT
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.link = None self.link = None
@ -861,8 +1015,28 @@ class Browser:
self.status = Browser.DONE self.status = Browser.DONE
self.page_data = request_receipt.response self.page_data = request_receipt.response
self.markup = self.page_data.decode("utf-8") self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.page_background_color = None
bgpos = self.markup.find("#!bg=")
if bgpos:
endpos = self.markup.find("\n", bgpos)
if endpos-(bgpos+5) == 3:
bg = self.markup[bgpos+5:endpos]
self.page_background_color = bg
self.page_foreground_color = None
fgpos = self.markup.find("#!fg=")
if fgpos:
endpos = self.markup.find("\n", fgpos)
if endpos-(fgpos+5) == 3:
fg = self.markup[fgpos+5:endpos]
self.page_foreground_color = fg
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.loaded_from_cache = False self.loaded_from_cache = False
# Simple header handling. Should be expanded when more # Simple header handling. Should be expanded when more
@ -974,22 +1148,41 @@ class Browser:
def file_received(self, request_receipt): def file_received(self, request_receipt):
try: try:
file_name = request_receipt.response[0] if type(request_receipt.response) == io.BufferedReader:
file_data = request_receipt.response[1] if request_receipt.metadata != None:
file_destination = self.app.downloads_path+"/"+file_name file_name = os.path.basename(request_receipt.metadata["name"].decode("utf-8"))
file_handle = request_receipt.response
file_destination = self.app.downloads_path+"/"+file_name
counter = 0 counter = 0
while os.path.isfile(file_destination): while os.path.isfile(file_destination):
counter += 1 counter += 1
file_destination = self.app.downloads_path+"/"+file_name+"."+str(counter) file_destination = self.app.downloads_path+"/"+file_name+"."+str(counter)
fh = open(file_destination, "wb") shutil.move(file_handle.name, file_destination)
fh.write(file_data)
fh.close() else:
file_name = request_receipt.response[0]
file_data = request_receipt.response[1]
file_destination_name = os.path.basename(file_name)
file_destination = self.app.downloads_path+"/"+file_destination_name
counter = 0
while os.path.isfile(file_destination):
counter += 1
file_destination = self.app.downloads_path+"/"+file_destination_name+"."+str(counter)
fh = open(file_destination, "wb")
fh.write(file_data)
fh.close()
self.saved_file_name = file_destination.replace(self.app.downloads_path+"/", "", 1)
self.saved_file_name = file_destination.replace(self.app.downloads_path+"/", "", 1)
self.status = Browser.DONE self.status = Browser.DONE
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.update_display() self.update_display()
except Exception as e: except Exception as e:
@ -1001,6 +1194,9 @@ class Browser:
if request_receipt.request_id == self.last_request_id: if request_receipt.request_id == self.last_request_id:
self.status = Browser.REQUEST_FAILED self.status = Browser.REQUEST_FAILED
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
@ -1013,6 +1209,9 @@ class Browser:
else: else:
self.status = Browser.REQUEST_FAILED self.status = Browser.REQUEST_FAILED
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
@ -1027,6 +1226,9 @@ class Browser:
def request_timeout(self, request_receipt=None): def request_timeout(self, request_receipt=None):
self.status = Browser.REQUEST_TIMEOUT self.status = Browser.REQUEST_TIMEOUT
self.response_progress = 0 self.response_progress = 0
self.response_speed = None
self.progress_updated_at = None
self.previous_progress = 0
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
@ -1043,6 +1245,17 @@ class Browser:
self.response_time = request_receipt.get_response_time() self.response_time = request_receipt.get_response_time()
self.response_size = request_receipt.response_size self.response_size = request_receipt.response_size
self.response_transfer_size = request_receipt.response_transfer_size self.response_transfer_size = request_receipt.response_transfer_size
now = time.time()
if self.progress_updated_at == None: self.progress_updated_at = now
if now > self.progress_updated_at+1:
td = now - self.progress_updated_at
pd = self.response_progress - self.previous_progress
bd = pd*self.response_size
self.response_speed = (bd/td)*8
self.previous_progress = self.response_progress
self.progress_updated_at = now
self.update_display() self.update_display()
@ -1093,8 +1306,14 @@ class Browser:
class ResponseProgressBar(urwid.ProgressBar): class ResponseProgressBar(urwid.ProgressBar):
def __init__(self, empty, full, current=None, done=None, satt=None, owner=None):
super().__init__(empty, full, current=current, done=done, satt=satt)
self.owner = owner
def get_text(self): def get_text(self):
return "Receiving response "+super().get_text() if self.owner.response_speed: speed_str = " "+RNS.prettyspeed(self.owner.response_speed)
else: speed_str = ""
return "Receiving response "+super().get_text().replace(" %", "%")+speed_str
# A convenience function for printing a human- # A convenience function for printing a human-
# readable file size # readable file size

View file

@ -12,13 +12,13 @@ class ConfigDisplayShortcuts():
class ConfigFiller(urwid.WidgetWrap): class ConfigFiller(urwid.WidgetWrap):
def __init__(self, widget, app): def __init__(self, widget, app):
self.app = app self.app = app
self.filler = urwid.Filler(widget, "top") self.filler = urwid.Filler(widget, urwid.TOP)
urwid.WidgetWrap.__init__(self, self.filler) super().__init__(self.filler)
def keypress(self, size, key): def keypress(self, size, key):
if key == "up": if key == "up":
self.app.ui.main_display.frame.set_focus("header") self.app.ui.main_display.frame.focus_position = "header"
return super(ConfigFiller, self).keypress(size, key) return super(ConfigFiller, self).keypress(size, key)
@ -31,12 +31,20 @@ class ConfigDisplay():
self.editor_term = EditorTerminal(self.app, self) self.editor_term = EditorTerminal(self.app, self)
self.widget = urwid.LineBox(self.editor_term) self.widget = urwid.LineBox(self.editor_term)
self.app.ui.main_display.update_active_sub_display() self.app.ui.main_display.update_active_sub_display()
self.app.ui.main_display.frame.set_focus("body") self.app.ui.main_display.frame.focus_position = "body"
self.editor_term.term.change_focus(True) self.editor_term.term.change_focus(True)
pile = urwid.Pile([ pile = urwid.Pile([
urwid.Text(("body_text", "\nTo change the configuration, edit the config file located at:\n\n"+self.app.configpath+"\n\nRestart Nomad Network for changes to take effect\n"), align="center"), urwid.Text(
urwid.Padding(urwid.Button("Open Editor", on_press=open_editor), width=15, align="center"), (
"body_text",
"\nTo change the configuration, edit the config file located at:\n\n"
+self.app.configpath
+"\n\nRestart Nomad Network for changes to take effect\n",
),
align=urwid.CENTER,
),
urwid.Padding(urwid.Button("Open Editor", on_press=open_editor), width=15, align=urwid.CENTER),
]) ])
self.config_explainer = ConfigFiller(pile, self.app) self.config_explainer = ConfigFiller(pile, self.app)
@ -71,11 +79,11 @@ class EditorTerminal(urwid.WidgetWrap):
urwid.connect_signal(self.term, 'closed', quit_term) urwid.connect_signal(self.term, 'closed', quit_term)
urwid.WidgetWrap.__init__(self, self.term) super().__init__(self.term)
def keypress(self, size, key): def keypress(self, size, key):
# TODO: Decide whether there should be a way to get out while editing # TODO: Decide whether there should be a way to get out while editing
#if key == "up": #if key == "up":
# nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") # nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
return super(EditorTerminal, self).keypress(size, key) return super(EditorTerminal, self).keypress(size, key)

View file

@ -37,9 +37,9 @@ class ConversationsArea(urwid.LineBox):
elif key == "ctrl g": elif key == "ctrl g":
self.delegate.toggle_fullscreen() self.delegate.toggle_fullscreen()
elif key == "tab": elif key == "tab":
self.delegate.app.ui.main_display.frame.set_focus("header") self.delegate.app.ui.main_display.frame.focus_position = "header"
elif key == "up" and (self.delegate.ilb.first_item_is_selected() or self.delegate.ilb.body_is_empty()): elif key == "up" and (self.delegate.ilb.first_item_is_selected() or self.delegate.ilb.body_is_empty()):
self.delegate.app.ui.main_display.frame.set_focus("header") self.delegate.app.ui.main_display.frame.focus_position = "header"
else: else:
return super(ConversationsArea, self).keypress(size, key) return super(ConversationsArea, self).keypress(size, key)
@ -69,10 +69,10 @@ class ConversationsDisplay():
self.columns_widget = urwid.Columns( self.columns_widget = urwid.Columns(
[ [
# ("weight", ConversationsDisplay.list_width, self.listbox), # (urwid.WEIGHT, ConversationsDisplay.list_width, self.listbox),
# ("weight", 1-ConversationsDisplay.list_width, self.make_conversation_widget(None)) # (urwid.WEIGHT, 1-ConversationsDisplay.list_width, self.make_conversation_widget(None))
(ConversationsDisplay.given_list_width, self.listbox), (ConversationsDisplay.given_list_width, self.listbox),
("weight", 1, self.make_conversation_widget(None)) (urwid.WEIGHT, 1, self.make_conversation_widget(None))
], ],
dividechars=0, focus_column=0, box_columns=[0] dividechars=0, focus_column=0, box_columns=[0]
) )
@ -105,7 +105,7 @@ class ConversationsDisplay():
highlight_offFocus="list_off_focus" highlight_offFocus="list_off_focus"
) )
self.listbox = ConversationsArea(urwid.Filler(self.ilb, height=("relative", 100)), title="Conversations") self.listbox = ConversationsArea(urwid.Filler(self.ilb, height=urwid.RELATIVE_100), title="Conversations")
self.listbox.delegate = self self.listbox.delegate = self
def delete_selected_conversation(self): def delete_selected_conversation(self):
@ -127,17 +127,33 @@ class ConversationsDisplay():
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Delete conversation with\n"+self.app.directory.simplest_display_str(bytes.fromhex(source_hash))+"\n", align="center"), urwid.Text(
urwid.Columns([("weight", 0.45, urwid.Button("Yes", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("No", on_press=dismiss_dialog))]) "Delete conversation with\n"+self.app.directory.simplest_display_str(bytes.fromhex(source_hash))+"\n",
align=urwid.CENTER,
),
urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Yes", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("No", on_press=dismiss_dialog)),
])
]), title="?" ]), title="?"
) )
dialog.delegate = self dialog.delegate = self
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
# options = self.columns_widget.options("weight", ConversationsDisplay.list_width) # options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def edit_selected_in_directory(self): def edit_selected_in_directory(self):
@ -223,9 +239,12 @@ class ConversationsDisplay():
RNS.log("Could not save directory entry. The contained exception was: "+str(e), RNS.LOG_VERBOSE) RNS.log("Could not save directory entry. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
if not dialog_pile.error_display: if not dialog_pile.error_display:
dialog_pile.error_display = True dialog_pile.error_display = True
options = dialog_pile.options(height_type="pack") options = dialog_pile.options(height_type=urwid.PACK)
dialog_pile.contents.append((urwid.Text(""), options)) dialog_pile.contents.append((urwid.Text(""), options))
dialog_pile.contents.append((urwid.Text(("error_text", "Could not save entry. Check your input."), align="center"), options)) dialog_pile.contents.append((
urwid.Text(("error_text", "Could not save entry. Check your input."), align=urwid.CENTER),
options,)
)
source_is_known = self.app.directory.is_known(bytes.fromhex(source_hash_text)) source_is_known = self.app.directory.is_known(bytes.fromhex(source_hash_text))
if source_is_known: if source_is_known:
@ -234,13 +253,23 @@ class ConversationsDisplay():
def query_action(sender, user_data): def query_action(sender, user_data):
self.close_conversation_by_hash(user_data) self.close_conversation_by_hash(user_data)
nomadnet.Conversation.query_for_peer(user_data) nomadnet.Conversation.query_for_peer(user_data)
options = dialog_pile.options(height_type="pack") options = dialog_pile.options(height_type=urwid.PACK)
dialog_pile.contents = [ dialog_pile.contents = [
(urwid.Text("Query sent"), options), (urwid.Text("Query sent"), options),
(urwid.Button("OK", on_press=dismiss_dialog), options) (urwid.Button("OK", on_press=dismiss_dialog), options)
] ]
query_button = urwid.Button("Query network for keys", on_press=query_action, user_data=source_hash_text) query_button = urwid.Button("Query network for keys", on_press=query_action, user_data=source_hash_text)
known_section = urwid.Pile([urwid.Divider(g["divider1"]), urwid.Text(g["info"]+"\n", align="center"), urwid.Text("The identity of this peer is not known, and you cannot currently send messages to it. You can query the network to obtain the identity.\n", align="center"), query_button, urwid.Divider(g["divider1"])]) known_section = urwid.Pile([
urwid.Divider(g["divider1"]),
urwid.Text(g["info"]+"\n", align=urwid.CENTER),
urwid.Text(
"The identity of this peer is not known, and you cannot currently send messages to it. "
"You can query the network to obtain the identity.\n",
align=urwid.CENTER,
),
query_button,
urwid.Divider(g["divider1"]),
])
dialog_pile = urwid.Pile([ dialog_pile = urwid.Pile([
selected_id_widget, selected_id_widget,
@ -253,7 +282,11 @@ class ConversationsDisplay():
r_direct, r_direct,
r_propagated, r_propagated,
known_section, known_section,
urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Back", on_press=dismiss_dialog)),
])
]) ])
dialog_pile.error_display = False dialog_pile.error_display = False
@ -261,10 +294,19 @@ class ConversationsDisplay():
dialog.delegate = self dialog.delegate = self
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
# options = self.columns_widget.options("weight", ConversationsDisplay.list_width) # options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def new_conversation(self): def new_conversation(self):
@ -312,9 +354,15 @@ class ConversationsDisplay():
RNS.log("Could not start conversation. The contained exception was: "+str(e), RNS.LOG_VERBOSE) RNS.log("Could not start conversation. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
if not dialog_pile.error_display: if not dialog_pile.error_display:
dialog_pile.error_display = True dialog_pile.error_display = True
options = dialog_pile.options(height_type="pack") options = dialog_pile.options(height_type=urwid.PACK)
dialog_pile.contents.append((urwid.Text(""), options)) dialog_pile.contents.append((urwid.Text(""), options))
dialog_pile.contents.append((urwid.Text(("error_text", "Could not start conversation. Check your input."), align="center"), options)) dialog_pile.contents.append((
urwid.Text(
("error_text", "Could not start conversation. Check your input."),
align=urwid.CENTER,
),
options,
))
dialog_pile = urwid.Pile([ dialog_pile = urwid.Pile([
e_id, e_id,
@ -324,7 +372,11 @@ class ConversationsDisplay():
r_unknown, r_unknown,
r_trusted, r_trusted,
urwid.Text(""), urwid.Text(""),
urwid.Columns([("weight", 0.45, urwid.Button("Create", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Create", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Back", on_press=dismiss_dialog)),
])
]) ])
dialog_pile.error_display = False dialog_pile.error_display = False
@ -332,10 +384,19 @@ class ConversationsDisplay():
dialog.delegate = self dialog.delegate = self
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
# options = self.columns_widget.options("weight", ConversationsDisplay.list_width) # options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def ingest_lxm_uri(self): def ingest_lxm_uri(self):
@ -366,7 +427,10 @@ class ConversationsDisplay():
rdialog_pile = urwid.Pile([ rdialog_pile = urwid.Pile([
urwid.Text("Message was decoded, decrypted successfully, and added to your conversation list."), urwid.Text("Message was decoded, decrypted successfully, and added to your conversation list."),
urwid.Text(""), urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.6, urwid.Text("")),
(urwid.WEIGHT, 0.4, urwid.Button("OK", on_press=dismiss_dialog)),
])
]) ])
rdialog_pile.error_display = False rdialog_pile.error_display = False
@ -374,16 +438,28 @@ class ConversationsDisplay():
rdialog.delegate = self rdialog.delegate = self
bottom = self.listbox bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) roverlay = urwid.Overlay(
rdialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options) self.columns_widget.contents[0] = (roverlay, options)
elif ingest_result == duplicate_signal: elif ingest_result == duplicate_signal:
rdialog_pile = urwid.Pile([ rdialog_pile = urwid.Pile([
urwid.Text("The decoded message has already been processed by the LXMF Router, and will not be ingested again."), urwid.Text("The decoded message has already been processed by the LXMF Router, and will not be ingested again."),
urwid.Text(""), urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.6, urwid.Text("")),
(urwid.WEIGHT, 0.4, urwid.Button("OK", on_press=dismiss_dialog)),
])
]) ])
rdialog_pile.error_display = False rdialog_pile.error_display = False
@ -391,9 +467,18 @@ class ConversationsDisplay():
rdialog.delegate = self rdialog.delegate = self
bottom = self.listbox bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) roverlay = urwid.Overlay(
rdialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options) self.columns_widget.contents[0] = (roverlay, options)
else: else:
@ -405,7 +490,10 @@ class ConversationsDisplay():
rdialog_pile = urwid.Pile([ rdialog_pile = urwid.Pile([
urwid.Text(propagation_text), urwid.Text(propagation_text),
urwid.Text(""), urwid.Text(""),
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.6, urwid.Text("")),
(urwid.WEIGHT, 0.4, urwid.Button("OK", on_press=dismiss_dialog)),
])
]) ])
rdialog_pile.error_display = False rdialog_pile.error_display = False
@ -413,23 +501,36 @@ class ConversationsDisplay():
rdialog.delegate = self rdialog.delegate = self
bottom = self.listbox bottom = self.listbox
roverlay = urwid.Overlay(rdialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) roverlay = urwid.Overlay(
rdialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (roverlay, options) self.columns_widget.contents[0] = (roverlay, options)
except Exception as e: except Exception as e:
RNS.log("Could not ingest LXM URI. The contained exception was: "+str(e), RNS.LOG_VERBOSE) RNS.log("Could not ingest LXM URI. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
if not dialog_pile.error_display: if not dialog_pile.error_display:
dialog_pile.error_display = True dialog_pile.error_display = True
options = dialog_pile.options(height_type="pack") options = dialog_pile.options(height_type=urwid.PACK)
dialog_pile.contents.append((urwid.Text(""), options)) dialog_pile.contents.append((urwid.Text(""), options))
dialog_pile.contents.append((urwid.Text(("error_text", "Could ingest LXM from URI data. Check your input."), align="center"), options)) dialog_pile.contents.append((urwid.Text(("error_text", "Could ingest LXM from URI data. Check your input."), align=urwid.CENTER), options))
dialog_pile = urwid.Pile([ dialog_pile = urwid.Pile([
e_uri, e_uri,
urwid.Text(""), urwid.Text(""),
urwid.Columns([("weight", 0.45, urwid.Button("Ingest", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Ingest", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Back", on_press=dismiss_dialog)),
])
]) ])
dialog_pile.error_display = False dialog_pile.error_display = False
@ -437,9 +538,18 @@ class ConversationsDisplay():
dialog.delegate = self dialog.delegate = self
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def delete_conversation(self, source_hash): def delete_conversation(self, source_hash):
@ -471,7 +581,7 @@ class ConversationsDisplay():
r_mall = urwid.RadioButton(max_messages_group, "Download all", state=True) r_mall = urwid.RadioButton(max_messages_group, "Download all", state=True)
r_mlim = urwid.RadioButton(max_messages_group, "Limit to", state=False) r_mlim = urwid.RadioButton(max_messages_group, "Limit to", state=False)
ie_lim = urwid.IntEdit("", 5) ie_lim = urwid.IntEdit("", 5)
rbs = urwid.GridFlow([r_mlim, ie_lim], 12, 1, 0, align="left") rbs = urwid.GridFlow([r_mlim, ie_lim], 12, 1, 0, align=urwid.LEFT)
def sync_now(sender): def sync_now(sender):
limit = None limit = None
@ -495,7 +605,11 @@ class ConversationsDisplay():
else: else:
sync_button = hidden_sync_button sync_button = hidden_sync_button
button_columns = urwid.Columns([("weight", 0.45, sync_button), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, cancel_button)]) button_columns = urwid.Columns([
(urwid.WEIGHT, 0.45, sync_button),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, cancel_button),
])
real_sync_button.bc = button_columns real_sync_button.bc = button_columns
pn_ident = None pn_ident = None
@ -518,7 +632,7 @@ class ConversationsDisplay():
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text(""+g["node"]+pn_display_str, align="center"), urwid.Text(""+g["node"]+pn_display_str, align=urwid.CENTER),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
sync_progress, sync_progress,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
@ -529,12 +643,23 @@ class ConversationsDisplay():
]), title="Message Sync" ]), title="Message Sync"
) )
else: else:
button_columns = urwid.Columns([("weight", 0.45, urwid.Text("" )), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, cancel_button)]) button_columns = urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Text("" )),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, cancel_button),
])
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text(""), urwid.Text(""),
urwid.Text("No trusted nodes found, cannot sync!\n", align="center"), urwid.Text("No trusted nodes found, cannot sync!\n", align=urwid.CENTER),
urwid.Text("To syncronise messages from the network, one or more nodes must be marked as trusted in the Known Nodes list, or a node must manually be selected as the default propagation node. Nomad Network will then automatically sync from the nearest trusted node, or the manually selected one.", align="left"), urwid.Text(
"To synchronise messages from the network, "
"one or more nodes must be marked as trusted in the Known Nodes list, "
"or a node must manually be selected as the default propagation node. "
"Nomad Network will then automatically sync from the nearest trusted node, "
"or the manually selected one.",
align=urwid.LEFT,
),
urwid.Text(""), urwid.Text(""),
button_columns button_columns
]), title="Message Sync" ]), title="Message Sync"
@ -550,10 +675,19 @@ class ConversationsDisplay():
self.sync_dialog = dialog self.sync_dialog = dialog
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
# options = self.columns_widget.options("weight", ConversationsDisplay.list_width) # options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
def update_sync_dialog(self, loop = None, sender = None): def update_sync_dialog(self, loop = None, sender = None):
@ -561,9 +695,9 @@ class ConversationsDisplay():
self.sync_dialog.sync_progress.set_completion(self.app.get_sync_progress()) self.sync_dialog.sync_progress.set_completion(self.app.get_sync_progress())
if self.app.get_sync_status() == "Idle" or self.app.message_router.propagation_transfer_state >= LXMF.LXMRouter.PR_COMPLETE: if self.app.get_sync_status() == "Idle" or self.app.message_router.propagation_transfer_state >= LXMF.LXMRouter.PR_COMPLETE:
self.sync_dialog.bc.contents[0] = (self.sync_dialog.real_sync_button, self.sync_dialog.bc.options("weight", 0.45)) self.sync_dialog.bc.contents[0] = (self.sync_dialog.real_sync_button, self.sync_dialog.bc.options(urwid.WEIGHT, 0.45))
else: else:
self.sync_dialog.bc.contents[0] = (self.sync_dialog.hidden_sync_button, self.sync_dialog.bc.options("weight", 0.45)) self.sync_dialog.bc.contents[0] = (self.sync_dialog.hidden_sync_button, self.sync_dialog.bc.options(urwid.WEIGHT, 0.45))
self.app.ui.loop.set_alarm_in(0.2, self.update_sync_dialog) self.app.ui.loop.set_alarm_in(0.2, self.update_sync_dialog)
@ -574,13 +708,22 @@ class ConversationsDisplay():
def update_conversation_list(self): def update_conversation_list(self):
ilb_position = self.ilb.get_selected_position() ilb_position = self.ilb.get_selected_position()
self.update_listbox() self.update_listbox()
# options = self.columns_widget.options("weight", ConversationsDisplay.list_width) # options = self.columns_widget.options(urwid.WEIGHT, ConversationsDisplay.list_width)
options = self.columns_widget.options("given", ConversationsDisplay.given_list_width) options = self.columns_widget.options(urwid.GIVEN, ConversationsDisplay.given_list_width)
if not (self.dialog_open and self.sync_dialog != None): if not (self.dialog_open and self.sync_dialog != None):
self.columns_widget.contents[0] = (self.listbox, options) self.columns_widget.contents[0] = (self.listbox, options)
else: else:
bottom = self.listbox bottom = self.listbox
overlay = urwid.Overlay(self.sync_dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
self.sync_dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
self.columns_widget.contents[0] = (overlay, options) self.columns_widget.contents[0] = (overlay, options)
if ilb_position != None: if ilb_position != None:
@ -606,17 +749,17 @@ class ConversationsDisplay():
self.app.mark_conversation_read(self.currently_displayed_conversation) self.app.mark_conversation_read(self.currently_displayed_conversation)
self.currently_displayed_conversation = source_hash self.currently_displayed_conversation = source_hash
# options = self.widget.options("weight", 1-ConversationsDisplay.list_width) # options = self.widget.options(urwid.WEIGHT, 1-ConversationsDisplay.list_width)
options = self.widget.options("weight", 1) options = self.widget.options(urwid.WEIGHT, 1)
self.widget.contents[1] = (self.make_conversation_widget(source_hash), options) self.widget.contents[1] = (self.make_conversation_widget(source_hash), options)
if source_hash == None: if source_hash == None:
self.widget.set_focus_column(0) self.widget.focus_position = 0
else: else:
if self.app.conversation_is_unread(source_hash): if self.app.conversation_is_unread(source_hash):
self.app.mark_conversation_read(source_hash) self.app.mark_conversation_read(source_hash)
self.update_conversation_list() self.update_conversation_list()
self.widget.set_focus_column(1) self.widget.focus_position = 1
conversation_position = None conversation_position = None
index = 0 index = 0
for widget in self.list_widgets: for widget in self.list_widgets:
@ -756,9 +899,9 @@ class MessageEdit(urwid.Edit):
y = self.get_cursor_coords(size)[1] y = self.get_cursor_coords(size)[1]
if y == 0: if y == 0:
if self.delegate.full_editor_active and self.name == "title_editor": if self.delegate.full_editor_active and self.name == "title_editor":
self.delegate.frame.set_focus("body") self.delegate.frame.focus_position = "body"
elif not self.delegate.full_editor_active and self.name == "content_editor": elif not self.delegate.full_editor_active and self.name == "content_editor":
self.delegate.frame.set_focus("body") self.delegate.frame.focus_position = "body"
else: else:
return super(MessageEdit, self).keypress(size, key) return super(MessageEdit, self).keypress(size, key)
else: else:
@ -769,11 +912,11 @@ class MessageEdit(urwid.Edit):
class ConversationFrame(urwid.Frame): class ConversationFrame(urwid.Frame):
def keypress(self, size, key): def keypress(self, size, key):
if self.get_focus() == "body": if self.focus_position == "body":
if key == "up" and self.delegate.messagelist.top_is_visible: if key == "up" and self.delegate.messagelist.top_is_visible:
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
elif key == "down" and self.delegate.messagelist.bottom_is_visible: elif key == "down" and self.delegate.messagelist.bottom_is_visible:
self.set_focus("footer") self.focus_position = "footer"
else: else:
return super(ConversationFrame, self).keypress(size, key) return super(ConversationFrame, self).keypress(size, key)
elif key == "ctrl k": elif key == "ctrl k":
@ -788,7 +931,7 @@ class ConversationWidget(urwid.WidgetWrap):
if source_hash == None: if source_hash == None:
self.frame = None self.frame = None
display_widget = urwid.LineBox(urwid.Filler(urwid.Text("\n No conversation selected"), "top")) display_widget = urwid.LineBox(urwid.Filler(urwid.Text("\n No conversation selected"), "top"))
urwid.WidgetWrap.__init__(self, display_widget) super().__init__(display_widget)
else: else:
if source_hash in ConversationsDisplay.cached_conversation_widgets: if source_hash in ConversationsDisplay.cached_conversation_widgets:
return ConversationsDisplay.cached_conversation_widgets[source_hash] return ConversationsDisplay.cached_conversation_widgets[source_hash]
@ -815,7 +958,11 @@ class ConversationWidget(urwid.WidgetWrap):
header = None header = None
if self.conversation.trust_level == DirectoryEntry.UNTRUSTED: if self.conversation.trust_level == DirectoryEntry.UNTRUSTED:
header = urwid.AttrMap(urwid.Padding(urwid.Text(g["warning"]+" Warning: Conversation with untrusted peer "+g["warning"], align="center")), "msg_warning_untrusted") header = urwid.AttrMap(
urwid.Padding(
urwid.Text(g["warning"]+" Warning: Conversation with untrusted peer "+g["warning"], align=urwid.CENTER)),
"msg_warning_untrusted",
)
self.minimal_editor = urwid.AttrMap(msg_editor, "msg_editor") self.minimal_editor = urwid.AttrMap(msg_editor, "msg_editor")
self.minimal_editor.name = "minimal_editor" self.minimal_editor.name = "minimal_editor"
@ -852,7 +999,7 @@ class ConversationWidget(urwid.WidgetWrap):
self.frame self.frame
) )
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def clear_history_dialog(self): def clear_history_dialog(self):
def dismiss_dialog(sender): def dismiss_dialog(sender):
@ -867,17 +1014,30 @@ class ConversationWidget(urwid.WidgetWrap):
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Clear conversation history\n", align="center"), urwid.Text("Clear conversation history\n", align=urwid.CENTER),
urwid.Columns([("weight", 0.45, urwid.Button("Yes", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("No", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Yes", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("No", on_press=dismiss_dialog)),
])
]), title="?" ]), title="?"
) )
dialog.delegate = self dialog.delegate = self
bottom = self.messagelist bottom = self.messagelist
overlay = urwid.Overlay(dialog, bottom, align="center", width=34, valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=34,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
self.frame.contents["body"] = (overlay, self.frame.options()) self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.set_focus("body") self.frame.focus_position = "body"
def toggle_editor(self): def toggle_editor(self):
if self.full_editor_active: if self.full_editor_active:
@ -894,7 +1054,17 @@ class ConversationWidget(urwid.WidgetWrap):
if allowed: if allowed:
self.frame.contents["footer"] = (self.minimal_editor, None) self.frame.contents["footer"] = (self.minimal_editor, None)
else: else:
warning = urwid.AttrMap(urwid.Padding(urwid.Text("\n"+g["info"]+"\n\nYou cannot currently message this peer, since it's identity keys are not known.\n\nWait for an announce to arrive from the peer, or query the network for it.\n\nTo query the network, select this conversation in the conversation list, press Ctrl-E, and use the query button.\n", align="center")), "msg_header_caution") warning = urwid.AttrMap(
urwid.Padding(urwid.Text(
"\n"+g["info"]+"\n\nYou cannot currently message this peer, since its identity keys are not known. "
"The keys have been requested from the network and should arrive shortly, if available. "
"Close this conversation and reopen it to try again.\n\n"
"To query the network manually, select this conversation in the conversation list, "
"press Ctrl-E, and use the query button.\n",
align=urwid.CENTER,
)),
"msg_header_caution",
)
self.frame.contents["footer"] = (warning, None) self.frame.contents["footer"] = (warning, None)
def toggle_focus_area(self): def toggle_focus_area(self):
@ -905,9 +1075,9 @@ class ConversationWidget(urwid.WidgetWrap):
pass pass
if name == "messagelist": if name == "messagelist":
self.frame.set_focus("footer") self.frame.focus_position = "footer"
elif name == "minimal_editor" or name == "full_editor": elif name == "minimal_editor" or name == "full_editor":
self.frame.set_focus("body") self.frame.focus_position = "body"
def keypress(self, size, key): def keypress(self, size, key):
if key == "tab": if key == "tab":
@ -974,7 +1144,30 @@ 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=urwid.CENTER),
urwid.Columns([
(urwid.WEIGHT, 0.6, urwid.Text("")),
(urwid.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=urwid.CENTER, width=60, valign=urwid.MIDDLE, height=urwid.PACK, left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.focus_position = "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 == "":
@ -983,6 +1176,70 @@ 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=urwid.CENTER,
),
urwid.Columns([
(urwid.WEIGHT, 0.5, urwid.Button("Print QR", on_press=print_qr)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.5, urwid.Button("Save QR", on_press=save_qr)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.5, urwid.Button("Save URI", on_press=save_uri)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.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=urwid.CENTER, width=60, valign=urwid.MIDDLE, height=urwid.PACK, left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.focus_position = "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
@ -990,17 +1247,23 @@ class ConversationWidget(urwid.WidgetWrap):
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Could not output paper message,\ncheck your settings. See the log\nfile for any error messages.\n", align="center"), urwid.Text(
urwid.Columns([("weight", 0.6, urwid.Text("")), ("weight", 0.4, urwid.Button("OK", on_press=dismiss_dialog))]) "Could not output paper message,\ncheck your settings. See the log\nfile for any error messages.\n",
align=urwid.CENTER,
),
urwid.Columns([
(urwid.WEIGHT, 0.6, urwid.Text("")),
(urwid.WEIGHT, 0.4, urwid.Button("OK", on_press=dismiss_dialog)),
])
]), title="!" ]), title="!"
) )
dialog.delegate = self dialog.delegate = self
bottom = self.messagelist bottom = self.messagelist
overlay = urwid.Overlay(dialog, bottom, align="center", width=34, valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=34, valign=urwid.MIDDLE, height=urwid.PACK, left=2, right=2)
self.frame.contents["body"] = (overlay, self.frame.options()) self.frame.contents["body"] = (overlay, self.frame.options())
self.frame.set_focus("body") self.frame.focus_position = "body"
def close(self): def close(self):
self.delegate.close_conversation(self) self.delegate.close_conversation(self)
@ -1060,7 +1323,7 @@ class LXMessageWidget(urwid.WidgetWrap):
urwid.Text("") urwid.Text("")
]) ])
urwid.WidgetWrap.__init__(self, display_widget) super().__init__(display_widget)
class SyncProgressBar(urwid.ProgressBar): class SyncProgressBar(urwid.ProgressBar):
def get_text(self): def get_text(self):

View file

@ -15,7 +15,7 @@ class DirectoryDisplay():
]) ])
self.shortcuts_display = DirectoryDisplayShortcuts(self.app) self.shortcuts_display = DirectoryDisplayShortcuts(self.app)
self.widget = urwid.Filler(pile, 'top') self.widget = urwid.Filler(pile, urwid.TOP)
def shortcuts(self): def shortcuts(self):
return self.shortcuts_display return self.shortcuts_display

View file

@ -6,13 +6,13 @@ class IntroDisplay():
font = urwid.font.HalfBlock5x4Font() font = urwid.font.HalfBlock5x4Font()
big_text = urwid.BigText(("intro_title", self.app.config["textui"]["intro_text"]), font) big_text = urwid.BigText(("intro_title", self.app.config["textui"]["intro_text"]), font)
big_text = urwid.Padding(big_text, align="center", width="clip") big_text = urwid.Padding(big_text, align=urwid.CENTER, width=urwid.CLIP)
intro = urwid.Pile([ intro = urwid.Pile([
big_text, big_text,
urwid.Text(("Version %s" % (str(self.app.version))), align="center"), urwid.Text(("Version %s" % (str(self.app.version))), align=urwid.CENTER),
urwid.Divider(), urwid.Divider(),
urwid.Text(("-= Starting =- "), align="center"), urwid.Text(("-= Starting =- "), align=urwid.CENTER),
]) ])
self.widget = urwid.Filler(intro) self.widget = urwid.Filler(intro)

View file

@ -10,7 +10,7 @@ class GuideDisplayShortcuts():
self.app = app self.app = app
g = app.ui.glyphs g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Padding(urwid.Text(""), align="left"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Padding(urwid.Text(""), align=urwid.LEFT), "shortcutbar")
class ListEntry(urwid.Text): class ListEntry(urwid.Text):
_selectable = True _selectable = True
@ -75,7 +75,7 @@ class GuideEntry(urwid.WidgetWrap):
style = "topic_list_normal" style = "topic_list_normal"
focus_style = "list_focus" focus_style = "list_focus"
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def display_topic(self, event, topic): def display_topic(self, event, topic):
markup = TOPICS[topic] markup = TOPICS[topic]
@ -109,6 +109,7 @@ class TopicList(urwid.WidgetWrap):
self.topic_list = [ self.topic_list = [
GuideEntry(self.app, self, guide_display, "Introduction"), GuideEntry(self.app, self, guide_display, "Introduction"),
GuideEntry(self.app, self, guide_display, "Concepts & Terminology"), GuideEntry(self.app, self, guide_display, "Concepts & Terminology"),
GuideEntry(self.app, self, guide_display, "Interfaces"),
GuideEntry(self.app, self, guide_display, "Hosting a Node"), GuideEntry(self.app, self, guide_display, "Hosting a Node"),
GuideEntry(self.app, self, guide_display, "Configuration Options"), GuideEntry(self.app, self, guide_display, "Configuration Options"),
GuideEntry(self.app, self, guide_display, "Keyboard Shortcuts"), GuideEntry(self.app, self, guide_display, "Keyboard Shortcuts"),
@ -125,12 +126,12 @@ class TopicList(urwid.WidgetWrap):
highlight_offFocus="list_off_focus" highlight_offFocus="list_off_focus"
) )
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.ilb, title="Topics")) super().__init__(urwid.LineBox(self.ilb, title="Topics"))
def keypress(self, size, key): def keypress(self, size, key):
if key == "up" and (self.ilb.first_item_is_selected()): if key == "up" and (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.focus_position = "header"
return super(TopicList, self).keypress(size, key) return super(TopicList, self).keypress(size, key)
@ -141,16 +142,16 @@ class GuideDisplay():
self.app = app self.app = app
g = self.app.ui.glyphs g = self.app.ui.glyphs
topic_text = urwid.Text("\n No topic selected", align="left") topic_text = urwid.Text("\n No topic selected", align=urwid.LEFT)
self.left_area = TopicList(self.app, self) self.left_area = TopicList(self.app, self)
self.right_area = urwid.LineBox(urwid.Filler(topic_text, "top")) self.right_area = urwid.LineBox(urwid.Filler(topic_text, urwid.TOP))
self.columns = urwid.Columns( self.columns = urwid.Columns(
[ [
("weight", GuideDisplay.list_width, self.left_area), (urwid.WEIGHT, GuideDisplay.list_width, self.left_area),
("weight", 1-GuideDisplay.list_width, self.right_area) (urwid.WEIGHT, 1-GuideDisplay.list_width, self.right_area)
], ],
dividechars=0, focus_column=0 dividechars=0, focus_column=0
) )
@ -163,9 +164,8 @@ class GuideDisplay():
entry.display_topic(entry.display_topic, entry.topic_name) entry.display_topic(entry.display_topic, entry.topic_name)
def set_content_widgets(self, new_content): def set_content_widgets(self, new_content):
options = self.columns.options(width_type="weight", width_amount=1-GuideDisplay.list_width) options = self.columns.options(width_type=urwid.WEIGHT, width_amount=1-GuideDisplay.list_width, box_widget=True)
pile = urwid.Pile(new_content) pile = urwid.Pile(new_content)
#content = urwid.LineBox(urwid.Filler(pile, "top"))
content = urwid.LineBox(urwid.AttrMap(ScrollBar(Scrollable(pile), thumb_char="\u2503", trough_char=" "), "scrollbar")) content = urwid.LineBox(urwid.AttrMap(ScrollBar(Scrollable(pile), thumb_char="\u2503", trough_char=" "), "scrollbar"))
self.columns.contents[1] = (content, options) self.columns.contents[1] = (content, options)
@ -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
@ -296,17 +301,22 @@ To learn how to host your own node, read the `*Hosting a Node`* section of this
TOPIC_HOSTING = '''>Hosting a Node TOPIC_HOSTING = '''>Hosting a Node
To host a node on the network, you must enable it in the configuration file, by setting `*enable_node`* directive to `*yes`*. You should also configure the other node-related parameters such as the node name and announce interval settings. Once node hosting has been enabled in the configuration, Nomad Network will start hosting your node as soon as the program is launched, and other peers on the network will be able to connect and interact with content on your node. To host a node on the network, you must enable it in the configuration file, by setting the `*enable_node`* directive to `*yes`*. You should also configure the other node-related parameters such as the node name and announce interval settings. Once node hosting has been enabled in the configuration, Nomad Network will start hosting your node as soon as the program is launched, and other peers on the network will be able to connect and interact with content on your node.
By default, no content is defined, apart from a short placeholder home page. To learn how to add your own content, read on. By default, no content is defined, apart from a short placeholder home page. To learn how to add your own content, read on.
>>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`!. `!However`!, 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:
- Immediately break peering with a node by pressing `!Ctrl-X`!
- 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.
@ -320,9 +330,9 @@ You can control how long a peer will cache your pages by including the cache hea
>> Dynamic Pages >> Dynamic Pages
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 and fully interactive applications running over Nomad Network. 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. 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 accessing 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:
@ -377,6 +387,225 @@ Links can be inserted into micron documents. See the `*Markup`* section of this
''' '''
TOPIC_INTERFACES = '''>Interfaces
Reticulum supports using many kinds of devices as networking interfaces, and allows you to mix and match them in any way you choose.
The number of distinct network topologies you can create with Reticulum is more or less endless, but common to them all is that you will need to define one or more interfaces for Reticulum to use.
The `![ Interfaces ]`! section of NomadNet lets you add, monitor, and update interfaces configured for your Reticulum instance.
If you are starting NomadNet for the first time you will find that an `!AutoInterface`! has been added by default. This interface will try to use your available network device to communicate with other peers discovered on your local network.
Interfaces come in many different types and can interact with physical mediums like LoRa radios or standard IP networks.
>>Viewing Interfaces
To view more info about an interface, navigate using the `!Up`! and `!Down`! arrow keys or by clicking with the mouse. Pressing `! < Enter >`! on a selected interface will bring you to a detailed interface view, which will show configuration parameters and realtime charts. From here you can also disable or edit the interface. To change the orientation of the TX/RX charts, press `!< V >`! for a vertical layout, and `!< H >`! for a horizontal view.
>>Updating Interfaces
To edit an interface, select the interface and press `!< Ctrl + E >`!.
To remove an interface, select the interface and press `!< Ctrl + X >`!. You can also perform both of these actions from the details view.
>>Adding Interfaces
To add a new interface, press `!< Ctrl + A >`!. From here you can select which type of interface you want to add. Each unique interface type will have different configuration options.
`Ffff`! (!) Note:`! After adding or modifying interfaces, you will need to restart NomadNet or your Reticulum instance for changes to take effect.`f`b
>Interface Types
>>AutoInterface
The Auto Interface enables communication with other discoverable Reticulum nodes over autoconfigured IPv6 and UDP. It does not need any functional IP infrastructure like routers or DHCP servers, but will require at least some sort of switching medium between peers (a wired switch, a hub, a WiFi access point or similar).
```
Required Parameters:
Interface Name
Optional Parameters:
Devices: Specific network devices to use
Ignored Devices: Network devices to exclude
Group ID: Create isolated networks on the same physical LAN
Discovery Scope: Can set to link, admin, site, organisation or global
```
The AutoInterface is ideal for quickly connecting to other Reticulum nodes on your local network without complex configuration.
>>TCPClientInterface
The TCP Client interface connects to remote TCP server interfaces, enabling communication over the Internet or private networks.
```
Required Parameters:
Target Host: Hostname or IP address of the server
Target Port: Port number to connect to
Optional Parameters:
I2P Tunneled: Enable for connecting through I2P
KISS Framing: Enable for KISS framing for software modems
```
This interface is commonly used to connect to Reticulum testnets or other persistent nodes on the Internet.
>>TCPServerInterface
The TCP Server interface listens for incoming connections, allowing other Reticulum peers to connect to your node using TCPClientInterface.
```
Required Parameters:
Listen IP: IP address to bind to (0.0.0.0 for all interfaces)
Listen Port: Port number to listen on
Optional Parameters:
Prefer IPv6: Bind to IPv6 address if available
I2P Tunneled: Enable for I2P tunnel support
Device: Specific network device to use (e,g
```
Useful when you want other nodes to be able to connect to your Transport instance over TCP/IP.
>>UDPInterface
The UDP interface allows communication over IP networks using UDP packets.
```
Required Parameters:
Listen IP: IP address to bind to
Listen Port: Port to listen on
Forward IP: IP address to forward to (Can be broadcast address)
Forward Port: Port to forward to
Optional Parameters:
Device: Specific network device to use
```
>>I2PInterface
The I2P interface enables connections over the Invisible Internet Protocol. The I2PInterface requires an I2P daemon to be running on your system, such as `!i2pd`!
```
Optional Parameters:
Peers: I2P addresses to connect to (Can be left as none if running as a Transport)
```
>>RNodeInterface
The RNode interface allows using LoRa transceivers running RNode firmware as Reticulum network interfaces.
```
Required Parameters:
Port: Serial port or BLE device path
Frequency: Operating frequency in MHz
Bandwidth: Channel bandwidth
TX Power: Transmit power in dBm
Spreading Factor: LoRa spreading factor (7-12)
Coding Rate: LoRa coding rate (4:5-4:8)
Optional Parameters:
ID Callsign: Station identification
ID Interval: Identification interval in seconds
Airtime Limits: Control duty cycle
```
The interface includes a parameter calculator to estimate link budget, sensitivity, and data rate on the air based on your settings.
>>RNodeMultiInterface
The RNode Multi Interface is designed for use with specific hardware platforms that support multiple subinterfaces or virtual radios. For most LoRa hardware platforms, you will want to use the standard `!RNodeInterface`! instead.
```
Required Parameters:
Port: Serial port or BLE device path
Subinterfaces: Configuration for each subinterface / virtual radio port
```
>>SerialInterface
The Serial interface enables using direct serial connections as Reticulum interfaces.
```
Required Parameters:
Port: Serial port path
Speed: Baud rate of serial device
Databits: Number of data bits
Parity: Parity setting
Stopbits: Number of stop bits
```
>>KISSInterface
The KISS interface supports packet radio modems and TNCs using the KISS protocol.
```
Required Parameters:
Port: Serial port path
Speed: Baud rate of serial device
Databits: Number of data bits
Parity: Parity setting
Stopbits: Number of stop bits
Preamble: Modem preamble in milliseconds
TX Tail: Transmit tail in milliseconds
Slottime: CSMA slottime in milliseconds
Persistence: CSMA persistence value
Optional Parameters:
ID Callsign: Station identification
ID Interval: Identification interval in seconds
Flow Control: Enable packet flow control
```
>>PipeInterface
The Pipe interface allows using external programs as Reticulum interfaces via stdin and stdout.
```
Required Parameters:
Command: External command to execute
Optional Parameters:
Respawn Delay: Seconds to wait before restarting after failure
```
<
>>
-
For more information and to view the full Interface documentation consult the Reticulum manual or visit https://reticulum.network/manual/interfaces.html (Requires external Internet connection)
>Interface Access Code (IFAC) Settings
Interface Access Codes create private networks and securely segment network traffic. Under `!Show more options`!, you'll find these IFAC settings:
`!Virtual Network Name`! (network_name):
When added, creates a logical network that only communicates with other interfaces using the same name. This allows multiple separate Reticulum networks to exist on the same physical channel or medium.
`!IFAC Passphrase`! (passphrase):
Sets an authentication passphrase for the interface. Only interfaces configured with the same passphrase will be able to communicate. Can be used with or without a network name.
`!IFAC Size`! (ifac_size):
Controls the length of Interface Authentication Codes (8-512 bits). Larger sizes provide stronger security but add overhead to each packet. The default of `!8`! is usually appropriate for most uses.
>Interface Modes
When running a Transport node, you can configure interface modes that affect how Reticulum selects paths, propagates announces, and discovers routes:
`!full`!: Default mode with all discovery, meshing and transport functionality
`!gateway`!: Discovers paths on behalf of other nodes
`!access_point`!: Operates as a network access point
`!roaming`!: For physically mobile interfaces
`!boundary`!: For interfaces connecting different network segments
'''
TOPIC_CONVERSATIONS = '''>Conversations TOPIC_CONVERSATIONS = '''>Conversations
Conversations in Nomad Network Conversations in Nomad Network
@ -391,7 +620,7 @@ You're currently located in the guide section of the program. I'm sorry I had to
To get the most out of Nomad Network, you will need a terminal that supports UTF-8 and at least 256 colors, ideally true-color. If your terminal supports true-color, you can go to the `![ Config ]`! menu item, launch the editor and change the configuration. To get the most out of Nomad Network, you will need a terminal that supports UTF-8 and at least 256 colors, ideally true-color. If your terminal supports true-color, you can go to the `![ Config ]`! menu item, launch the editor and change the configuration.
It is recommended to use a terminal size of at least 122x32. Nomad Network will work with smaller terminal sizes, but the interface might feel a bit cramped. It is recommended to use a terminal size of at least 135x32. Nomad Network will work with smaller terminal sizes, but the interface might feel a bit cramped.
If you don't already have a Nerd Font installed (see https://www.nerdfonts.com/), I also highly recommend to do so, since it will greatly expand the amount of glyphs, icons and graphics that Nomad Network can use. Once you have your terminal set up with a Nerd Font, go to the `![ Config ]`! menu item and enable Nerd Fonts in the configuration instead of normal unicode glyphs. If you don't already have a Nerd Font installed (see https://www.nerdfonts.com/), I also highly recommend to do so, since it will greatly expand the amount of glyphs, icons and graphics that Nomad Network can use. Once you have your terminal set up with a Nerd Font, go to the `![ Config ]`! menu item and enable Nerd Fonts in the configuration instead of normal unicode glyphs.
@ -476,6 +705,12 @@ Selects which interface to use. Currently, only the `!text`! interface is availa
Sets the filesystem path to store downloaded files in. Sets the filesystem path to store downloaded files in.
< <
>>>
`!notify_on_new_message = yes`!
>>>>
Sets whether to output a notification character (bell or flash) to the terminal when a new message is received.
<
>>> >>>
`!announce_at_start = yes`! `!announce_at_start = yes`!
>>>> >>>>
@ -506,6 +741,24 @@ 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.
< <
>>>
`!required_stamp_cost = None`!
>>>>
You can specify a required stamp cost for inbound messages to be accepted. Specifying a stamp cost will require untrusted senders that message you to include a cryptographic stamp in their messages. Performing this operation takes the sender an amount of time proportional to the stamp cost. As a rough estimate, a stamp cost of 8 will take less than a second to compute, and a stamp cost of 20 could take several minutes, even on a fast computer.
<
>>>
`!accept_invalid_stamps = False`!
>>>>
You can signal stamp requirements to senders, but still accept messages with invalid stamps by setting this option to True.
<
>>>
`!max_accepted_size = 500`!
>>>>
The maximum accepted unpacked size for messages received directly from other peers, specified in kilobytes. Messages larger than this will be rejected before the transfer begins.
<
>>> >>>
`!compact_announce_stream = yes`! `!compact_announce_stream = yes`!
>>>> >>>>
@ -614,24 +867,60 @@ Determines how often, in minutes, your node is announced on the network. Default
Determines where the node server will look for hosted pages. Must be a readable filesystem path. Determines where the node server will look for hosted pages. Must be a readable filesystem path.
< <
>>>
`!page_refresh_interval = 0`!
>>>>
Determines the interval in minutes for rescanning the hosted pages path. By default, this option is disabled, and the pages path will only be scanned on startup.
<
>>> >>>
`!files_path = ~/.nomadnetwork/storage/files`! `!files_path = ~/.nomadnetwork/storage/files`!
>>>> >>>>
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.
< <
>>>
`!file_refresh_interval = 0`!
>>>>
Determines the interval in minutes for rescanning the hosted files path. By default, this option is disabled, and the files path will only be scanned on startup.
<
>>>
`!disable_propagation = yes`!
>>>>
When Nomad Network is hosting a node, it can 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`!
>>>> >>>>
Configures the maximum amount of storage, in megabytes, that the LXMF Propagation Node will use to store messages. Configures the maximum amount of storage, in megabytes, that the LXMF Propagation Node will use to store messages.
< <
>>>
`!max_transfer_size = 256`!
>>>>
The maximum accepted transfer size per incoming propagation transfer, in kilobytes. This also sets the upper limit for the size of single messages accepted onto this propagation node. If a node wants to propagate a larger number of messages to this node, than what can fit within this limit, it will prioritise sending the smallest, newest messages first, and try with any remaining messages at a later point.
<
>>> >>>
`!prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf`! `!prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf`!
>>>> >>>>
Configures the LXMF Propagation Node to prioritise storing messages for certain destinations. If the message store reaches the specified limit, LXMF will prioritise keeping messages for destinations specified with this option. This setting is optional, and generally you do not need to use it. Configures the LXMF Propagation Node to prioritise storing messages for certain destinations. If the message store reaches the specified limit, LXMF will prioritise keeping messages for destinations specified with this option. This setting is optional, and generally you do not need to use it.
< <
>>>
`!max_peers = 25`!
>>>>
Configures the maximum number of other nodes the LXMF Propagation Node will automatically peer with. The default is 50, but can be lowered or increased according to available resources.
<
>>>
`!static_peers = e17f833c4ddf8890dd3a79a6fea8161d, 5a2d0029b6e5ec87020abaea0d746da4`!
>>>>
Configures the LXMF Propagation Node to always maintain propagation node peering with the specified list of destination hashes.
<
>> Printing Section >> Printing Section
This section holds configuration directives related to printing. It is delimited by the `![printing]`! header in the configuration file. Available directives, along with example values, are as follows: This section holds configuration directives related to printing. It is delimited by the `![printing]`! header in the configuration file. Available directives, along with example values, are as follows:
@ -694,12 +983,11 @@ If you have Internet access, and just want to get started experimenting, you are
The Testnet also runs the latest version of Reticulum, often even a short while before it is publicly released, which means strange behaviour might occur. If none of that scares you, add the following interface to your Reticulum configuration file to join: The Testnet also runs the latest version of Reticulum, often even a short while before it is publicly released, which means strange behaviour might occur. If none of that scares you, add the following interface to your Reticulum configuration file to join:
>> >>
[[RNS Testnet Zurich]] [[RNS Testnet Dublin]]
type = TCPClientInterface type = TCPClientInterface
interface_enabled = yes enabled = yes
outgoing = True target_host = dublin.connect.reticulum.network
target_host = zurich.connect.reticulum.network target_port = 4965
target_port = 4242
< <
If you connect to the testnet, you can leave nomadnet running for a while and wait for it to receive announces from other nodes on the network that host pages or services, or you can try connecting directly to some nodes listed here: If you connect to the testnet, you can leave nomadnet running for a while and wait for it to receive announces from other nodes on the network that host pages or services, or you can try connecting directly to some nodes listed here:
@ -744,7 +1032,7 @@ The following line should contain a grayscale gradient bar:
Unicode Glyphs : \u2713 \u2715 \u26a0 \u24c3 \u2193 Unicode Glyphs : \u2713 \u2715 \u26a0 \u24c3 \u2193
Nerd Font Glyphs : \uf484 \uf9c4 \uf719 \uf502 \uf415 \uf023 \uf06e Nerd Font Glyphs : \uf484 \U000f04c5 \U000f0219 \U000f0002 \uf415 \uf023 \uf06e
''' '''
@ -980,6 +1268,9 @@ You can use `B5d5`F222 color `f`B333 `Ff00f`Ff80o`Ffd0r`F9f0m`F0f2a`F0fdt`F07ft`
`` ``
>Page Foreground and Background Colors
To specify a background color for the entire page, place the `!#!bg=X`! header on one of the first lines of your page, where `!X`! is the color you want to use, for example `!444`!. If you're also using the cache control header, the background specifier must come `*after`* the cache control header. Likewise, you can specify the default text color by using the `!#!fg=X`! header.
>Links >Links
@ -1025,7 +1316,7 @@ Links can contain request variables and a list of fields to submit to the node-s
`= `=
`` ``
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: 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 `Faaa
`= `=
@ -1096,6 +1387,48 @@ A sized input field: `B444`<16|with_size`>`B333
A masked input field: `B444`<!|masked_demo`hidden text>`B333 A masked input field: `B444`<!|masked_demo`hidden text>`B333
Full control: `B444`<!32|all_options`hidden text>`B333 Full control: `B444`<!32|all_options`hidden text>`B333
`b
>>> Checkboxes
In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
`Faaa
`=
`<?|field_name|value`>`b Label Text`
`=
When the checkbox is checked, it's field will be set to the provided value. If there are multiple checkboxes that share the same field name, the checked values will be concatenated when they are sent to the node by a comma.
``
`B444`<?|sign_up|1`>`b Sign me up`
You can also pre-check both checkboxes and radio groups by appending a |* after the field value.
`B444`<?|checkbox|1|*`>`b Pre-checked checkbox`
>>> Radio groups
Radio groups are another input that lets the user chose from a set of options. Unlike checkboxes, radio buttons with the same field name are mutually exclusive.
Example:
`=
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
`=
will render:
`B900`<^|color|Red`>`b Red
`B090`<^|color|Green`>`b Green
`B009`<^|color|Blue`>`b Blue
In this example, when the data is submitted, `B444` field_color`b will be set to whichever value from the list was selected.
`` ``
@ -1131,13 +1464,14 @@ 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, pages and applications using micron and Nomad Network. 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"
TOPICS = { TOPICS = {
"Introduction": TOPIC_INTRODUCTION, "Introduction": TOPIC_INTRODUCTION,
"Concepts & Terminology": TOPIC_CONCEPTS, "Concepts & Terminology": TOPIC_CONCEPTS,
"Conversations": TOPIC_CONVERSATIONS, "Conversations": TOPIC_CONVERSATIONS,
"Interfaces": TOPIC_INTERFACES,
"Hosting a Node": TOPIC_HOSTING, "Hosting a Node": TOPIC_HOSTING,
"Configuration Options": TOPIC_CONFIG, "Configuration Options": TOPIC_CONFIG,
"Keyboard Shortcuts": TOPIC_SHORTCUTS, "Keyboard Shortcuts": TOPIC_SHORTCUTS,

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,11 @@
import os
import sys
import itertools
import mmap
import urwid import urwid
import nomadnet import nomadnet
class LogDisplayShortcuts(): class LogDisplayShortcuts():
def __init__(self, app): def __init__(self, app):
import urwid import urwid
@ -8,31 +13,115 @@ class LogDisplayShortcuts():
self.widget = urwid.AttrMap(urwid.Text(""), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text(""), "shortcutbar")
class LogDisplay(): class LogDisplay():
def __init__(self, app): def __init__(self, app):
import urwid
self.app = app self.app = app
self.log_term = LogTerminal(self.app)
self.shortcuts_display = LogDisplayShortcuts(self.app) self.shortcuts_display = LogDisplayShortcuts(self.app)
self.widget = urwid.LineBox(self.log_term) self.widget = None
@property
def log_term(self):
return self.widget
def show(self):
if self.widget is None:
self.widget = log_widget(self.app)
def kill(self):
if self.widget is not None:
self.widget.terminate()
self.widget = None
def shortcuts(self): def shortcuts(self):
return self.shortcuts_display return self.shortcuts_display
class LogTerminal(urwid.WidgetWrap): class LogTerminal(urwid.WidgetWrap):
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
self.log_term = urwid.Terminal( self.log_term = urwid.Terminal(
("tail", "-fn50", self.app.logfilepath), ("tail", "-fn50", self.app.logfilepath),
encoding='utf-8', encoding='utf-8',
escape_sequence="up" escape_sequence="up",
main_loop=self.app.ui.loop,
) )
urwid.WidgetWrap.__init__(self, self.log_term) self.widget = urwid.LineBox(self.log_term)
super().__init__(self.widget)
def terminate(self):
self.log_term.terminate()
def keypress(self, size, key): def keypress(self, size, key):
if key == "up": if key == "up":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
return super(LogTerminal, self).keypress(size, key) return super(LogTerminal, self).keypress(size, key)
class LogTail(urwid.WidgetWrap):
def __init__(self, app):
self.app = app
self.log_tail = urwid.Text(tail(self.app.logfilepath, 50))
self.log = urwid.Scrollable(self.log_tail)
self.log.set_scrollpos(-1)
self.log_scrollbar = urwid.ScrollBar(self.log)
# We have this here because ui.textui.Main depends on this field to kill it
self.log_term = None
super().__init__(self.log_scrollbar)
def terminate(self):
pass
def log_widget(app, platform=sys.platform):
if platform == "win32":
return LogTail(app)
else:
return LogTerminal(app)
# https://stackoverflow.com/a/34029605/3713120
def _tail(f_name, n, offset=0):
def skip_back_lines(mm: mmap.mmap, numlines: int, startidx: int) -> int:
'''Factored out to simplify handling of n and offset'''
for _ in itertools.repeat(None, numlines):
startidx = mm.rfind(b'\n', 0, startidx)
if startidx < 0:
break
return startidx
# Open file in binary mode
with open(f_name, 'rb') as binf, mmap.mmap(binf.fileno(), 0, access=mmap.ACCESS_READ) as mm:
# len(mm) - 1 handles files ending w/newline by getting the prior line
startofline = skip_back_lines(mm, offset, len(mm) - 1)
if startofline < 0:
return [] # Offset lines consumed whole file, nothing to return
# If using a generator function (yield-ing, see below),
# this should be a plain return, no empty list
endoflines = startofline + 1 # Slice end to omit offset lines
# Find start of lines to capture (add 1 to move from newline to beginning of following line)
startofline = skip_back_lines(mm, n, startofline) + 1
# Passing True to splitlines makes it return the list of lines without
# removing the trailing newline (if any), so list mimics f.readlines()
# return mm[startofline:endoflines].splitlines(True)
# If Windows style \r\n newlines need to be normalized to \n
return mm[startofline:endoflines].replace(os.linesep.encode(sys.getdefaultencoding()), b'\n').splitlines(True)
def tail(f_name, n):
"""
Return the last n lines of a given file name, f_name.
Akin to `tail -<n> <f_name>`
"""
def decode(b):
return b.decode(encoding)
encoding = sys.getdefaultencoding()
lines = map(decode, _tail(f_name=f_name, n=n))
return ''.join(lines)

View file

@ -1,10 +1,10 @@
import RNS import RNS
import time
from .Network import * from .Network import *
from .Conversations import * from .Conversations import *
from .Directory import * from .Directory import *
from .Config import * from .Config import *
from .Interfaces import *
from .Map import * from .Map import *
from .Log import * from .Log import *
from .Guide import * from .Guide import *
@ -17,6 +17,7 @@ class SubDisplays():
self.conversations_display = ConversationsDisplay(self.app) self.conversations_display = ConversationsDisplay(self.app)
self.directory_display = DirectoryDisplay(self.app) self.directory_display = DirectoryDisplay(self.app)
self.config_display = ConfigDisplay(self.app) self.config_display = ConfigDisplay(self.app)
self.interface_display = InterfaceDisplay(self.app)
self.map_display = MapDisplay(self.app) self.map_display = MapDisplay(self.app)
self.log_display = LogDisplay(self.app) self.log_display = LogDisplay(self.app)
self.guide_display = GuideDisplay(self.app) self.guide_display = GuideDisplay(self.app)
@ -114,8 +115,14 @@ class MainDisplay():
self.sub_displays.active_display = self.sub_displays.config_display self.sub_displays.active_display = self.sub_displays.config_display
self.update_active_sub_display() self.update_active_sub_display()
def show_interfaces(self, user_data):
self.sub_displays.active_display = self.sub_displays.interface_display
self.update_active_sub_display()
self.sub_displays.interface_display.start()
def show_log(self, user_data): def show_log(self, user_data):
self.sub_displays.active_display = self.sub_displays.log_display self.sub_displays.active_display = self.sub_displays.log_display
self.sub_displays.log_display.show()
self.update_active_sub_display() self.update_active_sub_display()
def show_guide(self, user_data): def show_guide(self, user_data):
@ -125,6 +132,8 @@ class MainDisplay():
def update_active_sub_display(self): def update_active_sub_display(self):
self.frame.contents["body"] = (self.sub_displays.active().widget, None) self.frame.contents["body"] = (self.sub_displays.active().widget, None)
self.update_active_shortcuts() self.update_active_shortcuts()
if self.sub_displays.active_display != self.sub_displays.log_display:
self.sub_displays.log_display.kill()
def update_active_shortcuts(self): def update_active_shortcuts(self):
self.frame.contents["footer"] = (self.sub_displays.active().shortcuts().widget, None) self.frame.contents["footer"] = (self.sub_displays.active().shortcuts().widget, None)
@ -155,7 +164,7 @@ class MainDisplay():
class MenuColumns(urwid.Columns): class MenuColumns(urwid.Columns):
def keypress(self, size, key): def keypress(self, size, key):
if key == "tab" or key == "down": if key == "tab" or key == "down":
self.handler.frame.set_focus("body") self.handler.frame.focus_position = "body"
return super(MenuColumns, self).keypress(size, key) return super(MenuColumns, self).keypress(size, key)
@ -169,21 +178,22 @@ class MenuDisplay():
self.menu_indicator = urwid.Text("") self.menu_indicator = urwid.Text("")
menu_text = ("pack", self.menu_indicator) menu_text = (urwid.PACK, self.menu_indicator)
button_network = (11, MenuButton("Network", on_press=handler.show_network)) button_network = (11, MenuButton("Network", on_press=handler.show_network))
button_conversations = (17, MenuButton("Conversations", on_press=handler.show_conversations)) button_conversations = (17, MenuButton("Conversations", on_press=handler.show_conversations))
button_directory = (13, MenuButton("Directory", on_press=handler.show_directory)) button_directory = (13, MenuButton("Directory", on_press=handler.show_directory))
button_map = (7, MenuButton("Map", on_press=handler.show_map)) button_map = (7, MenuButton("Map", on_press=handler.show_map))
button_log = (7, MenuButton("Log", on_press=handler.show_log)) button_log = (7, MenuButton("Log", on_press=handler.show_log))
button_config = (10, MenuButton("Config", on_press=handler.show_config)) button_config = (10, MenuButton("Config", on_press=handler.show_config))
button_guide = (9, MenuButton("Guide", on_press=handler.show_guide)) button_interfaces = (14, MenuButton("Interfaces", on_press=handler.show_interfaces))
button_quit = (8, MenuButton("Quit", on_press=handler.quit)) button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map] # buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
if self.app.config["textui"]["hide_guide"]: if self.app.config["textui"]["hide_guide"]:
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit] buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_quit]
else: else:
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_guide, button_quit] buttons = [menu_text, button_conversations, button_network, button_log, button_interfaces, button_config, button_guide, button_quit]
columns = MenuColumns(buttons, dividechars=1) columns = MenuColumns(buttons, dividechars=1)
columns.handler = handler columns.handler = handler

View file

@ -15,7 +15,7 @@ class MapDisplay():
]) ])
self.shortcuts_display = MapDisplayShortcuts(self.app) self.shortcuts_display = MapDisplayShortcuts(self.app)
self.widget = urwid.Filler(pile, 'top') self.widget = urwid.Filler(pile, urwid.TOP)
def shortcuts(self): def shortcuts(self):
return self.shortcuts_display return self.shortcuts_display

View file

@ -4,7 +4,6 @@ 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
import re
DEFAULT_FG_DARK = "ddd" DEFAULT_FG_DARK = "ddd"
DEFAULT_FG_LIGHT = "222" DEFAULT_FG_LIGHT = "222"
@ -32,20 +31,14 @@ SYNTH_SPECS = {}
SECTION_INDENT = 2 SECTION_INDENT = 2
INDENT_RIGHT = 1 INDENT_RIGHT = 1
def markup_to_attrmaps(markup, url_delegate = None): def default_state(fg=None, bg=None):
global SELECTED_STYLES if fg == None: fg = SELECTED_STYLES["plain"]["fg"]
if nomadnet.NomadNetworkApp.get_shared_instance().config["textui"]["theme"] == nomadnet.ui.TextUI.THEME_DARK: if bg == None: bg = DEFAULT_BG
SELECTED_STYLES = STYLES_DARK
else:
SELECTED_STYLES = STYLES_LIGHT
attrmaps = []
state = { state = {
"literal": False, "literal": False,
"depth": 0, "depth": 0,
"fg_color": SELECTED_STYLES["plain"]["fg"], "fg_color": fg,
"bg_color": DEFAULT_BG, "bg_color": bg,
"formatting": { "formatting": {
"bold": False, "bold": False,
"underline": False, "underline": False,
@ -55,7 +48,25 @@ def markup_to_attrmaps(markup, url_delegate = None):
}, },
"default_align": "left", "default_align": "left",
"align": "left", "align": "left",
"default_fg": fg,
"default_bg": bg,
} }
return state
def markup_to_attrmaps(markup, url_delegate = None, fg_color=None, bg_color=None):
global SELECTED_STYLES
if nomadnet.NomadNetworkApp.get_shared_instance().config["textui"]["theme"] == nomadnet.ui.TextUI.THEME_DARK:
SELECTED_STYLES = STYLES_DARK
else:
SELECTED_STYLES = STYLES_LIGHT
attrmaps = []
fgc = None; bgc = DEFAULT_BG
if bg_color != None: bgc = bg_color
if fg_color != None: fgc = fg_color
state = default_state(fgc, bgc)
# Split entire document into lines for # Split entire document into lines for
# processing. # processing.
@ -74,8 +85,8 @@ def markup_to_attrmaps(markup, url_delegate = None):
return attrmaps return attrmaps
def parse_line(line, state, url_delegate): def parse_line(line, state, url_delegate):
pre_escape = False
if len(line) > 0: if len(line) > 0:
first_char = line[0] first_char = line[0]
@ -89,6 +100,7 @@ def parse_line(line, state, url_delegate):
# Check if the command is an escape # Check if the command is an escape
if first_char == "\\": if first_char == "\\":
line = line[1:] line = line[1:]
pre_escape = True
# Check for comments # Check for comments
elif first_char == "#": elif first_char == "#":
@ -143,7 +155,7 @@ def parse_line(line, state, url_delegate):
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, pre_escape)
if output != None: if output != None:
text_only = True text_only = True
@ -161,8 +173,7 @@ def parse_line(line, state, url_delegate):
tw.in_columns = True tw.in_columns = True
else: else:
tw = urwid.Text(o, align=state["align"]) tw = urwid.Text(o, align=state["align"])
widgets.append((urwid.PACK, tw))
widgets.append(("pack", tw))
else: else:
if o["type"] == "field": if o["type"] == "field":
fw = o["width"] fw = o["width"]
@ -174,6 +185,36 @@ def parse_line(line, state, url_delegate):
f.field_name = fn f.field_name = fn
fa = urwid.AttrMap(f, fs) fa = urwid.AttrMap(f, fs)
widgets.append((fw, fa)) widgets.append((fw, fa))
elif o["type"] == "checkbox":
fn = o["name"]
fv = o["value"]
flabel = o["label"]
fs = o["style"]
fprechecked = o.get("prechecked", False)
f = urwid.CheckBox(flabel, state=fprechecked)
f.field_name = fn
f.field_value = fv
fa = urwid.AttrMap(f, fs)
widgets.append((urwid.PACK, fa))
elif o["type"] == "radio":
fn = o["name"]
fv = o["value"]
flabel = o["label"]
fs = o["style"]
fprechecked = o.get("prechecked", False)
if "radio_groups" not in state:
state["radio_groups"] = {}
if fn not in state["radio_groups"]:
state["radio_groups"][fn] = []
group = state["radio_groups"][fn]
f = urwid.RadioButton(group, flabel, state=fprechecked, user_data=fv)
f.field_name = fn
f.field_value = fv
fa = urwid.AttrMap(f, fs)
widgets.append((urwid.PACK, fa))
columns_widget = urwid.Columns(widgets, dividechars=0) columns_widget = urwid.Columns(widgets, dividechars=0)
text_widget = columns_widget text_widget = columns_widget
@ -397,7 +438,7 @@ def make_style(state):
return name return name
def make_output(state, line, url_delegate): def make_output(state, line, url_delegate, pre_escape=False):
output = [] output = []
if state["literal"]: if state["literal"]:
if line == "\\`=": if line == "\\`=":
@ -406,8 +447,9 @@ def make_output(state, line, url_delegate):
else: else:
part = "" part = ""
mode = "text" mode = "text"
escape = False escape = pre_escape
skip = 0 skip = 0
for i in range(0, len(line)): for i in range(0, len(line)):
c = line[i] c = line[i]
if skip > 0: if skip > 0:
@ -426,20 +468,20 @@ def make_output(state, line, url_delegate):
state["fg_color"] = color state["fg_color"] = color
skip = 3 skip = 3
elif c == "f": elif c == "f":
state["fg_color"] = SELECTED_STYLES["plain"]["fg"] state["fg_color"] = state["default_fg"]
elif c == "B": elif c == "B":
if len(line) >= i+4: if len(line) >= i+4:
color = line[i+1:i+4] color = line[i+1:i+4]
state["bg_color"] = color state["bg_color"] = color
skip = 3 skip = 3
elif c == "b": elif c == "b":
state["bg_color"] = DEFAULT_BG state["bg_color"] = state["default_bg"]
elif c == "`": elif c == "`":
state["formatting"]["bold"] = False state["formatting"]["bold"] = False
state["formatting"]["underline"] = False state["formatting"]["underline"] = False
state["formatting"]["italic"] = False state["formatting"]["italic"] = False
state["fg_color"] = SELECTED_STYLES["plain"]["fg"] state["fg_color"] = state["default_fg"]
state["bg_color"] = DEFAULT_BG state["bg_color"] = state["default_bg"]
state["align"] = state["default_align"] state["align"] = state["default_align"]
elif c == "c": elif c == "c":
if state["align"] != "center": if state["align"] != "center":
@ -459,54 +501,100 @@ 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 == "<": elif c == '<':
if len(part) > 0:
output.append(make_part(state, part))
part = ""
try: try:
field_name = None field_start = i + 1 # position after '<'
field_name_end = line[i:].find("`") backtick_pos = line.find('`', field_start)
if field_name_end == -1: if backtick_pos == -1:
pass pass # No '`', invalid field
else: else:
field_name = line[i+1:i+field_name_end] field_content = line[field_start:backtick_pos]
field_name_skip = len(field_name)
field_masked = False field_masked = False
field_width = 24 field_width = 24
field_type = "field"
field_name = field_content
field_value = ""
field_data = ""
field_prechecked = False
if "|" in field_name: # check if field_content contains '|'
f_components = field_name.split("|") if '|' in field_content:
f_components = field_content.split('|')
field_flags = f_components[0] field_flags = f_components[0]
field_name = f_components[1] field_name = f_components[1]
if "!" in field_flags:
# handle field type indicators
if '^' in field_flags:
field_type = "radio"
field_flags = field_flags.replace("^", "")
elif '?' in field_flags:
field_type = "checkbox"
field_flags = field_flags.replace("?", "")
elif '!' in field_flags:
field_flags = field_flags.replace("!", "") field_flags = field_flags.replace("!", "")
field_masked = True field_masked = True
# Handle field width
if len(field_flags) > 0: if len(field_flags) > 0:
field_width = min(int(field_flags), 256) try:
field_width = min(int(field_flags), 256)
except ValueError:
pass # Ignore invalid width
def sr(): # Check for value and pre-checked flag
return "@{"+str(random.randint(1000,9999))+"}" if len(f_components) > 2:
rsg = sr() field_value = f_components[2]
while rsg in line[i+field_name_end:]: else:
rsg = sr() field_value = ""
lr = line[i+field_name_end:].replace("\\>", rsg) if len(f_components) > 3:
endpos = lr.find(">") if f_components[3] == '*':
field_prechecked = True
if endpos == -1:
pass
else: else:
field_data = lr[1:endpos].replace(rsg, "\\>") # No '|', so field_name is field_content
skip = len(field_data)+field_name_skip+2 field_name = field_content
field_data = field_data.replace("\\>", ">") field_type = "field"
output.append({ field_masked = False
"type":"field", field_width = 24
"name": field_name, field_value = ""
"width": field_width, field_prechecked = False
"masked": field_masked,
"data": field_data, # Find the closing '>' character
"style": make_style(state) field_end = line.find('>', backtick_pos)
}) if field_end == -1:
pass # No closing '>', invalid field
else:
field_data = line[backtick_pos+1:field_end]
# Now, we have all field data
if field_type in ["checkbox", "radio"]:
# for checkboxes and radios, field_data is the label
output.append({
"type": field_type,
"name": field_name,
"value": field_value if field_value else field_data,
"label": field_data,
"prechecked": field_prechecked,
"style": make_style(state)
})
else:
# For text fields field_data is the initial text
output.append({
"type": "field",
"name": field_name,
"width": field_width,
"masked": field_masked,
"data": field_data,
"style": make_style(state)
})
skip = field_end - i
except Exception as e: except Exception as e:
pass pass
elif c == "[": elif c == "[":
endpos = line[i:].find("]") endpos = line[i:].find("]")
if endpos == -1: if endpos == -1:
@ -592,6 +680,7 @@ def make_output(state, line, url_delegate):
part = "" part = ""
else: else:
part += c part += c
escape = False
if i == len(line)-1: if i == len(line)-1:
if len(part) > 0: if len(part) > 0:
@ -608,7 +697,7 @@ class LinkSpec(urwid.AttrSpec):
self.link_target = link_target self.link_target = link_target
self.link_fields = None self.link_fields = None
urwid.AttrSpec.__init__(self, orig_spec.foreground, orig_spec.background) super().__init__(orig_spec.foreground, orig_spec.background)
class LinkableText(urwid.Text): class LinkableText(urwid.Text):
@ -618,7 +707,7 @@ class LinkableText(urwid.Text):
signals = ["click", "change"] signals = ["click", "change"]
def __init__(self, text, align=None, cursor_position=0, delegate=None): def __init__(self, text, align=None, cursor_position=0, delegate=None):
self.__super.__init__(text, align=align) super().__init__(text, align=align)
self.delegate = delegate self.delegate = delegate
self._cursor_position = 0 self._cursor_position = 0
self.key_timeout = 3 self.key_timeout = 3
@ -729,7 +818,7 @@ class LinkableText(urwid.Text):
def render(self, size, focus=False): def render(self, size, focus=False):
now = time.time() now = time.time()
c = self.__super.render(size, focus) c = super().render(size, focus)
if focus and (self.delegate == None or now < self.delegate.last_keypress+self.key_timeout): if focus and (self.delegate == None or now < self.delegate.last_keypress+self.key_timeout):
c = urwid.CompositeCanvas(c) c = urwid.CompositeCanvas(c)

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
@ -51,7 +52,7 @@ class ListEntry(urwid.Text):
class AnnounceInfo(urwid.WidgetWrap): class AnnounceInfo(urwid.WidgetWrap):
def keypress(self, size, key): def keypress(self, size, key):
if key == "esc": if key == "esc":
options = self.parent.left_pile.options(height_type="weight", height_amount=1) options = self.parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options) self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options)
else: else:
return super(AnnounceInfo, self).keypress(size, key) return super(AnnounceInfo, self).keypress(size, key)
@ -114,7 +115,7 @@ class AnnounceInfo(urwid.WidgetWrap):
style = "list_untrusted" style = "list_untrusted"
def show_announce_stream(sender): def show_announce_stream(sender):
options = self.parent.left_pile.options(height_type="weight", height_amount=1) options = self.parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options) self.parent.left_pile.contents[0] = (self.parent.announce_stream_display, options)
def connect(sender): def connect(sender):
@ -186,54 +187,66 @@ class AnnounceInfo(urwid.WidgetWrap):
RNS.log("Error while setting active propagation node from announce. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while setting active propagation node from announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
if is_node: if is_node:
type_button = ("weight", 0.45, urwid.Button("Connect", on_press=connect)) type_button = (urwid.WEIGHT, 0.45, urwid.Button("Connect", on_press=connect))
msg_button = ("weight", 0.45, urwid.Button("Msg Op", on_press=msg_op)) msg_button = (urwid.WEIGHT, 0.45, urwid.Button("Msg Op", on_press=msg_op))
save_button = ("weight", 0.45, urwid.Button("Save", on_press=save_node)) save_button = (urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=save_node))
elif is_pn: elif is_pn:
type_button = ("weight", 0.45, urwid.Button("Use as default", on_press=use_pn)) type_button = (urwid.WEIGHT, 0.45, urwid.Button("Use as default", on_press=use_pn))
save_button = None save_button = None
else: else:
type_button = ("weight", 0.45, urwid.Button("Converse", on_press=converse)) type_button = (urwid.WEIGHT, 0.45, urwid.Button("Converse", on_press=converse))
save_button = None save_button = None
if is_node: if is_node:
button_columns = urwid.Columns([("weight", 0.45, urwid.Button("Back", on_press=show_announce_stream)), ("weight", 0.1, urwid.Text("")), type_button, ("weight", 0.1, urwid.Text("")), msg_button, ("weight", 0.1, urwid.Text("")), save_button]) button_columns = urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Back", on_press=show_announce_stream)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
type_button,
(urwid.WEIGHT, 0.1, urwid.Text("")),
msg_button,
(urwid.WEIGHT, 0.1, urwid.Text("")),
save_button,
])
else: else:
button_columns = urwid.Columns([("weight", 0.45, urwid.Button("Back", on_press=show_announce_stream)), ("weight", 0.1, urwid.Text("")), type_button]) button_columns = urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Back", on_press=show_announce_stream)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
type_button,
])
pile_widgets = [] pile_widgets = []
if is_pn: if is_pn:
pile_widgets = [ pile_widgets = [
urwid.Text("Time : "+ts_string, align="left"), urwid.Text("Time : "+ts_string, align=urwid.LEFT),
urwid.Text("Addr : "+addr_str, align="left"), urwid.Text("Addr : "+addr_str, align=urwid.LEFT),
urwid.Text("Type : "+type_string, align="left"), urwid.Text("Type : "+type_string, align=urwid.LEFT),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
button_columns button_columns
] ]
else: else:
pile_widgets = [ pile_widgets = [
urwid.Text("Time : "+ts_string, align="left"), urwid.Text("Time : "+ts_string, align=urwid.LEFT),
urwid.Text("Addr : "+addr_str, align="left"), urwid.Text("Addr : "+addr_str, align=urwid.LEFT),
urwid.Text("Type : "+type_string, align="left"), urwid.Text("Type : "+type_string, align=urwid.LEFT),
urwid.Text("Name : "+display_str, align="left"), urwid.Text("Name : "+display_str, align=urwid.LEFT),
urwid.Text(["Trust : ", (style, trust_str)], align="left"), urwid.Text(["Trust : ", (style, trust_str)], align=urwid.LEFT),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Text(["Announce Data: \n", (data_style, data_str)], align="left"), urwid.Text(["Announce Data: \n", (data_style, data_str)], align=urwid.LEFT),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
button_columns button_columns
] ]
if is_node: if is_node:
operator_entry = urwid.Text("Oprtr : "+op_str, align="left") operator_entry = urwid.Text("Oprtr : "+op_str, align=urwid.LEFT)
pile_widgets.insert(4, operator_entry) pile_widgets.insert(4, operator_entry)
pile = urwid.Pile(pile_widgets) pile = urwid.Pile(pile_widgets)
self.display_widget = urwid.Filler(pile, valign="top", height="pack") self.display_widget = urwid.Filler(pile, valign=urwid.TOP, height=urwid.PACK)
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Announce Info")) super().__init__(urwid.LineBox(self.display_widget, title="Announce Info"))
class AnnounceStreamEntry(urwid.WidgetWrap): class AnnounceStreamEntry(urwid.WidgetWrap):
@ -295,13 +308,13 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
urwid.connect_signal(widget, "click", self.display_announce, announce) urwid.connect_signal(widget, "click", self.display_announce, announce)
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def display_announce(self, event, announce): def display_announce(self, event, announce):
try: try:
parent = self.app.ui.main_display.sub_displays.network_display parent = self.app.ui.main_display.sub_displays.network_display
info_widget = AnnounceInfo(announce, parent, self.app) info_widget = AnnounceInfo(announce, parent, self.app)
options = parent.left_pile.options(height_type="weight", height_amount=1) options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
except KeyError as e: except KeyError as e:
@ -312,17 +325,21 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
def close_req(sender): def close_req(sender):
self.delegate.parent.close_list_dialogs() self.delegate.parent.close_list_dialogs()
dialog_pile.contents[0] = (urwid.Text("\nKeys requested from network\n", align="center"), options) dialog_pile.contents[0] = (urwid.Text("\nKeys requested from network\n", align=urwid.CENTER), options)
RNS.Transport.request_path(announce[1]) RNS.Transport.request_path(announce[1])
confirmed_button = urwid.Button("Request keys", on_press=confirmed) confirmed_button = urwid.Button("Request keys", on_press=confirmed)
dialog_pile = urwid.Pile([ dialog_pile = urwid.Pile([
urwid.Text("The keys for the announced destination could not be recalled. You can wait for an announce to arrive, or request the keys from the network.\n", align="center"), urwid.Text(
"The keys for the announced destination could not be recalled. "
"You can wait for an announce to arrive, or request the keys from the network.\n",
align=urwid.CENTER,
),
urwid.Columns([ urwid.Columns([
("weight", 0.45, confirmed_button), (urwid.WEIGHT, 0.45, confirmed_button),
("weight", 0.1, urwid.Text("")), (urwid.WEIGHT, 0.1, urwid.Text("")),
("weight", 0.45, urwid.Button("Close", on_press=dismiss_dialog)), (urwid.WEIGHT, 0.45, urwid.Button("Close", on_press=dismiss_dialog)),
]) ])
]) ])
@ -334,14 +351,27 @@ class AnnounceStreamEntry(urwid.WidgetWrap):
dialog.delegate = self.delegate.parent dialog.delegate = self.delegate.parent
bottom = self.delegate bottom = self.delegate
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.delegate.parent.left_pile.options("weight", 1) options = self.delegate.parent.left_pile.options(urwid.WEIGHT, 1)
self.delegate.parent.left_pile.contents[0] = (overlay, options) self.delegate.parent.left_pile.contents[0] = (overlay, options)
def timestamp(self): def timestamp(self):
return self.timestamp return self.timestamp
class TabButton(urwid.Button):
button_left = urwid.Text("[")
button_right = urwid.Text("]")
class AnnounceStream(urwid.WidgetWrap): class AnnounceStream(urwid.WidgetWrap):
def __init__(self, app, parent): def __init__(self, app, parent):
self.app = app self.app = app
@ -350,11 +380,24 @@ class AnnounceStream(urwid.WidgetWrap):
self.timeout = self.app.config["textui"]["animation_interval"]*2 self.timeout = self.app.config["textui"]["animation_interval"]*2
self.ilb = None self.ilb = None
self.no_content = True self.no_content = True
self.current_tab = "nodes"
self.added_entries = [] self.added_entries = []
self.widget_list = [] self.widget_list = []
self.update_widget_list() self.update_widget_list()
# Create tab buttons
self.tab_nodes = TabButton("Nodes", on_press=self.show_nodes_tab)
self.tab_peers = TabButton("Peers", on_press=self.show_peers_tab)
self.tab_pn = TabButton("Propagation Nodes", on_press=self.show_pn_tab)
# Create tab bar with proportional widths
self.tab_bar = urwid.Columns([
('weight', 1, self.tab_nodes),
('weight', 1, self.tab_peers),
('weight', 3, self.tab_pn),
], dividechars=1) # Add 1 character spacing between tabs
self.ilb = ExceptionHandlingListBox( self.ilb = ExceptionHandlingListBox(
self.widget_list, self.widget_list,
on_selection_change=self.list_selection, on_selection_change=self.list_selection,
@ -363,12 +406,18 @@ class AnnounceStream(urwid.WidgetWrap):
#highlight_offFocus="list_off_focus" #highlight_offFocus="list_off_focus"
) )
self.display_widget = self.ilb # Combine tab bar and list box
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Announce Stream")) self.pile = urwid.Pile([
('pack', self.tab_bar),
('weight', 1, self.ilb),
])
self.display_widget = self.pile
super().__init__(urwid.LineBox(self.display_widget, title="Announce Stream"))
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.focus_position = "header"
elif key == "ctrl x": elif key == "ctrl x":
self.delete_selected_entry() self.delete_selected_entry()
@ -386,28 +435,45 @@ class AnnounceStream(urwid.WidgetWrap):
self.update_widget_list() self.update_widget_list()
def update_widget_list(self): def update_widget_list(self):
self.widget_list = []
new_entries = [] new_entries = []
for e in self.app.directory.announce_stream: for e in self.app.directory.announce_stream:
if not e[0] in self.added_entries: announce_type = e[3]
self.added_entries.insert(0, e[0])
new_entries.insert(0, e) # Filter based on current tab
if self.current_tab == "nodes" and (announce_type == "node" or announce_type == True):
new_entries.append(e)
elif self.current_tab == "peers" and (announce_type == "peer" or announce_type == False):
new_entries.append(e)
elif self.current_tab == "pn" and announce_type == "pn":
new_entries.append(e)
for e in new_entries: for e in new_entries:
nw = AnnounceStreamEntry(self.app, e, self) nw = AnnounceStreamEntry(self.app, e, self)
nw.timestamp = e[0] nw.timestamp = e[0]
self.widget_list.insert(0, nw) self.widget_list.append(nw)
if len(new_entries) > 0: if len(new_entries) > 0:
self.no_content = False self.no_content = False
if self.ilb != None:
self.ilb.set_body(self.widget_list)
else: else:
if len(self.widget_list) == 0: self.no_content = True
self.no_content = True self.widget_list = [urwid.Text(f"No {self.current_tab} announces", align='center')]
if self.ilb != None: if self.ilb:
self.ilb.set_body(self.widget_list) self.ilb.set_body(self.widget_list)
def show_nodes_tab(self, button):
self.current_tab = "nodes"
self.update_widget_list()
def show_peers_tab(self, button):
self.current_tab = "peers"
self.update_widget_list()
def show_pn_tab(self, button):
self.current_tab = "pn"
self.update_widget_list()
def list_selection(self, arg1, arg2): def list_selection(self, arg1, arg2):
pass pass
@ -463,7 +529,7 @@ class ListDialogLineBox(urwid.LineBox):
class KnownNodeInfo(urwid.WidgetWrap): class KnownNodeInfo(urwid.WidgetWrap):
def keypress(self, size, key): def keypress(self, size, key):
if key == "esc": if key == "esc":
options = self.parent.left_pile.options(height_type="weight", height_amount=1) options = self.parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.parent.left_pile.contents[0] = (self.parent.known_nodes_display, options) self.parent.left_pile.contents[0] = (self.parent.known_nodes_display, options)
else: else:
return super(KnownNodeInfo, self).keypress(size, key) return super(KnownNodeInfo, self).keypress(size, key)
@ -480,6 +546,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:
@ -540,6 +612,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
@ -551,7 +624,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
op_str = "Unknown" op_str = "Unknown"
def show_known_nodes(sender): def show_known_nodes(sender):
options = self.parent.left_pile.options(height_type="weight", height_amount=1) options = self.parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.parent.left_pile.contents[0] = (self.parent.known_nodes_display, options) self.parent.left_pile.contents[0] = (self.parent.known_nodes_display, options)
def connect(sender): def connect(sender):
@ -604,8 +677,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()
@ -614,20 +695,21 @@ class KnownNodeInfo(urwid.WidgetWrap):
show_known_nodes(None) show_known_nodes(None)
back_button = ("weight", 0.2, urwid.Button("Back", on_press=show_known_nodes)) back_button = (urwid.WEIGHT, 0.2, urwid.Button("Back", on_press=show_known_nodes))
connect_button = ("weight", 0.2, urwid.Button("Connect", on_press=connect)) connect_button = (urwid.WEIGHT, 0.2, urwid.Button("Connect", on_press=connect))
save_button = ("weight", 0.2, urwid.Button("Save", on_press=save_node)) save_button = (urwid.WEIGHT, 0.2, urwid.Button("Save", on_press=save_node))
msg_button = ("weight", 0.2, urwid.Button("Msg Op", on_press=msg_op)) msg_button = (urwid.WEIGHT, 0.2, urwid.Button("Msg Op", on_press=msg_op))
bdiv = ("weight", 0.02, urwid.Text("")) bdiv = (urwid.WEIGHT, 0.02, urwid.Text(""))
button_columns = urwid.Columns([back_button, bdiv, connect_button, bdiv, msg_button, bdiv, save_button]) button_columns = urwid.Columns([back_button, bdiv, connect_button, bdiv, msg_button, bdiv, save_button])
pile_widgets = [ pile_widgets = [
urwid.Text("Type : "+type_string, align="left"), urwid.Text("Type : "+type_string, align=urwid.LEFT),
e_name, e_name,
urwid.Text("Node Addr : "+addr_str, align="left"), urwid.Text("Node Addr : "+addr_str, align=urwid.LEFT),
e_sort,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Text(lxmf_addr_str, align="center"), urwid.Text(lxmf_addr_str, align=urwid.CENTER),
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
propagation_node_checkbox, propagation_node_checkbox,
connect_identify_checkbox, connect_identify_checkbox,
@ -639,7 +721,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
button_columns button_columns
] ]
operator_entry = urwid.Text("Operator : "+op_str, align="left") operator_entry = urwid.Text("Operator : "+op_str, align=urwid.LEFT)
pile_widgets.insert(3, operator_entry) pile_widgets.insert(3, operator_entry)
hops = RNS.Transport.hops_to(source_hash) hops = RNS.Transport.hops_to(source_hash)
@ -653,7 +735,7 @@ class KnownNodeInfo(urwid.WidgetWrap):
else: else:
hops_str = "Unknown" hops_str = "Unknown"
operator_entry = urwid.Text("Distance : "+hops_str, align="left") operator_entry = urwid.Text("Distance : "+hops_str, align=urwid.LEFT)
pile_widgets.insert(4, operator_entry) pile_widgets.insert(4, operator_entry)
pile = urwid.Pile(pile_widgets) pile = urwid.Pile(pile_widgets)
@ -662,9 +744,9 @@ class KnownNodeInfo(urwid.WidgetWrap):
button_columns.focus_position = 0 button_columns.focus_position = 0
self.display_widget = urwid.Filler(pile, valign="top", height="pack") self.display_widget = urwid.Filler(pile, valign=urwid.TOP, height=urwid.PACK)
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Node Info")) super().__init__(urwid.LineBox(self.display_widget, title="Node Info"))
# Yes, this is weird. There is a bug in Urwid/ILB that causes # Yes, this is weird. There is a bug in Urwid/ILB that causes
@ -677,9 +759,9 @@ class ExceptionHandlingListBox(IndicativeListBox):
except Exception as e: except Exception as e:
if key == "up": if key == "up":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
elif key == "down": elif key == "down":
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.left_pile.set_focus(1) nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.left_pile.focus_position = 1
else: else:
RNS.log("An error occurred while processing an interface event. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("An error occurred while processing an interface event. The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -706,14 +788,23 @@ class KnownNodes(urwid.WidgetWrap):
else: else:
self.no_content = True self.no_content = True
widget_style = "inactive_text" widget_style = "inactive_text"
self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are saved\n\nCtrl+L to view the announce stream\n\n"), align="center")]) self.pile = urwid.Pile([
self.display_widget = urwid.Filler(self.pile, valign="top", height="pack") urwid.Text(("warning_text", g["info"]+"\n"), align=urwid.CENTER),
SelectText(
(
"warning_text",
"Currently, no nodes are saved\n\nCtrl+L to view the announce stream\n\n",
),
align=urwid.CENTER,
),
])
self.display_widget = urwid.Filler(self.pile, valign=urwid.TOP, height=urwid.PACK)
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Saved Nodes"), widget_style)) super().__init__(urwid.AttrMap(urwid.LineBox(self.display_widget, title="Saved Nodes"), 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.focus_position = "header"
elif key == "ctrl x": elif key == "ctrl x":
self.delete_selected_entry() self.delete_selected_entry()
@ -740,27 +831,27 @@ class KnownNodes(urwid.WidgetWrap):
def show_info(sender): def show_info(sender):
info_widget = KnownNodeInfo(source_hash) info_widget = KnownNodeInfo(source_hash)
options = parent.left_pile.options(height_type="weight", height_amount=1) options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
dialog = ListDialogLineBox( dialog = ListDialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Connect to node\n"+self.app.directory.simplest_display_str(source_hash)+"\n", align="center"), urwid.Text("Connect to node\n"+self.app.directory.simplest_display_str(source_hash)+"\n", align=urwid.CENTER),
urwid.Columns([ urwid.Columns([
("weight", 0.45, urwid.Button("Yes", on_press=confirmed)), (urwid.WEIGHT, 0.45, urwid.Button("Yes", on_press=confirmed)),
("weight", 0.1, urwid.Text("")), (urwid.WEIGHT, 0.1, urwid.Text("")),
("weight", 0.45, urwid.Button("No", on_press=dismiss_dialog)), (urwid.WEIGHT, 0.45, urwid.Button("No", on_press=dismiss_dialog)),
("weight", 0.1, urwid.Text("")), (urwid.WEIGHT, 0.1, urwid.Text("")),
("weight", 0.45, urwid.Button("Info", on_press=show_info))]) (urwid.WEIGHT, 0.45, urwid.Button("Info", on_press=show_info))])
]), title="?" ]), title="?"
) )
dialog.delegate = self.delegate dialog.delegate = self.delegate
bottom = self bottom = self
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=2, right=2)
options = self.delegate.left_pile.options("weight", 1) options = self.delegate.left_pile.options(urwid.WEIGHT, 1)
self.delegate.left_pile.contents[0] = (overlay, options) self.delegate.left_pile.contents[0] = (overlay, options)
def delete_selected_entry(self): def delete_selected_entry(self):
@ -779,16 +870,29 @@ class KnownNodes(urwid.WidgetWrap):
dialog = ListDialogLineBox( dialog = ListDialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("Delete Node\n"+self.app.directory.simplest_display_str(source_hash)+"\n", align="center"), urwid.Text("Delete Node\n"+self.app.directory.simplest_display_str(source_hash)+"\n", align=urwid.CENTER),
urwid.Columns([("weight", 0.45, urwid.Button("Yes", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("No", on_press=dismiss_dialog))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Yes", on_press=confirmed)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("No", on_press=dismiss_dialog)),
])
]), title="?" ]), title="?"
) )
dialog.delegate = self.delegate dialog.delegate = self.delegate
bottom = self bottom = self
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) overlay = urwid.Overlay(
dialog,
bottom,
align=urwid.CENTER,
width=urwid.RELATIVE_100,
valign=urwid.MIDDLE,
height=urwid.PACK,
left=2,
right=2,
)
options = self.delegate.left_pile.options("weight", 1) options = self.delegate.left_pile.options(urwid.WEIGHT, 1)
self.delegate.left_pile.contents[0] = (overlay, options) self.delegate.left_pile.contents[0] = (overlay, options)
@ -852,7 +956,7 @@ class NodeEntry(urwid.WidgetWrap):
self.display_widget = urwid.AttrMap(widget, style, focus_style) self.display_widget = urwid.AttrMap(widget, style, focus_style)
self.display_widget.source_hash = source_hash self.display_widget.source_hash = source_hash
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
class AnnounceTime(urwid.WidgetWrap): class AnnounceTime(urwid.WidgetWrap):
@ -863,7 +967,7 @@ class AnnounceTime(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_time() self.update_time()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_time(self): def update_time(self):
self.last_announce_string = "Never" self.last_announce_string = "Never"
@ -895,7 +999,7 @@ class NodeAnnounceTime(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_time() self.update_time()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_time(self): def update_time(self):
self.last_announce_string = "Never" self.last_announce_string = "Never"
@ -926,7 +1030,7 @@ class NodeActiveConnections(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_stat() self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_stat(self): def update_stat(self):
self.stat_string = "None" self.stat_string = "None"
@ -957,16 +1061,16 @@ class NodeStorageStats(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_stat() self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
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)
@ -1000,7 +1104,7 @@ class NodeTotalConnections(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_stat() self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_stat(self): def update_stat(self):
self.stat_string = "None" self.stat_string = "None"
@ -1032,7 +1136,7 @@ class NodeTotalPages(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_stat() self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_stat(self): def update_stat(self):
self.stat_string = "None" self.stat_string = "None"
@ -1064,7 +1168,7 @@ class NodeTotalFiles(urwid.WidgetWrap):
self.display_widget = urwid.Text("") self.display_widget = urwid.Text("")
self.update_stat() self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update_stat(self): def update_stat(self):
self.stat_string = "None" self.stat_string = "None"
@ -1113,45 +1217,45 @@ class LocalPeer(urwid.WidgetWrap):
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("\n\n\nSaved\n\n", align="center"), urwid.Text("\n\n\nSaved\n\n", align=urwid.CENTER),
urwid.Button("OK", on_press=dismiss_dialog) urwid.Button("OK", on_press=dismiss_dialog)
]), title=g["info"] ]), title=g["info"]
) )
dialog.delegate = self dialog.delegate = self
bottom = self bottom = self
#overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=4, right=4) #overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4)
overlay = dialog overlay = dialog
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.dialog_open = True self.dialog_open = True
self.parent.left_pile.contents[1] = (overlay, options) self.parent.left_pile.contents[1] = (overlay, options)
def announce_query(sender): def announce_query(sender):
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.dialog_open = False self.dialog_open = False
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options)
self.app.announce_now() self.app.announce_now()
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("\n\n\nAnnounce Sent\n\n\n", align="center"), urwid.Text("\n\n\nAnnounce Sent\n\n\n", align=urwid.CENTER),
urwid.Button("OK", on_press=dismiss_dialog) urwid.Button("OK", on_press=dismiss_dialog)
]), title=g["info"] ]), title=g["info"]
) )
dialog.delegate = self dialog.delegate = self
bottom = self bottom = self
#overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=4, right=4) #overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4)
overlay = dialog overlay = dialog
self.dialog_open = True self.dialog_open = True
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options) self.parent.left_pile.contents[1] = (overlay, options)
def node_info_query(sender): def node_info_query(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (self.parent.node_info_display, options) self.parent.left_pile.contents[1] = (self.parent.node_info_display, options)
if LocalPeer.announce_timer == None: if LocalPeer.announce_timer == None:
@ -1172,11 +1276,15 @@ class LocalPeer(urwid.WidgetWrap):
self.t_last_announce, self.t_last_announce,
announce_button, announce_button,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=save_query)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Node Info", on_press=node_info_query))]) urwid.Columns([
(urwid.WEIGHT, 0.45, urwid.Button("Save", on_press=save_query)),
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("Node Info", on_press=node_info_query)),
])
] ]
) )
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Local Peer Info")) super().__init__(urwid.LineBox(self.display_widget, title="Local Peer Info"))
def start(self): def start(self):
self.t_last_announce.start() self.t_last_announce.start()
@ -1200,7 +1308,7 @@ class NodeInfo(urwid.WidgetWrap):
widget_style = "" widget_style = ""
def show_peer_info(sender): def show_peer_info(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[1] = (LocalPeer(self.app, self.parent), options)
if self.app.enable_node: if self.app.enable_node:
@ -1224,25 +1332,25 @@ class NodeInfo(urwid.WidgetWrap):
def announce_query(sender): def announce_query(sender):
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.dialog_open = False self.dialog_open = False
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (NodeInfo(self.app, self.parent), options) self.parent.left_pile.contents[1] = (NodeInfo(self.app, self.parent), options)
self.app.node.announce() self.app.node.announce()
dialog = DialogLineBox( dialog = DialogLineBox(
urwid.Pile([ urwid.Pile([
urwid.Text("\n\n\nAnnounce Sent\n\n", align="center"), urwid.Text("\n\n\nAnnounce Sent\n\n", align=urwid.CENTER),
urwid.Button("OK", on_press=dismiss_dialog) urwid.Button("OK", on_press=dismiss_dialog)
]), title=g["info"] ]), title=g["info"]
) )
dialog.delegate = self dialog.delegate = self
bottom = self bottom = self
#overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=4, right=4) #overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=4, right=4)
overlay = dialog overlay = dialog
self.dialog_open = True self.dialog_open = True
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type=urwid.PACK, height_amount=None)
self.parent.left_pile.contents[1] = (overlay, options) self.parent.left_pile.contents[1] = (overlay, options)
def connect_query(sender): def connect_query(sender):
@ -1291,18 +1399,41 @@ class NodeInfo(urwid.WidgetWrap):
self.t_total_files.update_stat() self.t_total_files.update_stat()
lxmf_addr_str = g["sent"]+" LXMF Propagation Node Address is "+RNS.prettyhexrep(RNS.Destination.hash_from_name_and_identity("lxmf.propagation", self.app.node.destination.identity)) lxmf_addr_str = g["sent"]+" LXMF Propagation Node Address is "+RNS.prettyhexrep(RNS.Destination.hash_from_name_and_identity("lxmf.propagation", self.app.node.destination.identity))
e_lxmf = urwid.Text(lxmf_addr_str, align="center") e_lxmf = urwid.Text(lxmf_addr_str, align=urwid.CENTER)
announce_button = urwid.Button("Announce", on_press=announce_query) announce_button = urwid.Button("Announce", on_press=announce_query)
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)
pile = urwid.Pile([ if not self.app.disable_propagation:
pile = urwid.Pile([
t_id,
e_name,
urwid.Divider(g["divider1"]),
e_lxmf,
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([
(urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
(urwid.WEIGHT, 0.5, urwid.Text("")),
(urwid.WEIGHT, 6, connect_button),
(urwid.WEIGHT, 0.5, urwid.Text("")),
(urwid.WEIGHT, 8, reset_button),
(urwid.WEIGHT, 0.5, urwid.Text("")),
(urwid.WEIGHT, 7, announce_button),
])
])
else:
pile = urwid.Pile([
t_id, t_id,
e_name, e_name,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
e_lxmf,
urwid.Divider(g["divider1"]),
self.t_last_announce, self.t_last_announce,
self.t_storage_stats, self.t_storage_stats,
self.t_active_links, self.t_active_links,
@ -1311,25 +1442,25 @@ class NodeInfo(urwid.WidgetWrap):
self.t_total_files, self.t_total_files,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Columns([ urwid.Columns([
("weight", 5, urwid.Button("Back", on_press=show_peer_info)), (urwid.WEIGHT, 5, urwid.Button("Back", on_press=show_peer_info)),
("weight", 0.5, urwid.Text("")), (urwid.WEIGHT, 0.5, urwid.Text("")),
("weight", 6, connect_button), (urwid.WEIGHT, 6, connect_button),
("weight", 0.5, urwid.Text("")), (urwid.WEIGHT, 0.5, urwid.Text("")),
("weight", 8, reset_button), (urwid.WEIGHT, 8, reset_button),
("weight", 0.5, urwid.Text("")), (urwid.WEIGHT, 0.5, urwid.Text("")),
("weight", 7, announce_button), (urwid.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=urwid.CENTER),
urwid.Text("\nThis instance is not hosting a node\n\n", align="center"), urwid.Text("\nThis instance is not hosting a node\n\n", align=urwid.CENTER),
urwid.Padding(urwid.Button("Back", on_press=show_peer_info), "center", "pack") urwid.Padding(urwid.Button("Back", on_press=show_peer_info), urwid.CENTER, urwid.PACK)
]) ])
self.display_widget = pile self.display_widget = pile
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Local Node Info"), widget_style)) super().__init__(urwid.AttrMap(urwid.LineBox(self.display_widget, title="Local Node Info"), widget_style))
def start(self): def start(self):
if self.app.node != None: if self.app.node != None:
@ -1352,7 +1483,7 @@ class UpdatingText(urwid.WidgetWrap):
self.append_text = append_text self.append_text = append_text
self.update() self.update()
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def update(self): def update(self):
self.value = self.value_method() self.value = self.value_method()
@ -1394,7 +1525,7 @@ class NetworkStats(urwid.WidgetWrap):
self.display_widget = urwid.LineBox(pile, title="Network Stats") self.display_widget = urwid.LineBox(pile, title="Network Stats")
urwid.WidgetWrap.__init__(self, self.display_widget) super().__init__(self.display_widget)
def start(self): def start(self):
self.w_heard_peers.start() self.w_heard_peers.start()
@ -1446,9 +1577,9 @@ class NetworkDisplay():
self.list_display = 1 self.list_display = 1
self.left_pile = NetworkLeftPile([ self.left_pile = NetworkLeftPile([
("weight", 1, self.known_nodes_display), (urwid.WEIGHT, 1, self.known_nodes_display),
# ("pack", self.network_stats_display), # (urwid.PACK, self.network_stats_display),
("pack", self.local_peer_display), (urwid.PACK, self.local_peer_display),
]) ])
self.left_pile.parent = self self.left_pile.parent = self
@ -1459,10 +1590,10 @@ class NetworkDisplay():
self.columns = urwid.Columns( self.columns = urwid.Columns(
[ [
# ("weight", NetworkDisplay.list_width, self.left_area), # (urwid.WEIGHT, NetworkDisplay.list_width, self.left_area),
# ("weight", self.right_area_width, self.right_area) # (urwid.WEIGHT, self.right_area_width, self.right_area)
(NetworkDisplay.given_list_width, self.left_area), (NetworkDisplay.given_list_width, self.left_area),
("weight", 1, self.right_area) (urwid.WEIGHT, 1, self.right_area)
], ],
dividechars=0, focus_column=0 dividechars=0, focus_column=0
) )
@ -1472,11 +1603,11 @@ class NetworkDisplay():
def toggle_list(self): def toggle_list(self):
if self.list_display != 0: if self.list_display != 0:
options = self.left_pile.options(height_type="weight", height_amount=1) options = self.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.left_pile.contents[0] = (self.announce_stream_display, options) self.left_pile.contents[0] = (self.announce_stream_display, options)
self.list_display = 0 self.list_display = 0
else: else:
options = self.left_pile.options(height_type="weight", height_amount=1) options = self.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.left_pile.contents[0] = (self.known_nodes_display, options) self.left_pile.contents[0] = (self.known_nodes_display, options)
self.list_display = 1 self.list_display = 1
@ -1491,7 +1622,7 @@ class NetworkDisplay():
self.widget.contents[0] = (self.left_area, options) self.widget.contents[0] = (self.left_area, options)
def show_peers(self): def show_peers(self):
options = self.left_pile.options(height_type="weight", height_amount=1) options = self.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.left_pile.contents[0] = (self.lxmf_peers_display, options) self.left_pile.contents[0] = (self.lxmf_peers_display, options)
if self.list_display != 0: if self.list_display != 0:
@ -1503,12 +1634,12 @@ class NetworkDisplay():
if self.list_display == 1: if self.list_display == 1:
parent = self.app.ui.main_display.sub_displays.network_display parent = self.app.ui.main_display.sub_displays.network_display
selected_node_entry = parent.known_nodes_display.ilb.get_selected_item() selected_node_entry = parent.known_nodes_display.ilb.get_selected_item()
if selected_node_entry != None: if selected_node_entry is not None:
selected_node_hash = selected_node_entry._get_base_widget().display_widget.source_hash selected_node_hash = selected_node_entry.base_widget.display_widget.source_hash
if selected_node_hash != None: if selected_node_hash is not None:
info_widget = KnownNodeInfo(selected_node_hash) info_widget = KnownNodeInfo(selected_node_hash)
options = parent.left_pile.options(height_type="weight", height_amount=1) options = parent.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
parent.left_pile.contents[0] = (info_widget, options) parent.left_pile.contents[0] = (info_widget, options)
def focus_lists(self): def focus_lists(self):
@ -1521,16 +1652,22 @@ class NetworkDisplay():
self.announce_stream_display.rebuild_widget_list() self.announce_stream_display.rebuild_widget_list()
def reinit_lxmf_peers(self): def reinit_lxmf_peers(self):
if self.lxmf_peers_display:
si = self.lxmf_peers_display.ilb.get_selected_position()
else:
si = None
self.lxmf_peers_display = LXMFPeers(self.app) self.lxmf_peers_display = LXMFPeers(self.app)
self.lxmf_peers_display.delegate = self self.lxmf_peers_display.delegate = self
self.close_list_dialogs() self.close_list_dialogs()
if si != None:
self.lxmf_peers_display.ilb.select_item(si)
def close_list_dialogs(self): def close_list_dialogs(self):
if self.list_display == 0: if self.list_display == 0:
options = self.left_pile.options(height_type="weight", height_amount=1) options = self.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.left_pile.contents[0] = (self.announce_stream_display, options) self.left_pile.contents[0] = (self.announce_stream_display, options)
else: else:
options = self.left_pile.options(height_type="weight", height_amount=1) options = self.left_pile.options(height_type=urwid.WEIGHT, height_amount=1)
self.left_pile.contents[0] = (self.known_nodes_display, options) self.left_pile.contents[0] = (self.known_nodes_display, options)
def start(self): def start(self):
@ -1573,20 +1710,25 @@ class LXMFPeers(urwid.WidgetWrap):
else: else:
self.no_content = True self.no_content = True
widget_style = "inactive_text" widget_style = "inactive_text"
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([
self.display_widget = urwid.Filler(self.pile, valign="top", height="pack") urwid.Text(("warning_text", g["info"]+"\n"), align=urwid.CENTER),
SelectText(("warning_text", "Currently, no LXMF nodes are peered\n\n"), align=urwid.CENTER),
])
self.display_widget = urwid.Filler(self.pile, valign=urwid.TOP, height=urwid.PACK)
if hasattr(self, "peer_list") and self.peer_list: if hasattr(self, "peer_list") and self.peer_list:
pl = len(self.peer_list) pl = len(self.peer_list)
else: else:
pl = 0 pl = 0
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title=f"LXMF Propagation Peers ({pl})"), widget_style)) super().__init__(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.focus_position = "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)
@ -1602,6 +1744,48 @@ 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=urwid.CENTER),
urwid.Columns([
(urwid.WEIGHT, 0.1, urwid.Text("")),
(urwid.WEIGHT, 0.45, urwid.Button("OK", on_press=dismiss_dialog)),
])
]),
title="!",
)
dialog.delegate = self.delegate
bottom = self
overlay = urwid.Overlay(dialog, bottom, align=urwid.CENTER, width=urwid.RELATIVE_100, valign=urwid.MIDDLE, height=urwid.PACK, left=2, right=2)
options = self.delegate.left_pile.options(urwid.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
@ -1615,17 +1799,18 @@ class LXMFPeers(urwid.WidgetWrap):
def make_peer_widgets(self): def make_peer_widgets(self):
widget_list = [] widget_list = []
sorted_peers = sorted(self.peer_list, key=lambda pid: self.peer_list[pid].link_establishment_rate, reverse=True) sorted_peers = sorted(self.peer_list, key=lambda pid: (self.app.directory.pn_trust_level(pid), self.peer_list[pid].sync_transfer_rate), reverse=True)
for peer_id in sorted_peers: 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)
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
@ -1647,19 +1832,30 @@ 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" if trust_level == DirectoryEntry.TRUSTED:
focus_style = "list_focus" 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" style = "list_unresponsive"
focus_style = "list_focus_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") if peer.propagation_transfer_limit:
# urwid.connect_signal(widget, "click", delegate.connect_node, node) txfer_limit = RNS.prettysize(peer.propagation_transfer_limit*1000)
else:
txfer_limit = "No"
ar = round(peer.acceptance_rate*100, 2)
peer_info_str = sym+" "+display_str+"\n "+alive_string+", last heard "+pretty_date(int(peer.last_heard))
peer_info_str += "\n "+str(peer.unhandled_message_count)+f" unhandled LXMs, {txfer_limit} sync limit\n"
peer_info_str += f" {RNS.prettyspeed(peer.sync_transfer_rate)} STR, "
peer_info_str += f"{RNS.prettyspeed(peer.link_establishment_rate)} LER, {ar}% AR\n"
widget = ListEntry(peer_info_str)
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) super().__init__(self.display_widget)
def pretty_date(time=False): def pretty_date(time=False):

91
nomadnet/vendor/AsciiChart.py vendored Normal file
View file

@ -0,0 +1,91 @@
from __future__ import division
from math import ceil, floor, isnan
# Derived from asciichartpy | https://github.com/kroitor/asciichart/blob/master/asciichartpy/__init__.py
class AsciiChart:
def __init__(self, glyphset="unicode"):
self.symbols = ['', '', '', '', '', '', '', '', '', '']
if glyphset == "plain":
self.symbols = ['+', '|', '-', '-', '-', '\'', ',', '.', '`', '|']
def plot(self, series, cfg=None):
if len(series) == 0:
return ''
if not isinstance(series[0], list):
if all(isnan(n) for n in series):
return ''
else:
series = [series]
cfg = cfg or {}
minimum = cfg.get('min', min(filter(lambda n: not isnan(n), [j for i in series for j in i])))
maximum = cfg.get('max', max(filter(lambda n: not isnan(n), [j for i in series for j in i])))
symbols = cfg.get('symbols', self.symbols)
if minimum > maximum:
raise ValueError('The min value cannot exceed the max value.')
interval = maximum - minimum
offset = cfg.get('offset', 3)
height = cfg.get('height', interval)
ratio = height / interval if interval > 0 else 1
min2 = int(floor(minimum * ratio))
max2 = int(ceil(maximum * ratio))
def clamp(n):
return min(max(n, minimum), maximum)
def scaled(y):
return int(round(clamp(y) * ratio) - min2)
rows = max2 - min2
width = 0
for i in range(0, len(series)):
width = max(width, len(series[i]))
width += offset
placeholder = cfg.get('format', '{:8.2f} ')
result = [[' '] * width for i in range(rows + 1)]
for y in range(min2, max2 + 1):
if callable(placeholder):
label = placeholder(maximum - ((y - min2) * interval / (rows if rows else 1))).rjust(12)
else:
label = placeholder.format(maximum - ((y - min2) * interval / (rows if rows else 1)))
result[y - min2][max(offset - len(label), 0)] = label
result[y - min2][offset - 1] = symbols[0] if y == 0 else symbols[1]
d0 = series[0][0]
if not isnan(d0):
result[rows - scaled(d0)][offset - 1] = symbols[0]
for i in range(0, len(series)):
for x in range(0, len(series[i]) - 1):
d0 = series[i][x + 0]
d1 = series[i][x + 1]
if isnan(d0) and isnan(d1):
continue
if isnan(d0) and not isnan(d1):
result[rows - scaled(d1)][x + offset] = symbols[2]
continue
if not isnan(d0) and isnan(d1):
result[rows - scaled(d0)][x + offset] = symbols[3]
continue
y0 = scaled(d0)
y1 = scaled(d1)
if y0 == y1:
result[rows - y0][x + offset] = symbols[4]
continue
result[rows - y1][x + offset] = symbols[5] if y0 > y1 else symbols[6]
result[rows - y0][x + offset] = symbols[7] if y0 > y1 else symbols[8]
start = min(y0, y1) + 1
end = max(y0, y1)
for y in range(start, end):
result[rows - y][x + offset] = symbols[9]
return '\n'.join([''.join(row).rstrip() for row in result])

View file

@ -50,7 +50,7 @@ class Scrollable(urwid.WidgetDecoration):
self._old_cursor_coords = None self._old_cursor_coords = None
self._rows_max_cached = 0 self._rows_max_cached = 0
self.force_forward_keypress = force_forward_keypress self.force_forward_keypress = force_forward_keypress
self.__super.__init__(widget) super().__init__(widget)
def render(self, size, focus=False): def render(self, size, focus=False):
maxcol, maxrow = size maxcol, maxrow = size
@ -268,10 +268,10 @@ class Scrollable(urwid.WidgetDecoration):
def _get_original_widget_size(self, size): def _get_original_widget_size(self, size):
ow = self._original_widget ow = self._original_widget
sizing = ow.sizing() sizing = ow.sizing()
if FIXED in sizing: if FLOW in sizing:
return ()
elif FLOW in sizing:
return (size[0],) return (size[0],)
elif FIXED in sizing:
return ()
def get_scrollpos(self, size=None, focus=False): def get_scrollpos(self, size=None, focus=False):
"""Current scrolling position """Current scrolling position
@ -340,7 +340,7 @@ class ScrollBar(urwid.WidgetDecoration):
""" """
if BOX not in widget.sizing(): if BOX not in widget.sizing():
raise ValueError('Not a box widget: %r' % widget) raise ValueError('Not a box widget: %r' % widget)
self.__super.__init__(widget) super().__init__(widget)
self._thumb_char = thumb_char self._thumb_char = thumb_char
self._trough_char = trough_char self._trough_char = trough_char
self.scrollbar_side = side self.scrollbar_side = side

View file

@ -0,0 +1,537 @@
import urwid
class DialogLineBox(urwid.LineBox):
def __init__(self, body, parent=None, title="?"):
super().__init__(body, title=title)
self.parent = parent
def keypress(self, size, key):
if key == "esc":
if self.parent and hasattr(self.parent, "dismiss_dialog"):
self.parent.dismiss_dialog()
return None
return super().keypress(size, key)
class Placeholder(urwid.Edit):
def __init__(self, caption="", edit_text="", placeholder="", **kwargs):
super().__init__(caption, edit_text, **kwargs)
self.placeholder = placeholder
def render(self, size, focus=False):
if not self.edit_text and not focus:
placeholder_widget = urwid.Text(("placeholder", self.placeholder))
return placeholder_widget.render(size, focus)
else:
return super().render(size, focus)
class Dropdown(urwid.WidgetWrap):
signals = ['change'] # emit for urwid.connect_signal fn
def __init__(self, label, options, default=None):
self.label = label
self.options = options
self.selected = default if default is not None else options[0]
self.main_text = f"{self.selected}"
self.main_button = urwid.SelectableIcon(self.main_text, 0)
self.main_button = urwid.AttrMap(self.main_button, "button_normal", "button_focus")
self.option_widgets = []
for opt in options:
icon = urwid.SelectableIcon(opt, 0)
icon = urwid.AttrMap(icon, "list_normal", "list_focus")
self.option_widgets.append(icon)
self.options_walker = urwid.SimpleFocusListWalker(self.option_widgets)
self.options_listbox = urwid.ListBox(self.options_walker)
self.dropdown_box = None # will be created on open_dropdown
self.pile = urwid.Pile([self.main_button])
self.dropdown_visible = False
super().__init__(self.pile)
def open_dropdown(self):
if not self.dropdown_visible:
height = len(self.options)
self.dropdown_box = urwid.BoxAdapter(self.options_listbox, height)
self.pile.contents.append((self.dropdown_box, self.pile.options()))
self.dropdown_visible = True
self.pile.focus_position = 1
self.options_walker.set_focus(0)
def close_dropdown(self):
if self.dropdown_visible:
self.pile.contents.pop() # remove the dropdown_box
self.dropdown_visible = False
self.pile.focus_position = 0
self.dropdown_box = None
def keypress(self, size, key):
if not self.dropdown_visible:
if key == "enter":
self.open_dropdown()
return None
return self.main_button.keypress(size, key)
else:
if key == "enter":
focus_result = self.options_walker.get_focus()
if focus_result is not None:
focus_widget = focus_result[0]
new_val = focus_widget.base_widget.text
old_val = self.selected
self.selected = new_val
self.main_button.base_widget.set_text(f"{self.selected}")
if old_val != new_val:
self._emit('change', new_val)
self.close_dropdown()
return None
return self.dropdown_box.keypress(size, key)
def get_value(self):
return self.selected
class ValidationError(urwid.Text):
def __init__(self, message=""):
super().__init__(("error", message))
class FormField:
def __init__(self, config_key, transform=None):
self.config_key = config_key
self.transform = transform or (lambda x: x)
class FormEdit(Placeholder, FormField):
def __init__(self, config_key, caption="", edit_text="", placeholder="", validation_types=None, transform=None, **kwargs):
Placeholder.__init__(self, caption, edit_text, placeholder, **kwargs)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
def get_value(self):
return self.transform(self.edit_text.strip())
def validate(self):
value = self.edit_text.strip()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
elif validation == "number":
if value and not value.replace('-', '').replace('.', '').isdigit():
self.error = "This field must be a number"
break
elif validation == "float":
try:
if value:
float(value)
except ValueError:
self.error = "This field must be decimal number"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormCheckbox(urwid.CheckBox, FormField):
def __init__(self, config_key, label="", state=False, validation_types=None, transform=None, **kwargs):
urwid.CheckBox.__init__(self, label, state, **kwargs)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
def get_value(self):
return self.transform(self.get_state())
def validate(self):
value = self.get_state()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormDropdown(Dropdown, FormField):
signals = ['change']
def __init__(self, config_key, label, options, default=None, validation_types=None, transform=None):
self.options = [str(opt) for opt in options]
if default is not None:
default_str = str(default)
if default_str in self.options:
default = default_str
elif transform:
try:
default_transformed = transform(default_str)
for opt in self.options:
if transform(opt) == default_transformed:
default = opt
break
except:
default = self.options[0]
else:
default = self.options[0]
else:
default = self.options[0]
Dropdown.__init__(self, label, self.options, default)
FormField.__init__(self, config_key, transform)
self.validation_types = validation_types or []
self.error_widget = urwid.Text("")
self.error = None
if hasattr(self, 'main_button'):
self.main_button.base_widget.set_text(str(default))
def get_value(self):
return self.transform(self.selected)
def validate(self):
value = self.get_value()
self.error = None
for validation in self.validation_types:
if validation == "required":
if not value:
self.error = "This field is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
def open_dropdown(self):
if not self.dropdown_visible:
super().open_dropdown()
try:
current_index = self.options.index(self.selected)
self.options_walker.set_focus(current_index)
except ValueError:
pass
class FormMultiList(urwid.Pile, FormField):
def __init__(self, config_key, placeholder="", validation_types=None, transform=None, **kwargs):
self.entries = []
self.error_widget = urwid.Text("")
self.error = None
self.placeholder = placeholder
self.validation_types = validation_types or []
first_entry = self.create_entry_row()
self.entries.append(first_entry)
self.add_button = urwid.Button("+ Add Another", on_press=self.add_entry)
add_button_padded = urwid.Padding(self.add_button, left=2, right=2)
pile_widgets = [first_entry, add_button_padded]
urwid.Pile.__init__(self, pile_widgets)
FormField.__init__(self, config_key, transform)
def create_entry_row(self):
edit = urwid.Edit("", "")
entry_row = urwid.Columns([
('weight', 1, edit),
(3, urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))),
])
return entry_row
def remove_entry(self, button, entry_row):
if len(self.entries) > 1:
self.entries.remove(entry_row)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def add_entry(self, button):
new_entry = self.create_entry_row()
self.entries.append(new_entry)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def get_pile_widgets(self):
return self.entries + [urwid.Padding(self.add_button, left=2, right=2)]
def get_value(self):
values = []
for entry in self.entries:
edit_widget = entry.contents[0][0]
value = edit_widget.edit_text.strip()
if value:
values.append(value)
return self.transform(values)
def validate(self):
values = self.get_value()
self.error = None
for validation in self.validation_types:
if validation == "required" and not values:
self.error = "At least one entry is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormMultiTable(urwid.Pile, FormField):
def __init__(self, config_key, fields, validation_types=None, transform=None, **kwargs):
self.entries = []
self.fields = fields
self.error_widget = urwid.Text("")
self.error = None
self.validation_types = validation_types or []
header_columns = [('weight', 3, urwid.Text(("list_focus", "Name")))]
for field_key, field_config in self.fields.items():
header_columns.append(('weight', 2, urwid.Text(("list_focus", field_config.get("label", field_key)))))
header_columns.append((4, urwid.Text(("list_focus", ""))))
self.header_row = urwid.Columns(header_columns)
first_entry = self.create_entry_row()
self.entries.append(first_entry)
self.add_button = urwid.Button("+ Add ", on_press=self.add_entry)
add_button_padded = urwid.Padding(self.add_button, left=2, right=2)
pile_widgets = [
self.header_row,
urwid.Divider("-"),
first_entry,
add_button_padded
]
urwid.Pile.__init__(self, pile_widgets)
FormField.__init__(self, config_key, transform)
def create_entry_row(self, name="", values=None):
if values is None:
values = {}
name_edit = urwid.Edit("", name)
columns = [('weight', 3, name_edit)]
field_widgets = {}
for field_key, field_config in self.fields.items():
field_value = values.get(field_key, "")
if field_config.get("type") == "checkbox":
widget = urwid.CheckBox("", state=bool(field_value))
elif field_config.get("type") == "dropdown":
# TODO: dropdown in MultiTable
widget = urwid.Edit("", str(field_value))
else:
widget = urwid.Edit("", str(field_value))
field_widgets[field_key] = widget
columns.append(('weight', 2, widget))
remove_button = urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))
columns.append((4, remove_button))
entry_row = urwid.Columns(columns)
entry_row.name_edit = name_edit
entry_row.field_widgets = field_widgets
return entry_row
def remove_entry(self, button, entry_row):
if len(self.entries) > 1:
self.entries.remove(entry_row)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def add_entry(self, button):
new_entry = self.create_entry_row()
self.entries.append(new_entry)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def get_pile_widgets(self):
return [
self.header_row,
urwid.Divider("-")
] + self.entries + [
urwid.Padding(self.add_button, left=2, right=2)
]
def get_value(self):
values = {}
for entry in self.entries:
name = entry.name_edit.edit_text.strip()
if name:
subinterface = {}
subinterface["interface_enabled"] = True
for field_key, widget in entry.field_widgets.items():
field_config = self.fields.get(field_key, {})
if hasattr(widget, "get_state"):
value = widget.get_state()
elif hasattr(widget, "edit_text"):
value = widget.edit_text.strip()
transform = field_config.get("transform")
if transform and value:
try:
value = transform(value)
except (ValueError, TypeError):
value = ""
if value:
subinterface[field_key] = value
values[name] = subinterface
return self.transform(values) if self.transform else values
def set_value(self, value):
self.entries = []
if not value:
self.entries.append(self.create_entry_row())
else:
for name, config in value.items():
self.entries.append(self.create_entry_row(name=name, values=config))
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def validate(self):
values = self.get_value()
self.error = None
for validation in self.validation_types:
if validation == "required" and not values:
self.error = "At least one subinterface is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None
class FormKeyValuePairs(urwid.Pile, FormField):
def __init__(self, config_key, validation_types=None, transform=None, **kwargs):
self.entries = []
self.error_widget = urwid.Text("")
self.error = None
self.validation_types = validation_types or []
header_columns = [
('weight', 1, urwid.AttrMap(urwid.Text("Parameter Key"), "multitable_header")),
('weight', 1, urwid.AttrMap(urwid.Text("Parameter Value"), "multitable_header")),
(4, urwid.AttrMap(urwid.Text("Action"), "multitable_header"))
]
self.header_row = urwid.AttrMap(urwid.Columns(header_columns), "multitable_header")
first_entry = self.create_entry_row()
self.entries.append(first_entry)
self.add_button = urwid.Button("+ Add Parameter", on_press=self.add_entry)
add_button_padded = urwid.Padding(self.add_button, left=2, right=2)
pile_widgets = [
self.header_row,
urwid.Divider("-"),
first_entry,
add_button_padded
]
urwid.Pile.__init__(self, pile_widgets)
FormField.__init__(self, config_key, transform)
def create_entry_row(self, key="", value=""):
key_edit = urwid.Edit("", key)
value_edit = urwid.Edit("", value)
remove_button = urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))
entry_row = urwid.Columns([
('weight', 1, key_edit),
('weight', 1, value_edit),
(4, remove_button)
])
entry_row.key_edit = key_edit
entry_row.value_edit = value_edit
return entry_row
def remove_entry(self, button, entry_row):
if len(self.entries) > 1:
self.entries.remove(entry_row)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def add_entry(self, button):
new_entry = self.create_entry_row()
self.entries.append(new_entry)
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def get_pile_widgets(self):
return [
self.header_row,
urwid.Divider("-")
] + self.entries + [
urwid.Padding(self.add_button, left=2, right=2)
]
def get_value(self):
values = {}
for entry in self.entries:
key = entry.key_edit.edit_text.strip()
value = entry.value_edit.edit_text.strip()
if key:
if value.isdigit():
values[key] = int(value)
elif value.replace('.', '', 1).isdigit() and value.count('.') <= 1:
values[key] = float(value)
elif value.lower() == 'true':
values[key] = True
elif value.lower() == 'false':
values[key] = False
else:
values[key] = value
return self.transform(values) if self.transform else values
def set_value(self, value):
self.entries = []
if not value or not isinstance(value, dict):
self.entries.append(self.create_entry_row())
else:
for key, val in value.items():
self.entries.append(self.create_entry_row(key=key, value=str(val)))
self.contents = [(w, self.options()) for w in self.get_pile_widgets()]
def validate(self):
values = self.get_value()
self.error = None
keys = [entry.key_edit.edit_text.strip() for entry in self.entries
if entry.key_edit.edit_text.strip()]
if len(keys) != len(set(keys)):
self.error = "Duplicate keys are not allowed"
self.error_widget.set_text(("error", self.error))
return False
for validation in self.validation_types:
if validation == "required" and not values:
self.error = "Atleast one parameter is required"
break
self.error_widget.set_text(("error", self.error or ""))
return self.error is None

View file

@ -269,11 +269,13 @@ class IndicativeListBox(urwid.WidgetWrap):
# mousewheel up # mousewheel up
elif button == 4.0: elif button == 4.0:
was_handeled = self._pass_key_to_contained_listbox(modified_size, "page up") # was_handeled = self._pass_key_to_contained_listbox(modified_size, "page up")
was_handeled = self._pass_key_to_contained_listbox(modified_size, "up")
# mousewheel down # mousewheel down
elif button == 5.0: elif button == 5.0:
was_handeled = self._pass_key_to_contained_listbox(modified_size, "page down") # was_handeled = self._pass_key_to_contained_listbox(modified_size, "page down")
was_handeled = self._pass_key_to_contained_listbox(modified_size, "down")
focus_position_after_input = self.get_selected_position() focus_position_after_input = self.get_selected_position()

File diff suppressed because it is too large Load diff

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.6.2", "lxmf>=0.3.4", "urwid==2.1.2", "qrcode"], install_requires=["rns>=0.9.6", "lxmf>=0.7.1", "urwid>=2.6.16", "qrcode"],
python_requires=">=3.6", python_requires=">=3.7",
) )