mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-10-01 19:49:08 +00:00
Interface: Add Profiles and grid-view option. Add option to paste in an Interface
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import os
|
||||
import threading
|
||||
|
||||
import RNS
|
||||
import RNS.vendor.umsgpack as msgpack
|
||||
|
||||
|
||||
class InterfaceProfiles:
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.path = os.path.join(app.storagepath, "interface_profiles.msgpack")
|
||||
self._lock = threading.Lock()
|
||||
self.profiles = []
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
self.profiles = []
|
||||
self.default_members = None
|
||||
try:
|
||||
if not os.path.isfile(self.path):
|
||||
return
|
||||
with open(self.path, "rb") as f:
|
||||
data = msgpack.unpackb(f.read())
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
clean = []
|
||||
seen = set()
|
||||
for p in data.get("profiles") or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
pid = str(p.get("id") or "")
|
||||
if not pid or pid in seen:
|
||||
continue
|
||||
members = [str(m) for m in (p.get("members") or []) if isinstance(m, (str, bytes))]
|
||||
clean.append({"id": pid, "name": str(p.get("name") or ""), "members": members})
|
||||
seen.add(pid)
|
||||
self.profiles = clean
|
||||
dm = data.get("default_members")
|
||||
self.default_members = [str(m) for m in dm] if isinstance(dm, list) else None
|
||||
except Exception as e:
|
||||
RNS.log("Could not load interface profiles: "+str(e), RNS.LOG_ERROR)
|
||||
self.profiles = []
|
||||
self.default_members = None
|
||||
|
||||
def save(self):
|
||||
with self._lock:
|
||||
tmp = self.path + ".tmp"
|
||||
try:
|
||||
payload = {
|
||||
"version": InterfaceProfiles.VERSION,
|
||||
"profiles": [{"id": p["id"], "name": p["name"], "members": list(p["members"])} for p in self.profiles],
|
||||
"default_members": self.default_members,
|
||||
}
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(msgpack.packb(payload))
|
||||
f.flush()
|
||||
try: os.fsync(f.fileno())
|
||||
except Exception: pass
|
||||
os.replace(tmp, self.path)
|
||||
except Exception as e:
|
||||
RNS.log("Could not save interface profiles: "+str(e), RNS.LOG_ERROR)
|
||||
try: os.remove(tmp)
|
||||
except Exception: pass
|
||||
|
||||
def _interfaces(self):
|
||||
try:
|
||||
return self.app.rns.config["interfaces"]
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def interface_names(self):
|
||||
try:
|
||||
return list(self._interfaces().keys())
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _is_enabled(self, iface):
|
||||
return str(iface.get("enabled")).lower() not in ('false', 'off', 'no', '0') and \
|
||||
str(iface.get("interface_enabled")).lower() not in ('false', 'off', 'no', '0')
|
||||
|
||||
def enabled_set(self):
|
||||
interfaces = self._interfaces()
|
||||
result = set()
|
||||
for name in interfaces:
|
||||
try:
|
||||
if self._is_enabled(interfaces[name]):
|
||||
result.add(name)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
def get(self, pid):
|
||||
for p in self.profiles:
|
||||
if p["id"] == pid:
|
||||
return p
|
||||
return None
|
||||
|
||||
def members_existing(self, p):
|
||||
existing = set(self.interface_names())
|
||||
return set(m for m in p["members"] if m in existing)
|
||||
|
||||
def active_profile_id(self):
|
||||
enabled = self.enabled_set()
|
||||
for p in self.profiles:
|
||||
if self.members_existing(p) == enabled:
|
||||
return p["id"]
|
||||
return None
|
||||
|
||||
def profiles_for(self, iface_name):
|
||||
return [p for p in self.profiles if iface_name in p["members"]]
|
||||
|
||||
def label_for(self, iface_name):
|
||||
names = [p["name"] or p["id"] for p in self.profiles_for(iface_name)]
|
||||
return ", ".join(names) if names else None
|
||||
|
||||
def member_count(self, p):
|
||||
return len(self.members_existing(p))
|
||||
|
||||
def _new_id(self):
|
||||
while True:
|
||||
pid = os.urandom(4).hex()
|
||||
if not self.get(pid):
|
||||
return pid
|
||||
|
||||
def create(self, name):
|
||||
pid = self._new_id()
|
||||
self.profiles.append({"id": pid, "name": name or "Profile", "members": []})
|
||||
self.save()
|
||||
return pid
|
||||
|
||||
def rename(self, pid, name):
|
||||
p = self.get(pid)
|
||||
if p is not None and name:
|
||||
p["name"] = name
|
||||
self.save()
|
||||
|
||||
def delete(self, pid):
|
||||
self.profiles = [p for p in self.profiles if p["id"] != pid]
|
||||
self.save()
|
||||
|
||||
def set_members(self, pid, members):
|
||||
p = self.get(pid)
|
||||
if p is not None:
|
||||
valid = set(self.interface_names())
|
||||
seen = set()
|
||||
ordered = []
|
||||
for m in members:
|
||||
if m in valid and m not in seen:
|
||||
ordered.append(m)
|
||||
seen.add(m)
|
||||
p["members"] = ordered
|
||||
self.save()
|
||||
|
||||
def set_interface_profiles(self, iface_name, profile_ids):
|
||||
pset = set(profile_ids)
|
||||
for p in self.profiles:
|
||||
if p["id"] in pset:
|
||||
if iface_name not in p["members"]:
|
||||
p["members"].append(iface_name)
|
||||
elif iface_name in p["members"]:
|
||||
p["members"] = [m for m in p["members"] if m != iface_name]
|
||||
self.save()
|
||||
|
||||
def _apply_members(self, members):
|
||||
existing = set(self.interface_names())
|
||||
members = set(m for m in members if m in existing)
|
||||
interfaces = self._interfaces()
|
||||
try:
|
||||
for name in interfaces:
|
||||
value = "true" if name in members else "false"
|
||||
interfaces[name]["interface_enabled"] = value
|
||||
if "enabled" in interfaces[name]:
|
||||
interfaces[name]["enabled"] = value
|
||||
self.app.rns.config.write()
|
||||
return True
|
||||
except Exception as e:
|
||||
RNS.log("Could not apply interface profile: "+str(e), RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
def select_profile(self, pid):
|
||||
p = self.get(pid)
|
||||
if p is None:
|
||||
return False
|
||||
return self._apply_members(self.members_existing(p))
|
||||
|
||||
def select_default(self):
|
||||
if self.default_members is None:
|
||||
return False
|
||||
return self._apply_members(set(self.default_members))
|
||||
|
||||
def update_default_if_custom(self):
|
||||
# Remember the live enabled-set as "Default" whenever it matches no named
|
||||
# profile -- i.e. it's the user's manual setup. Only relevant once profiles exist.
|
||||
if not self.profiles:
|
||||
return
|
||||
enabled = self.enabled_set()
|
||||
matches_named = any(self.members_existing(p) == enabled for p in self.profiles)
|
||||
if matches_named and self.default_members is not None:
|
||||
return
|
||||
new_default = sorted(enabled)
|
||||
if self.default_members != new_default:
|
||||
self.default_members = new_default
|
||||
self.save()
|
||||
|
||||
def remove_interface(self, iface_name):
|
||||
changed = False
|
||||
for p in self.profiles:
|
||||
if iface_name in p["members"]:
|
||||
p["members"] = [m for m in p["members"] if m != iface_name]
|
||||
changed = True
|
||||
if changed:
|
||||
self.save()
|
||||
|
||||
def rename_interface(self, old_name, new_name):
|
||||
changed = False
|
||||
for p in self.profiles:
|
||||
if old_name in p["members"]:
|
||||
p["members"] = [new_name if m == old_name else m for m in p["members"]]
|
||||
changed = True
|
||||
if changed:
|
||||
self.save()
|
||||
|
||||
def prune(self):
|
||||
valid = set(self.interface_names())
|
||||
changed = False
|
||||
for p in self.profiles:
|
||||
pruned = [m for m in p["members"] if m in valid]
|
||||
if len(pruned) != len(p["members"]):
|
||||
p["members"] = pruned
|
||||
changed = True
|
||||
if changed:
|
||||
self.save()
|
||||
@@ -330,6 +330,9 @@ class NomadNetworkApp:
|
||||
self.rrc = RRCManager(self)
|
||||
self.rrc.load()
|
||||
|
||||
from nomadnet.InterfaceProfiles import InterfaceProfiles
|
||||
self.interface_profiles = InterfaceProfiles(self)
|
||||
|
||||
static_peers = []
|
||||
for static_peer in self.static_peers:
|
||||
try:
|
||||
|
||||
@@ -55,6 +55,7 @@ THEMES = {
|
||||
("progress_empty", "light gray", "default", "default", "#ddd", "default"),
|
||||
("interface_title", "", "", "default", "", ""),
|
||||
("interface_title_selected", "bold", "", "bold", "", ""),
|
||||
("interface_tile_focus", "light cyan,bold", "default", "bold", "#3dd,bold", "default"),
|
||||
("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"),
|
||||
@@ -108,6 +109,7 @@ THEMES = {
|
||||
("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"),
|
||||
("interface_tile_focus", "dark blue,bold", "default", "bold", "#06a,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"),
|
||||
@@ -164,6 +166,8 @@ GLYPHS = {
|
||||
("qrcode", "QR", "\u25a4", "\uf029"),
|
||||
("selected", "[*] ", "\u25CF", "\u25CF"),
|
||||
("unselected", "[ ] ", "\u25CB", "\u25CB"),
|
||||
("focus_arrow", ">", "\u25B8", "\u25B8"),
|
||||
("sep_dot", "|", "\u00B7", "\u00B7"),
|
||||
("file", "[F]", "\u25a4", "\uf15b"),
|
||||
("image", "[I]", "\u25a3", "\uf1c5"),
|
||||
("audio", "[~]", "\u266b", "\uf1c7"),
|
||||
|
||||
+593
-175
@@ -7,6 +7,7 @@ from math import log10, pow
|
||||
from nomadnet.vendor.additional_urwid_widgets.FormWidgets import *
|
||||
from nomadnet.vendor.AsciiChart import AsciiChart
|
||||
from .ReadlineEdit import ReadlineEdit
|
||||
from .Helpers import ClickableIcon
|
||||
|
||||
### GYLPHS ###
|
||||
INTERFACE_GLYPHS = {
|
||||
@@ -1123,7 +1124,7 @@ INTERFACE_FIELDS = {
|
||||
|
||||
### INTERFACE WIDGETS ####
|
||||
class SelectableInterfaceItem(urwid.WidgetWrap):
|
||||
def __init__(self, parent, name, is_connected, is_enabled, iface_type, tx, rx, icon="?", iface_options=None):
|
||||
def __init__(self, parent, name, is_connected, is_enabled, iface_type, tx, rx, icon="?", iface_options=None, profiles_label=None):
|
||||
self.parent = parent
|
||||
self._selectable = True
|
||||
self.icon = icon
|
||||
@@ -1180,6 +1181,12 @@ class SelectableInterfaceItem(urwid.WidgetWrap):
|
||||
]),
|
||||
]
|
||||
|
||||
if profiles_label is not None:
|
||||
rows.insert(2, urwid.Columns([
|
||||
(10, urwid.Text(("key", "Profiles:"))),
|
||||
urwid.Text(("value", profiles_label)),
|
||||
]))
|
||||
|
||||
pile_contents = [title_content] + rows
|
||||
|
||||
pile = urwid.Pile(pile_contents)
|
||||
@@ -1240,10 +1247,11 @@ class SelectableInterfaceItem(urwid.WidgetWrap):
|
||||
self.rx_widget.set_text(("value", format_bytes(rx)))
|
||||
|
||||
class InterfaceOptionItem(urwid.WidgetWrap):
|
||||
def __init__(self, parent_display, label, value):
|
||||
def __init__(self, parent_display, label, value, on_select=None):
|
||||
self.parent_display = parent_display
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.on_select = on_select
|
||||
self._selectable = True
|
||||
|
||||
text_widget = urwid.Text(label, align="left")
|
||||
@@ -1252,13 +1260,25 @@ class InterfaceOptionItem(urwid.WidgetWrap):
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "enter":
|
||||
def _activate(self):
|
||||
if self.on_select is not None:
|
||||
self.on_select()
|
||||
else:
|
||||
self.parent_display.dismiss_dialog()
|
||||
self.parent_display.switch_to_add_interface(self.value)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "enter":
|
||||
self._activate()
|
||||
return None
|
||||
return super().keypress(size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, x, y, focus):
|
||||
if button == 1 and urwid.util.is_mouse_press(event):
|
||||
self._activate()
|
||||
return True
|
||||
return False
|
||||
|
||||
class InterfaceBandwidthChart:
|
||||
|
||||
def __init__(self, history_length=60, glyphset="unicode"):
|
||||
@@ -1395,6 +1415,12 @@ class InterfaceFiller(urwid.WidgetWrap):
|
||||
super().__init__(self.filler)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "tab":
|
||||
self.app.ui.main_display.sub_displays.interface_display.show_profiles()
|
||||
return None
|
||||
if key == "ctrl t":
|
||||
self.app.ui.main_display.sub_displays.interface_display.toggle_view_mode()
|
||||
return None
|
||||
if key == "ctrl a":
|
||||
# add interface
|
||||
self.app.ui.main_display.sub_displays.interface_display.add_interface()
|
||||
@@ -1717,6 +1743,7 @@ class AddInterfaceView(urwid.WidgetWrap):
|
||||
self.calculator_button,
|
||||
self.calculator_widget,
|
||||
])
|
||||
pile_items.extend(self._build_profiles_section())
|
||||
pile_items.extend([
|
||||
urwid.Divider("─"),
|
||||
button_row,
|
||||
@@ -1724,6 +1751,37 @@ class AddInterfaceView(urwid.WidgetWrap):
|
||||
|
||||
return pile_items
|
||||
|
||||
def _build_profiles_section(self):
|
||||
self.profile_checkboxes = []
|
||||
profiles = getattr(self.parent.app, "interface_profiles", None)
|
||||
if profiles is None or not profiles.profiles:
|
||||
return []
|
||||
iface_name = getattr(self, "iface_name", None)
|
||||
current = set()
|
||||
if iface_name:
|
||||
current = set(p["id"] for p in profiles.profiles_for(iface_name))
|
||||
items = [
|
||||
urwid.Divider("─"),
|
||||
urwid.Text(("key", "Profiles"), align="left"),
|
||||
urwid.Text(("inactive_text", " Assign this interface to one or more profiles."), align="left"),
|
||||
]
|
||||
for p in profiles.profiles:
|
||||
cb = urwid.CheckBox(p["name"] or "(unnamed)", state=(p["id"] in current))
|
||||
cb.profile_id = p["id"]
|
||||
self.profile_checkboxes.append(cb)
|
||||
items.append(urwid.Padding(cb, left=2))
|
||||
return items
|
||||
|
||||
def _apply_profile_membership(self, iface_name):
|
||||
profiles = getattr(self.parent.app, "interface_profiles", None)
|
||||
if profiles is None:
|
||||
return
|
||||
checked = [cb.profile_id for cb in getattr(self, "profile_checkboxes", []) if cb.get_state()]
|
||||
try:
|
||||
profiles.set_interface_profiles(iface_name, checked)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def toggle_more_options(self, button):
|
||||
if self.more_options_visible:
|
||||
self.more_options_widget.contents = []
|
||||
@@ -1939,6 +1997,7 @@ class AddInterfaceView(urwid.WidgetWrap):
|
||||
interfaces = self.parent.app.rns.config['interfaces']
|
||||
interfaces[name] = interface_config
|
||||
self.parent.app.rns.config.write()
|
||||
self._apply_profile_membership(name)
|
||||
|
||||
display_type = custom_type if self.iface_type == "CustomInterface" else self.iface_type
|
||||
|
||||
@@ -2181,6 +2240,9 @@ class EditInterfaceView(AddInterfaceView):
|
||||
del interfaces[self.iface_name]
|
||||
interfaces[new_name] = updated_config
|
||||
|
||||
try: self.parent.app.interface_profiles.rename_interface(self.iface_name, new_name)
|
||||
except Exception: pass
|
||||
|
||||
for i, item in enumerate(self.parent.interface_items):
|
||||
if item.name == self.iface_name:
|
||||
self.parent.interface_items[i].name = new_name
|
||||
@@ -2189,6 +2251,7 @@ class EditInterfaceView(AddInterfaceView):
|
||||
interfaces[self.iface_name] = updated_config
|
||||
|
||||
self.parent.app.rns.config.write()
|
||||
self._apply_profile_membership(new_name)
|
||||
|
||||
display_type = interface_type
|
||||
|
||||
@@ -2514,77 +2577,33 @@ class ShowInterface(urwid.WidgetWrap):
|
||||
body_pile.contents[chart_index] = (new_widget, body_pile.options())
|
||||
|
||||
def on_toggle_enabled(self, button):
|
||||
action = "disable" if self.is_enabled else "enable"
|
||||
self.is_enabled = not self.is_enabled
|
||||
|
||||
def on_confirm_yes(confirm_button):
|
||||
self.parent.app.ui.main_display.frame.body = self.parent.app.ui.main_display.sub_displays.active().widget
|
||||
self.toggle_button.set_label("Disable" if self.is_enabled else "Enable")
|
||||
|
||||
self.is_enabled = not self.is_enabled
|
||||
self.interface_config["interface_enabled"] = self.is_enabled
|
||||
if "enabled" in self.interface_config:
|
||||
self.interface_config["enabled"] = self.is_enabled
|
||||
|
||||
self.toggle_button.set_label("Disable" if self.is_enabled else "Enable")
|
||||
try:
|
||||
interfaces = self.parent.app.rns.config['interfaces']
|
||||
interfaces[self.iface_name] = self.interface_config
|
||||
self.parent.app.rns.config.write()
|
||||
|
||||
if "interface_enabled" in self.interface_config:
|
||||
self.interface_config["interface_enabled"] = self.is_enabled
|
||||
else:
|
||||
self.interface_config["enabled"] = self.is_enabled
|
||||
self.update_status_display()
|
||||
|
||||
try:
|
||||
interfaces = self.parent.app.rns.config['interfaces']
|
||||
for item in self.parent.interface_items:
|
||||
if item.name == self.iface_name:
|
||||
item.is_enabled = self.is_enabled
|
||||
item.update_status_display()
|
||||
|
||||
interfaces[self.iface_name] = self.interface_config
|
||||
if hasattr(self.parent.app.ui, 'loop') and self.parent.app.ui.loop is not None:
|
||||
self.parent.app.ui.loop.draw_screen()
|
||||
|
||||
self.parent.app.rns.config.write()
|
||||
self.show_restart_required_message()
|
||||
|
||||
self.update_status_display()
|
||||
|
||||
for item in self.parent.interface_items:
|
||||
if item.name == self.iface_name:
|
||||
item.is_enabled = self.is_enabled
|
||||
item.update_status_display()
|
||||
|
||||
if hasattr(self.parent.app.ui, 'loop') and self.parent.app.ui.loop is not None:
|
||||
self.parent.app.ui.loop.draw_screen()
|
||||
|
||||
self.show_restart_required_message()
|
||||
|
||||
except Exception as e:
|
||||
self.show_error_message(f"Error updating interface: {str(e)}")
|
||||
|
||||
def on_confirm_no(confirm_button):
|
||||
self.parent.app.ui.main_display.frame.body = self.parent.app.ui.main_display.sub_displays.active().widget
|
||||
|
||||
confirm_text = urwid.Text((
|
||||
"interface_title",
|
||||
f"Are you sure you want to {action} the {self.iface_name} interface?"
|
||||
), align="center")
|
||||
|
||||
yes_button = urwid.Button("Yes", on_press=on_confirm_yes)
|
||||
no_button = urwid.Button("No", on_press=on_confirm_no)
|
||||
|
||||
buttons_row = urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, yes_button),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, no_button),
|
||||
])
|
||||
|
||||
pile = urwid.Pile([
|
||||
confirm_text,
|
||||
urwid.Divider(),
|
||||
buttons_row
|
||||
])
|
||||
|
||||
dialog = DialogLineBox(pile, title="Confirm")
|
||||
|
||||
overlay = urwid.Overlay(
|
||||
dialog,
|
||||
self.parent.app.ui.main_display.frame.body,
|
||||
align='center',
|
||||
width=50,
|
||||
valign='middle',
|
||||
height=7
|
||||
)
|
||||
|
||||
self.parent.app.ui.main_display.frame.body = overlay
|
||||
except Exception as e:
|
||||
self.show_error_message(f"Error updating interface: {str(e)}")
|
||||
|
||||
def show_restart_required_message(self):
|
||||
|
||||
@@ -2822,6 +2841,247 @@ class ShowInterface(urwid.WidgetWrap):
|
||||
self.started = False
|
||||
self.parent.switch_to_edit_interface(self.iface_name)
|
||||
|
||||
class InterfaceTile(urwid.WidgetWrap):
|
||||
def __init__(self, parent, name, is_connected, is_enabled, iface_type, icon):
|
||||
self.parent = parent
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.g = parent.g
|
||||
self.dot_style = "connected_status" if is_enabled else "disconnected_status"
|
||||
self.dot = self.g['selected'] if is_enabled else self.g['unselected']
|
||||
self.title_widget = urwid.Text("", wrap="clip")
|
||||
typ = urwid.Text(("value", iface_type), wrap="clip")
|
||||
status = urwid.Text([
|
||||
(self.dot_style, "Enabled" if is_enabled else "Disabled"),
|
||||
("value", f" {self.g['sep_dot']} "),
|
||||
("connected_status" if is_connected else "disconnected_status", "Conn." if is_connected else "Disc."),
|
||||
], wrap="clip")
|
||||
inner = urwid.Pile([self.title_widget, typ, status])
|
||||
self.box = urwid.LineBox(
|
||||
urwid.Padding(inner, left=1, right=1),
|
||||
tlcorner="╭", trcorner="╮", blcorner="╰", brcorner="╯",
|
||||
)
|
||||
self.attr = urwid.AttrMap(self.box, None, focus_map={None: "interface_tile_focus"})
|
||||
super().__init__(self.attr)
|
||||
self._render_title(False)
|
||||
|
||||
def _render_title(self, focus):
|
||||
arrow = self.g['focus_arrow'] if focus else " "
|
||||
style = "interface_tile_focus" if focus else "interface_title"
|
||||
self.title_widget.set_text([(self.dot_style, self.dot), (style, f" {arrow} {self.icon} {self.name}")])
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def render(self, size, focus=False):
|
||||
self._render_title(focus)
|
||||
return super().render(size, focus)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == "enter":
|
||||
self.parent.switch_to_show_interface(self.name)
|
||||
return None
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, x, y, focus):
|
||||
if button == 1 and urwid.util.is_mouse_press(event):
|
||||
self.parent.switch_to_show_interface(self.name)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
### PROFILE VIEWS ###
|
||||
class ProfileListRow(urwid.WidgetWrap):
|
||||
def __init__(self, view, pid, label, selected):
|
||||
self.view = view
|
||||
self.pid = pid
|
||||
self.label = label
|
||||
self.selected = selected
|
||||
self.g = view.parent.g
|
||||
self.text_widget = urwid.Text("")
|
||||
super().__init__(urwid.AttrMap(self.text_widget, None))
|
||||
self._render_row(False)
|
||||
|
||||
def _render_row(self, focus):
|
||||
mark = self.g['selected'] if self.selected else self.g['unselected']
|
||||
arrow = self.g['focus_arrow'] if focus else " "
|
||||
if focus:
|
||||
style = "interface_tile_focus"
|
||||
elif self.selected:
|
||||
style = "connected_status"
|
||||
else:
|
||||
style = "interface_title"
|
||||
self.text_widget.set_text((style, f" {arrow} {mark} {self.label}"))
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def render(self, size, focus=False):
|
||||
self._render_row(focus)
|
||||
return super().render(size, focus)
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key in (" ", "enter"):
|
||||
if self.pid is not None:
|
||||
self.view.select(self.pid)
|
||||
return None
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, x, y, focus):
|
||||
if button == 1 and urwid.util.is_mouse_press(event):
|
||||
if self.pid is not None:
|
||||
self.view.select(self.pid)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ProfilesView(urwid.WidgetWrap):
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.app = parent.app
|
||||
self.profiles = parent.app.interface_profiles
|
||||
self.profiles.prune()
|
||||
self.status = urwid.Text("", align="center")
|
||||
self.walker = urwid.SimpleFocusListWalker([])
|
||||
self.listbox = urwid.ListBox(self.walker)
|
||||
body_rows = max(3, parent.list_rows - 3)
|
||||
self.pile = urwid.Pile([
|
||||
('pack', parent._make_tab_bar("profiles")),
|
||||
('pack', self.status),
|
||||
('pack', urwid.Divider()),
|
||||
urwid.BoxAdapter(self.listbox, body_rows),
|
||||
])
|
||||
super().__init__(urwid.Filler(self.pile, urwid.TOP))
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, focus_pid="__keep__"):
|
||||
prev = None
|
||||
try:
|
||||
w, _ = self.listbox.body.get_focus()
|
||||
prev = getattr(w, "pid", None)
|
||||
except Exception:
|
||||
pass
|
||||
self.profiles.update_default_if_custom()
|
||||
active = self.profiles.active_profile_id()
|
||||
items = [ProfileListRow(self, "__default__", "Default", active is None)]
|
||||
for p in self.profiles.profiles:
|
||||
count = self.profiles.member_count(p)
|
||||
label = f"{p['name'] or '(unnamed)'} ({count} interface{'' if count == 1 else 's'})"
|
||||
items.append(ProfileListRow(self, p["id"], label, p["id"] == active))
|
||||
if len(self.profiles.profiles) == 0:
|
||||
items.append(urwid.Text("\n No profiles yet. Press Ctrl + A to create one.", align="left"))
|
||||
self.walker[:] = items
|
||||
target = focus_pid if focus_pid != "__keep__" else prev
|
||||
for i, w in enumerate(self.walker):
|
||||
if getattr(w, "pid", "__x__") == target:
|
||||
try: self.listbox.focus_position = i
|
||||
except Exception: pass
|
||||
break
|
||||
|
||||
def _focused(self):
|
||||
try:
|
||||
w, _ = self.listbox.body.get_focus()
|
||||
return w if isinstance(w, ProfileListRow) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def select(self, pid):
|
||||
if pid == "__default__":
|
||||
self.profiles.select_default()
|
||||
else:
|
||||
self.profiles.select_profile(pid)
|
||||
self.refresh(focus_pid=pid)
|
||||
self.status.set_text(("connected_status", "Restart NomadNet for changes to take effect"))
|
||||
|
||||
def _overlay(self, dialog, height=8, width=50):
|
||||
frame = self.app.ui.main_display.frame
|
||||
frame.body = urwid.Overlay(dialog, self, align="center", width=width,
|
||||
valign="middle", height=height, min_width=20)
|
||||
|
||||
def dismiss_dialog(self):
|
||||
self.app.ui.main_display.frame.body = self
|
||||
|
||||
def new_profile(self):
|
||||
edit = ReadlineEdit(caption="Name: ")
|
||||
|
||||
def ok(_b):
|
||||
val = edit.get_edit_text().strip()
|
||||
self.dismiss_dialog()
|
||||
if val:
|
||||
pid = self.profiles.create(val)
|
||||
self.refresh(focus_pid=pid)
|
||||
|
||||
self._prompt("New Profile", edit, ok)
|
||||
|
||||
def rename_profile(self):
|
||||
w = self._focused()
|
||||
if w is None or w.pid is None:
|
||||
return
|
||||
p = self.profiles.get(w.pid)
|
||||
if p is None:
|
||||
return
|
||||
edit = ReadlineEdit(caption="Name: ", edit_text=p["name"])
|
||||
|
||||
def ok(_b):
|
||||
val = edit.get_edit_text().strip()
|
||||
self.dismiss_dialog()
|
||||
if val:
|
||||
self.profiles.rename(p["id"], val)
|
||||
self.refresh(focus_pid=p["id"])
|
||||
|
||||
self._prompt("Rename Profile", edit, ok)
|
||||
|
||||
def delete_profile(self):
|
||||
w = self._focused()
|
||||
if w is None or w.pid is None:
|
||||
return
|
||||
p = self.profiles.get(w.pid)
|
||||
if p is None:
|
||||
return
|
||||
|
||||
def yes(_b):
|
||||
self.dismiss_dialog()
|
||||
self.profiles.delete(p["id"])
|
||||
self.refresh()
|
||||
|
||||
def no(_b):
|
||||
self.dismiss_dialog()
|
||||
|
||||
pile = urwid.Pile([
|
||||
urwid.Text(f"Delete profile '{p['name'] or p['id']}'?\nInterfaces keep their current enabled state.", align="center"),
|
||||
urwid.Divider(),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Yes", on_press=yes)),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("No", on_press=no)),
|
||||
]),
|
||||
])
|
||||
self._overlay(DialogLineBox(urwid.Filler(pile, urwid.TOP), parent=self, title="Confirm"), height=10)
|
||||
|
||||
def _prompt(self, title, edit, on_ok):
|
||||
pile = urwid.Pile([
|
||||
edit,
|
||||
urwid.Divider(),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("OK", on_press=on_ok)),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Cancel", on_press=lambda _b: self.dismiss_dialog())),
|
||||
]),
|
||||
])
|
||||
self._overlay(DialogLineBox(urwid.Filler(pile, urwid.TOP), parent=self, title=title))
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key in ("tab", "esc"):
|
||||
self.parent.show_interfaces(); return None
|
||||
if key == "ctrl a":
|
||||
self.new_profile(); return None
|
||||
if key == "ctrl e":
|
||||
self.rename_profile(); return None
|
||||
if key == "ctrl x":
|
||||
self.delete_profile(); return None
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
### MAIN DISPLAY ###
|
||||
class InterfaceDisplay:
|
||||
def __init__(self, app):
|
||||
@@ -2837,87 +3097,93 @@ class InterfaceDisplay:
|
||||
self.iface_row_offset = 4
|
||||
self.list_rows = self.terminal_rows - self.iface_row_offset
|
||||
|
||||
interfaces = app.rns.config['interfaces']
|
||||
processed_interfaces = {}
|
||||
self.active_tab = "interfaces"
|
||||
self.iface_view_mode = "rows"
|
||||
self.profiles_view = None
|
||||
|
||||
for interface_name, interface in interfaces.items():
|
||||
interface_data = interface.copy()
|
||||
self._build_interface_items()
|
||||
|
||||
# handle sub-interfaces for RNodeMultiInterface
|
||||
if interface_data.get("type") == "RNodeMultiInterface":
|
||||
sub_interfaces = []
|
||||
for sub_name, sub_config in interface_data.items():
|
||||
if sub_name not in {"type", "port", "interface_enabled", "selected_interface_mode",
|
||||
"configured_bitrate"}:
|
||||
if isinstance(sub_config, dict):
|
||||
sub_config["name"] = sub_name
|
||||
sub_interfaces.append(sub_config)
|
||||
|
||||
# add sub-interfaces to the main interface data
|
||||
interface_data["sub_interfaces"] = sub_interfaces
|
||||
|
||||
for sub in sub_interfaces:
|
||||
del interface_data[sub["name"]]
|
||||
|
||||
processed_interfaces[interface_name] = interface_data
|
||||
|
||||
app.interface_stats = app.rns.get_interface_stats()
|
||||
interface_stats = app.interface_stats
|
||||
stats_lookup = {interface['short_name']: interface for interface in interface_stats['interfaces']}
|
||||
# print(stats_lookup)
|
||||
for interface_name, interface_data in processed_interfaces.items():
|
||||
# configobj false values
|
||||
is_enabled = str(interface_data.get("enabled")).lower() not in ('false', 'off', 'no', '0') and str(interface_data.get("interface_enabled")).lower() not in ('false', 'off', 'no', '0')
|
||||
|
||||
iface_type = interface_data.get("type", "Unknown")
|
||||
icon = _get_interface_icon(self.glyphset, iface_type)
|
||||
|
||||
stats_for_interface = stats_lookup.get(interface_name)
|
||||
|
||||
if stats_for_interface:
|
||||
tx = stats_for_interface.get("txb", 0)
|
||||
rx = stats_for_interface.get("rxb", 0)
|
||||
is_connected = stats_for_interface["status"]
|
||||
else:
|
||||
tx = 0
|
||||
rx = 0
|
||||
is_connected = False
|
||||
|
||||
item = SelectableInterfaceItem(
|
||||
parent=self,
|
||||
name=interface_data.get("name", interface_name),
|
||||
is_connected=is_connected,
|
||||
is_enabled=is_enabled,
|
||||
iface_type=iface_type,
|
||||
tx=tx,
|
||||
rx=rx,
|
||||
icon=icon
|
||||
)
|
||||
|
||||
self.interface_items.append(item)
|
||||
|
||||
interface_header = urwid.Text(("interface_title", "Interfaces"), align="center")
|
||||
if len(self.interface_items) == 0:
|
||||
interface_header = urwid.Text(
|
||||
("interface_title", "No interfaces found. Press Ctrl + A to add a new interface "), align="center")
|
||||
|
||||
|
||||
list_contents = [
|
||||
interface_header,
|
||||
urwid.Divider(),
|
||||
] + self.interface_items
|
||||
|
||||
self.list_walker = urwid.SimpleFocusListWalker(list_contents)
|
||||
self.list_walker = urwid.SimpleFocusListWalker(self._interface_list_contents())
|
||||
self.list_box = urwid.ListBox(self.list_walker)
|
||||
|
||||
self.box_adapter = urwid.BoxAdapter(self.list_box, self.list_rows)
|
||||
self.body_rows = max(3, self.list_rows - 3)
|
||||
self.box_adapter = urwid.BoxAdapter(self.list_box, self.body_rows)
|
||||
|
||||
|
||||
pile = urwid.Pile([self.box_adapter])
|
||||
self.interfaces_display = InterfaceFiller(pile, self.app)
|
||||
self.tabs_pile = urwid.Pile([
|
||||
('pack', self._make_tab_bar("interfaces")),
|
||||
('pack', urwid.Divider()),
|
||||
self.box_adapter,
|
||||
])
|
||||
self.interfaces_display = InterfaceFiller(self.tabs_pile, self.app)
|
||||
self.shortcuts_display = InterfaceDisplayShortcuts(self.app)
|
||||
self.widget = self.interfaces_display
|
||||
|
||||
def _interface_list_contents(self):
|
||||
if not self.interface_items:
|
||||
return [urwid.Text(("interface_title", "No interfaces found. Press Ctrl + A to add a new interface "), align="center")]
|
||||
return list(self.interface_items)
|
||||
|
||||
def _make_tab_bar(self, active):
|
||||
iface = f" Interfaces ({len(self.interface_items)}) "
|
||||
prof = " Profiles "
|
||||
if active == "interfaces":
|
||||
iface_w = ClickableIcon(("interface_title_selected", "["+iface+"]"), on_click=self.show_interfaces)
|
||||
prof_w = ClickableIcon(("interface_title", " "+prof+" "), on_click=self.show_profiles)
|
||||
else:
|
||||
iface_w = ClickableIcon(("interface_title", " "+iface+" "), on_click=self.show_interfaces)
|
||||
prof_w = ClickableIcon(("interface_title_selected", "["+prof+"]"), on_click=self.show_profiles)
|
||||
return urwid.Columns([
|
||||
('weight', 1, urwid.Text("")),
|
||||
('pack', iface_w),
|
||||
('pack', urwid.Text(" ")),
|
||||
('pack', prof_w),
|
||||
('weight', 1, urwid.Text("")),
|
||||
], dividechars=0)
|
||||
|
||||
def _build_grid_listbox(self):
|
||||
tiles = self.interface_tiles if self.interface_tiles else [urwid.Text("No interfaces found. Press Ctrl + A to add a new interface ", align="center")]
|
||||
self.iface_grid = urwid.GridFlow(tiles, cell_width=34, h_sep=2, v_sep=1, align="center")
|
||||
return urwid.ListBox(urwid.SimpleFocusListWalker([self.iface_grid]))
|
||||
|
||||
def _set_iface_body(self):
|
||||
if self.iface_view_mode == "grid":
|
||||
self.box_adapter.original_widget = self._build_grid_listbox()
|
||||
else:
|
||||
self.list_walker = urwid.SimpleFocusListWalker(self._interface_list_contents())
|
||||
self.list_box = urwid.ListBox(self.list_walker)
|
||||
self.box_adapter.original_widget = self.list_box
|
||||
|
||||
def _focused_interface_name(self):
|
||||
try:
|
||||
if self.iface_view_mode == "grid":
|
||||
return getattr(self.iface_grid.focus, "name", None)
|
||||
fw, _ = self.box_adapter._original_widget.body.get_focus()
|
||||
return getattr(fw, "name", None) if isinstance(fw, SelectableInterfaceItem) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_view_mode(self):
|
||||
self.iface_view_mode = "grid" if self.iface_view_mode == "rows" else "rows"
|
||||
self._set_iface_body()
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def show_profiles(self):
|
||||
if getattr(self.app, "interface_profiles", None) is None:
|
||||
return
|
||||
self.active_tab = "profiles"
|
||||
self.profiles_view = ProfilesView(self)
|
||||
self.shortcuts_display.set_profiles_tab_shortcuts()
|
||||
self.widget = self.profiles_view
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def show_interfaces(self):
|
||||
self.active_tab = "interfaces"
|
||||
self._build_interface_items()
|
||||
self._rebuild_list()
|
||||
self.shortcuts_display.reset_shortcuts()
|
||||
self.widget = self.interfaces_display
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def start(self):
|
||||
# started from Main.py
|
||||
self.started = True
|
||||
@@ -2930,14 +3196,9 @@ class InterfaceDisplay:
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def edit_selected_interface(self):
|
||||
focus_widget, focus_position = self.box_adapter._original_widget.body.get_focus()
|
||||
|
||||
if not isinstance(focus_widget, SelectableInterfaceItem):
|
||||
interface_name = self._focused_interface_name()
|
||||
if interface_name is None:
|
||||
return
|
||||
|
||||
selected_item = focus_widget
|
||||
interface_name = selected_item.name
|
||||
|
||||
self.switch_to_edit_interface(interface_name)
|
||||
|
||||
def check_terminal_size(self, loop, user_data):
|
||||
@@ -2947,8 +3208,9 @@ class InterfaceDisplay:
|
||||
if new_rows != self.terminal_rows or new_cols != self.terminal_cols:
|
||||
self.terminal_cols, self.terminal_rows = new_cols, new_rows
|
||||
self.list_rows = self.terminal_rows - self.iface_row_offset
|
||||
self.body_rows = max(3, self.list_rows - 3)
|
||||
|
||||
self.box_adapter.height = self.list_rows
|
||||
self.box_adapter.height = self.body_rows
|
||||
|
||||
loop.draw_screen()
|
||||
|
||||
@@ -3009,10 +3271,7 @@ class InterfaceDisplay:
|
||||
show_interface.start()
|
||||
|
||||
def switch_to_list(self):
|
||||
self.shortcuts_display.reset_shortcuts()
|
||||
self.widget = self.interfaces_display
|
||||
self._rebuild_list()
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
self.show_interfaces()
|
||||
|
||||
def add_interface(self):
|
||||
dialog_widgets = []
|
||||
@@ -3024,6 +3283,10 @@ class InterfaceDisplay:
|
||||
item = InterfaceOptionItem(self, label, value)
|
||||
dialog_widgets.append(item)
|
||||
|
||||
add_heading("Paste Configuration")
|
||||
dialog_widgets.append(InterfaceOptionItem(self, "Paste an interface config…", None, on_select=self.show_paste_config_dialog))
|
||||
dialog_widgets.append(urwid.Divider())
|
||||
|
||||
# Get the icons based on plain, unicode, nerdfont glyphset
|
||||
network_icon = _get_interface_icon(self.glyphset, "AutoInterface")
|
||||
rnode_icon = _get_interface_icon(self.glyphset, "RNodeInterface")
|
||||
@@ -3072,28 +3335,117 @@ class InterfaceDisplay:
|
||||
self.widget = overlay
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def show_paste_config_dialog(self):
|
||||
self.paste_edit = ReadlineEdit(caption="", multiline=True)
|
||||
self.paste_status = urwid.Text("")
|
||||
|
||||
instructions = urwid.Text(
|
||||
"Paste an interface block, including its [[Name]] header, then Create. "
|
||||
"Multiple interfaces are supported.", align="left")
|
||||
|
||||
editor = urwid.AttrMap(
|
||||
urwid.LineBox(urwid.BoxAdapter(urwid.ListBox(urwid.SimpleListWalker([self.paste_edit])), 12)),
|
||||
"list_off_focus", "list_focus")
|
||||
|
||||
pile = urwid.Pile([
|
||||
instructions,
|
||||
urwid.Divider(),
|
||||
editor,
|
||||
self.paste_status,
|
||||
urwid.Divider(),
|
||||
urwid.Columns([
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Create", on_press=lambda _b: self._create_interfaces_from_text(self.paste_edit.get_edit_text()))),
|
||||
(urwid.WEIGHT, 0.1, urwid.Text("")),
|
||||
(urwid.WEIGHT, 0.45, urwid.Button("Cancel", on_press=lambda _b: self.dismiss_dialog())),
|
||||
]),
|
||||
])
|
||||
|
||||
dialog = DialogLineBox(urwid.Filler(pile, valign="top"), parent=self, title="Paste Interface Config")
|
||||
overlay = urwid.Overlay(
|
||||
dialog, self.interfaces_display,
|
||||
align="center", width=("relative", 75),
|
||||
valign="middle", height=("relative", 75),
|
||||
min_width=44, min_height=14,
|
||||
)
|
||||
self.widget = overlay
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def _section_to_dict(self, sec):
|
||||
out = {}
|
||||
for k, v in sec.items():
|
||||
out[k] = self._section_to_dict(v) if hasattr(v, "items") else v
|
||||
return out
|
||||
|
||||
def _parse_pasted_interfaces(self, text):
|
||||
import RNS.vendor.configobj as configobj
|
||||
raw = (text or "").strip("\n")
|
||||
if not raw.strip():
|
||||
return {}
|
||||
for prefix in ("", "[interfaces]\n"):
|
||||
try:
|
||||
parsed = configobj.ConfigObj(infile=(prefix + raw).split("\n"))
|
||||
except Exception:
|
||||
continue
|
||||
found = {}
|
||||
sections = []
|
||||
ifsec = parsed.get("interfaces")
|
||||
if hasattr(ifsec, "items"):
|
||||
sections.extend(ifsec.items())
|
||||
for name, sec in parsed.items():
|
||||
if name != "interfaces" and hasattr(sec, "items"):
|
||||
sections.append((name, sec))
|
||||
for name, sec in sections:
|
||||
if not hasattr(sec, "items"):
|
||||
continue
|
||||
d = self._section_to_dict(sec)
|
||||
if "type" in d and name not in found:
|
||||
found[name] = d
|
||||
if found:
|
||||
return found
|
||||
return {}
|
||||
|
||||
def _create_interfaces_from_text(self, text):
|
||||
parsed = self._parse_pasted_interfaces(text)
|
||||
if not parsed:
|
||||
self.paste_status.set_text(("error", " Could not find an interface with a 'type'. Include the [[Name]] header."))
|
||||
return
|
||||
|
||||
interfaces = self.app.rns.config['interfaces']
|
||||
existing = [n for n in parsed if n in interfaces]
|
||||
if existing:
|
||||
self.paste_status.set_text(("error", " Already exists: " + ", ".join(existing)))
|
||||
return
|
||||
|
||||
try:
|
||||
for name, conf in parsed.items():
|
||||
interfaces[name] = conf
|
||||
self.app.rns.config.write()
|
||||
except Exception as e:
|
||||
self.paste_status.set_text(("error", " Failed: " + str(e)))
|
||||
return
|
||||
|
||||
self.dismiss_dialog()
|
||||
self.show_interfaces()
|
||||
|
||||
def switch_to_add_interface(self, iface_type):
|
||||
self.add_interface_view = AddInterfaceView(self, iface_type)
|
||||
self.widget = self.add_interface_view
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def remove_selected_interface(self):
|
||||
focus_widget, focus_position = self.box_adapter._original_widget.body.get_focus()
|
||||
if not isinstance(focus_widget, SelectableInterfaceItem):
|
||||
interface_name = self._focused_interface_name()
|
||||
if interface_name is None:
|
||||
return
|
||||
|
||||
selected_item = focus_widget
|
||||
interface_name = selected_item.name
|
||||
|
||||
def on_confirm_yes(button):
|
||||
try:
|
||||
if interface_name in self.app.rns.config['interfaces']:
|
||||
del self.app.rns.config['interfaces'][interface_name]
|
||||
self.app.rns.config.write()
|
||||
try: self.app.interface_profiles.remove_interface(interface_name)
|
||||
except Exception: pass
|
||||
|
||||
if selected_item in self.interface_items:
|
||||
self.interface_items.remove(selected_item)
|
||||
|
||||
self._build_interface_items()
|
||||
self._rebuild_list()
|
||||
self.dismiss_dialog()
|
||||
|
||||
@@ -3142,20 +3494,82 @@ class InterfaceDisplay:
|
||||
self.widget = self.interfaces_display
|
||||
self.app.ui.main_display.update_active_sub_display()
|
||||
|
||||
def _build_interface_items(self):
|
||||
self.interface_items = []
|
||||
self.interface_tiles = []
|
||||
interfaces = self.app.rns.config['interfaces']
|
||||
processed_interfaces = {}
|
||||
|
||||
for interface_name, interface in interfaces.items():
|
||||
interface_data = interface.copy()
|
||||
|
||||
if interface_data.get("type") == "RNodeMultiInterface":
|
||||
sub_interfaces = []
|
||||
for sub_name, sub_config in interface_data.items():
|
||||
if sub_name not in {"type", "port", "interface_enabled", "selected_interface_mode",
|
||||
"configured_bitrate"}:
|
||||
if isinstance(sub_config, dict):
|
||||
sub_config["name"] = sub_name
|
||||
sub_interfaces.append(sub_config)
|
||||
|
||||
interface_data["sub_interfaces"] = sub_interfaces
|
||||
|
||||
for sub in sub_interfaces:
|
||||
del interface_data[sub["name"]]
|
||||
|
||||
processed_interfaces[interface_name] = interface_data
|
||||
|
||||
self.app.interface_stats = self.app.rns.get_interface_stats()
|
||||
stats_lookup = {interface['short_name']: interface for interface in self.app.interface_stats['interfaces']}
|
||||
|
||||
profiles = getattr(self.app, "interface_profiles", None)
|
||||
|
||||
for interface_name, interface_data in processed_interfaces.items():
|
||||
is_enabled = str(interface_data.get("enabled")).lower() not in ('false', 'off', 'no', '0') and str(interface_data.get("interface_enabled")).lower() not in ('false', 'off', 'no', '0')
|
||||
|
||||
iface_type = interface_data.get("type", "Unknown")
|
||||
icon = _get_interface_icon(self.glyphset, iface_type)
|
||||
|
||||
stats_for_interface = stats_lookup.get(interface_name)
|
||||
|
||||
if stats_for_interface:
|
||||
tx = stats_for_interface.get("txb", 0)
|
||||
rx = stats_for_interface.get("rxb", 0)
|
||||
is_connected = stats_for_interface["status"]
|
||||
else:
|
||||
tx = 0
|
||||
rx = 0
|
||||
is_connected = False
|
||||
|
||||
profiles_label = profiles.label_for(interface_name) if profiles is not None else None
|
||||
|
||||
item = SelectableInterfaceItem(
|
||||
parent=self,
|
||||
name=interface_data.get("name", interface_name),
|
||||
is_connected=is_connected,
|
||||
is_enabled=is_enabled,
|
||||
iface_type=iface_type,
|
||||
tx=tx,
|
||||
rx=rx,
|
||||
icon=icon,
|
||||
profiles_label=profiles_label
|
||||
)
|
||||
|
||||
self.interface_items.append(item)
|
||||
|
||||
tile = InterfaceTile(
|
||||
parent=self,
|
||||
name=interface_data.get("name", interface_name),
|
||||
is_connected=is_connected,
|
||||
is_enabled=is_enabled,
|
||||
iface_type=iface_type,
|
||||
icon=icon,
|
||||
)
|
||||
self.interface_tiles.append(tile)
|
||||
|
||||
def _rebuild_list(self):
|
||||
interface_header = urwid.Text(("interface_title", f"Interfaces ({len(self.interface_items)})"), align="center")
|
||||
if len(self.interface_items) == 0:
|
||||
interface_header = urwid.Text(("interface_title", "No interfaces found. Press Ctrl + A to add a new interface "), align="center")
|
||||
|
||||
new_list = [
|
||||
interface_header,
|
||||
urwid.Divider(),
|
||||
] + self.interface_items
|
||||
# RNS.log(f"items: {self.interface_items}")
|
||||
|
||||
walker = urwid.SimpleFocusListWalker(new_list)
|
||||
self.box_adapter._original_widget.body = walker
|
||||
self.box_adapter._original_widget.focus_position = len(new_list) - 1
|
||||
self._set_iface_body()
|
||||
self.tabs_pile.contents[0] = (self._make_tab_bar("interfaces"), self.tabs_pile.options('pack'))
|
||||
|
||||
def open_config_editor(self):
|
||||
import platform
|
||||
@@ -3188,7 +3602,7 @@ class InterfaceDisplay:
|
||||
class InterfaceDisplayShortcuts:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.default_shortcuts = "[C-a] Add Interface [C-e] Edit Interface [C-x] Remove Interface [Enter] Show Interface [C-w] Open Text Editor"
|
||||
self.default_shortcuts = "[C-a] Add [C-e] Edit [C-x] Remove [Enter] Show [C-t] Grid/Rows [C-w] Editor [Tab] Profiles"
|
||||
self.current_shortcuts = self.default_shortcuts
|
||||
self.widget = urwid.AttrMap(
|
||||
urwid.Text(self.current_shortcuts),
|
||||
@@ -3212,4 +3626,8 @@ class InterfaceDisplayShortcuts:
|
||||
|
||||
def set_edit_interface_shortcuts(self):
|
||||
edit_shortcuts = "[Up/Down] Navigate Fields [Enter] Select Option"
|
||||
self.update_shortcuts(edit_shortcuts)
|
||||
self.update_shortcuts(edit_shortcuts)
|
||||
|
||||
def set_profiles_tab_shortcuts(self):
|
||||
profiles_shortcuts = "[Up/Down] Navigate [Space] Select Profile [C-a] New [C-e] Rename [C-x] Delete [Tab] Interfaces"
|
||||
self.update_shortcuts(profiles_shortcuts)
|
||||
Reference in New Issue
Block a user