mirror of
https://github.com/markqvist/NomadNet.git
synced 2026-10-06 10:17:47 +00:00
2345 lines
100 KiB
Python
2345 lines
100 KiB
Python
import RNS
|
|
import LXMF
|
|
import io
|
|
import os
|
|
import time
|
|
import bisect
|
|
import urwid
|
|
import shutil
|
|
import nomadnet
|
|
import subprocess
|
|
import threading
|
|
from threading import Lock
|
|
from .MicronParser import markup_to_attrmaps, make_style, default_state
|
|
from nomadnet.Directory import DirectoryEntry
|
|
from nomadnet.Conversation import ConversationMessage
|
|
from nomadnet.vendor.Scrollable import *
|
|
from nomadnet.util import strip_modifiers
|
|
from nomadnet.util import sanitize_name
|
|
from nomadnet.ui.textui.images import _termlib
|
|
from .Helpers import ClickableIcon, osc52_copy
|
|
from .ReadlineEdit import ReadlineMixin, ReadlineEdit
|
|
|
|
IMGLOAD_NEVER = 0x00
|
|
IMGLOAD_MANUAL = 0x01
|
|
IMGLOAD_AUTO = 0x02
|
|
IMGLOAD_ALWAYS = 0x03
|
|
|
|
class BrowserFrame(urwid.Frame):
|
|
def keypress(self, size, key):
|
|
if key == "ctrl w":
|
|
self.delegate.disconnect()
|
|
elif key == "ctrl d":
|
|
self.delegate.back()
|
|
elif key == "ctrl f":
|
|
self.delegate.forward()
|
|
elif key == "ctrl r":
|
|
self.delegate.reload()
|
|
elif key == "ctrl u":
|
|
self.delegate.url_dialog()
|
|
elif key == "ctrl l":
|
|
self.delegate.load_images()
|
|
elif key == "ctrl x":
|
|
self.delegate.load_images(force_reload=True)
|
|
elif key == "ctrl s":
|
|
self.delegate.save_node_dialog()
|
|
elif key == "ctrl b":
|
|
self.delegate.save_node_dialog()
|
|
elif key == "ctrl g":
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen()
|
|
elif key == "ctrl y":
|
|
if self.delegate.app.config["textui"]["clipboard_copy"]:
|
|
self.delegate.copy_url()
|
|
elif self.focus_position == "body":
|
|
if key == "down" or key == "up":
|
|
try:
|
|
if hasattr(self.delegate, "page_list") and self.delegate.page_list:
|
|
def df(loop, user_data):
|
|
st = None
|
|
if self.delegate.page_list:
|
|
nf = self.delegate.page_list.focus
|
|
if hasattr(nf, "key_timeout"):
|
|
st = nf
|
|
elif hasattr(nf, "original_widget"):
|
|
no = nf.original_widget
|
|
if hasattr(no, "original_widget"):
|
|
st = no.original_widget
|
|
else:
|
|
if hasattr(no, "key_timeout"):
|
|
st = no
|
|
|
|
if st and hasattr(st, "key_timeout") and hasattr(st, "keypress") and callable(st.keypress):
|
|
st.keypress(None, None)
|
|
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.25, df)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while setting up cursor timeout. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
|
|
return super(BrowserFrame, self).keypress(size, key)
|
|
|
|
else:
|
|
return super(BrowserFrame, self).keypress(size, key)
|
|
|
|
class PageList(urwid.ListBox):
|
|
def __init__(self, body):
|
|
super().__init__(body)
|
|
self._page_size = (0, 0)
|
|
self._page_focus = None
|
|
self._page_layouts = {}
|
|
|
|
def invalidate_layout(self):
|
|
self._page_layouts = {}
|
|
self._invalidate()
|
|
|
|
def _layout(self, maxcol):
|
|
if maxcol not in self._page_layouts:
|
|
layout_started = time.time()
|
|
offsets = [0]
|
|
for widget in self.body:
|
|
offsets.append(offsets[-1] + widget.rows((maxcol,), True))
|
|
self._page_layouts[maxcol] = offsets
|
|
# RNS.log(f" rebuilt {len(self.body)} widgets, {offsets[-1]} rows, width {maxcol}, {(time.time()-layout_started)*1000:.1f}ms", RNS.LOG_DEBUG)
|
|
return self._page_layouts[maxcol]
|
|
|
|
def render(self, size, focus=False):
|
|
self._page_size = size
|
|
if self.body and self.focus is not self._page_focus:
|
|
self._page_focus = self.focus
|
|
widget = self.focus
|
|
while widget is not None:
|
|
widget._invalidate()
|
|
widget = getattr(widget, "original_widget", None)
|
|
return super().render(size, focus)
|
|
|
|
def rows_max(self, size=None, focus=False):
|
|
size = size or self._page_size
|
|
return self._layout(size[0])[-1]
|
|
|
|
def get_scrollpos(self, size=None, focus=False):
|
|
size = size or self._page_size
|
|
if not self.body or not size[1]: return 0
|
|
|
|
middle, top, bottom = self.calculate_visible(size, focus)
|
|
if middle is None: return 0
|
|
return self._layout(size[0])[self.top_position(size)] + top.trim
|
|
|
|
def set_scrollpos(self, position):
|
|
size = self._page_size
|
|
maxcol, maxrow = size
|
|
if not self.body or not maxrow: return
|
|
|
|
offsets = self._layout(maxcol)
|
|
position = max(0, min(int(position), max(0, offsets[-1] - maxrow)))
|
|
pos = bisect.bisect_left(offsets, position, 0, len(self.body))
|
|
if pos == len(self.body) or offsets[pos] - position >= maxrow: pos -= 1
|
|
self.change_focus(size, pos, offsets[pos] - position)
|
|
|
|
def top_position(self, size=None):
|
|
size = size or self._page_size
|
|
if not self.body or not size[1]: return 0
|
|
middle, top, bottom = self.calculate_visible(size, True)
|
|
if middle is None: return 0
|
|
return top.fill[-1].position if top.fill else middle.focus_pos
|
|
|
|
def scroll(self, rows):
|
|
self.set_scrollpos(self.get_scrollpos() + rows)
|
|
|
|
def focused_widget_name(self):
|
|
widget = self.focus
|
|
while getattr(widget, "original_widget", None) is not None: widget = widget.original_widget
|
|
return type(widget).__name__
|
|
|
|
def keypress(self, size, key):
|
|
self._page_size = size
|
|
key_started = time.time()
|
|
focus_before = self.focus_position if self.body else None
|
|
scroll_before = self.get_scrollpos(size, True)
|
|
unhandled_key = self.handle_key(size, key)
|
|
|
|
# RNS.log(f" {key}: {unhandled_key} focus {focus_before} -> {self.focus_position if self.body else None} ({self.focused_widget_name()}) scrollpos {scroll_before}->{self.get_scrollpos(size, True)}, {(time.time()-key_started) * 1000:.1f}ms", RNS.LOG_DEBUG)
|
|
return unhandled_key
|
|
|
|
def handle_key(self, size, key):
|
|
try:
|
|
if key in ("page up", "page down", "home", "end"):
|
|
focus_widget = self.focus
|
|
if focus_widget is not None and focus_widget.selectable():
|
|
key = focus_widget.keypress((size[0],), key)
|
|
if key is None:
|
|
self.make_cursor_visible(size)
|
|
return None
|
|
|
|
if key == "page down": self.scroll(size[1] - 1)
|
|
elif key == "page up": self.scroll(1 - size[1])
|
|
elif key == "home": self.set_scrollpos(0)
|
|
elif key == "end": self.set_scrollpos(self.rows_max(size))
|
|
return None
|
|
|
|
unhandled_key = super().keypress(size, key)
|
|
if key in ("up", "down"): return None
|
|
return unhandled_key
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while handling browser keypress: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
return None
|
|
|
|
def mouse_event(self, size, event, button, col, row, focus):
|
|
self._page_size = size
|
|
if button in (4, 5): return False
|
|
return super().mouse_event(size, event, button, col, row, focus)
|
|
|
|
class Browser:
|
|
DEFAULT_PATH = "/page/index.mu"
|
|
DEFAULT_TIMEOUT = 10
|
|
DEFAULT_CACHE_TIME = 12*60*60
|
|
|
|
NO_PATH = 0x00
|
|
PATH_REQUESTED = 0x01
|
|
ESTABLISHING_LINK = 0x02
|
|
LINK_TIMEOUT = 0x03
|
|
LINK_ESTABLISHED = 0x04
|
|
REQUESTING = 0x05
|
|
REQUEST_SENT = 0x06
|
|
REQUEST_FAILED = 0x07
|
|
REQUEST_TIMEOUT = 0x08
|
|
RECEIVING_RESPONSE = 0x09
|
|
DISCONECTED = 0xFE
|
|
DONE = 0xFF
|
|
|
|
def __init__(self, app, app_name, aspects, destination_hash = None, path = None, auth_identity = None, delegate = None):
|
|
self.app = app
|
|
self.g = self.app.ui.glyphs
|
|
self.delegate = delegate
|
|
self.app_name = app_name
|
|
self.aspects = aspects
|
|
self.destination_hash = destination_hash
|
|
self.path = path
|
|
self.request_data = None
|
|
self.timeout = Browser.DEFAULT_TIMEOUT
|
|
self.last_keypress = None
|
|
|
|
self.link = None
|
|
self.loopback = None
|
|
self.status = Browser.DISCONECTED
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.response_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_size = 0
|
|
self.file_saved_at = 0
|
|
self.file_save_notice_timeout = 3
|
|
self.page_data = None
|
|
self.displayed_page_data = None
|
|
self.auth_identity = auth_identity
|
|
self.display_widget = None
|
|
self.link_status_showing = False
|
|
self.link_target = None
|
|
self.frame = None
|
|
self.attr_maps = []
|
|
self.page_list = None
|
|
self.page_rows = []
|
|
self.page_partials = {}
|
|
self.page_images = {}
|
|
self.updater_running = False
|
|
self.image_updater_running = False
|
|
self.partial_updater_lock = Lock()
|
|
self.image_updater_lock = Lock()
|
|
self.image_mode = self.app.config["textui"]["image_loading"]
|
|
self.image_rendering_supported = _termlib.is_kitty_supported() and self.image_mode != IMGLOAD_NEVER
|
|
self.build_display()
|
|
|
|
self.history = []
|
|
self.history_ptr = 0
|
|
self.history_inc = False
|
|
self.history_dec = False
|
|
self.reloading = False
|
|
self.loaded_from_cache = False
|
|
|
|
if self.path == None:
|
|
self.path = Browser.DEFAULT_PATH
|
|
|
|
if self.destination_hash != None:
|
|
self.load_page()
|
|
|
|
self.clean_cache()
|
|
|
|
if self.image_rendering_supported: RNS.log(f"In-browser image rendering supported", RNS.LOG_DEBUG)
|
|
else: RNS.log(f"In-browser image rendering is not supported", RNS.LOG_DEBUG)
|
|
|
|
def current_url(self):
|
|
if self.destination_hash == None:
|
|
return ""
|
|
path = "" if self.path == None else self.path
|
|
url = RNS.hexrep(self.destination_hash, delimit=False)+":"+path
|
|
|
|
if isinstance(self.request_data, dict) and self.request_data:
|
|
parts = []
|
|
for k, v in self.request_data.items():
|
|
if not isinstance(k, str):
|
|
continue
|
|
if not k.startswith("var_"):
|
|
continue
|
|
parts.append(k[4:]+"="+str(v))
|
|
if parts:
|
|
url += "`" + "|".join(parts)
|
|
|
|
return url
|
|
|
|
def url_hash(self, url):
|
|
if url == None:
|
|
return None
|
|
else:
|
|
url = url.encode("utf-8")
|
|
return RNS.hexrep(RNS.Identity.full_hash(url), delimit=False)
|
|
|
|
|
|
def marked_link(self, link_target, link_fields=None):
|
|
if link_fields:
|
|
fields_str = "|".join(link_fields)
|
|
link_target = f"{link_target}`{fields_str}"
|
|
if self.status == Browser.DONE:
|
|
self.link_target = link_target
|
|
self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job)
|
|
|
|
def marked_link_job(self, sender, event):
|
|
link_target = self.link_target
|
|
can_display = time.time() > self.file_saved_at + self.file_save_notice_timeout
|
|
|
|
if link_target == None:
|
|
if self.link_status_showing:
|
|
self.browser_footer = self.make_status_widget()
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
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:
|
|
if can_display:
|
|
self.link_status_showing = True
|
|
lt_str = str(link_target)
|
|
lmax = self._content_cols()
|
|
lstr = "Link to "+lt_str
|
|
if len(lstr) > lmax: lstr = lstr[:lmax-1]+"…"
|
|
self.browser_footer = urwid.AttrMap(urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text(lstr)]), "browser_controls")
|
|
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):
|
|
if destination_type == "nnn":
|
|
return "nomadnetwork.node"
|
|
elif destination_type == "lxmf":
|
|
return "lxmf.delivery"
|
|
elif destination_type == "rrc":
|
|
return "rrc.hub.session"
|
|
else:
|
|
return destination_type
|
|
|
|
def handle_link(self, link_target, link_data = None):
|
|
partial_ids = None
|
|
request_data = None
|
|
if link_data != None:
|
|
link_fields = []
|
|
request_data = {}
|
|
all_fields = True if "*" in link_data else False
|
|
|
|
for e in link_data:
|
|
if "=" in e:
|
|
c = e.split("=")
|
|
if len(c) == 2:
|
|
request_data["var_"+str(c[0])] = str(c[1])
|
|
else:
|
|
link_fields.append(e)
|
|
|
|
def recurse_down(w):
|
|
if isinstance(w, list):
|
|
for t in w:
|
|
recurse_down(t)
|
|
elif isinstance(w, tuple):
|
|
for t in w:
|
|
recurse_down(t)
|
|
elif hasattr(w, "contents"):
|
|
recurse_down(w.contents)
|
|
elif hasattr(w, "original_widget"):
|
|
recurse_down(w.original_widget)
|
|
elif hasattr(w, "_original_widget"):
|
|
recurse_down(w._original_widget)
|
|
else:
|
|
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
|
|
field_key = "field_" + w.field_name
|
|
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)
|
|
RNS.log("Including request data: "+str(request_data), RNS.LOG_EXTREME)
|
|
|
|
# In-document anchor link (#name or empty #)
|
|
if link_target.startswith("#"):
|
|
def df(loop, user_data): self._jump_to_anchor(link_target[1:])
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.0, df)
|
|
return
|
|
|
|
# rrc://<hex>[:<dest_name>]/<room> URL form
|
|
if link_target.startswith("rrc://"):
|
|
self.handle_rrc_link(link_target[6:])
|
|
return
|
|
|
|
components = link_target.split("@")
|
|
destination_type = None
|
|
|
|
if len(components) == 2:
|
|
destination_type = self.expand_shorthands(components[0])
|
|
link_target = components[1]
|
|
elif link_target.startswith("p:"):
|
|
comps = link_target.split(":")
|
|
if len(comps) > 1: partial_ids = comps[1:]
|
|
destination_type = "partial"
|
|
else:
|
|
destination_type = "nomadnetwork.node"
|
|
link_target = components[0]
|
|
|
|
if destination_type == "nomadnetwork.node":
|
|
if self.status >= Browser.DISCONECTED:
|
|
RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG)
|
|
self.browser_footer = urwid.Text("Opening link to: "+str(link_target))
|
|
try:
|
|
self.retrieve_url(link_target, request_data)
|
|
except Exception as e:
|
|
self.browser_footer = urwid.Text("Could not open link: "+str(e))
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
else:
|
|
RNS.log("Browser already handling link, cannot handle link to: "+str(link_target), RNS.LOG_DEBUG)
|
|
|
|
elif destination_type == "lxmf.delivery":
|
|
RNS.log("Passing LXMF link to handler", RNS.LOG_DEBUG)
|
|
self.handle_lxmf_link(link_target)
|
|
|
|
elif destination_type == "rrc.hub.session":
|
|
RNS.log("Passing RRC link to handler", RNS.LOG_DEBUG)
|
|
self.handle_rrc_link(link_target)
|
|
|
|
elif destination_type == "partial":
|
|
if partial_ids != None and len(partial_ids) > 0: self.handle_partial_updates(partial_ids)
|
|
|
|
else:
|
|
RNS.log("No known handler for destination type "+str(destination_type), RNS.LOG_DEBUG)
|
|
self.browser_footer = urwid.Text("Could not open link: "+"No known handler for destination type "+str(destination_type))
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
def _jump_to_anchor(self, name):
|
|
anchors = getattr(self.attr_maps, "anchors", None) or {}
|
|
header_rows = getattr(self.attr_maps, "header_rows", None) or []
|
|
|
|
target_idx = None
|
|
if name:
|
|
target_idx = anchors.get(name)
|
|
if target_idx is None:
|
|
self.browser_footer = urwid.Text("Unknown anchor: #"+name)
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
return
|
|
else:
|
|
current = 0
|
|
try:
|
|
current = self.page_rows[self.page_list.top_position()]
|
|
except Exception:
|
|
current = 0
|
|
for hr in header_rows:
|
|
if hr > current:
|
|
target_idx = hr
|
|
break
|
|
if target_idx is None:
|
|
return
|
|
|
|
self.reveal_index(int(target_idx))
|
|
|
|
try:
|
|
self.page_list.set_focus(self.page_rows.index(int(target_idx)))
|
|
self.page_list.set_focus_valign("top")
|
|
|
|
except Exception as e: RNS.log("Anchor jump failed: "+str(e), RNS.LOG_ERROR)
|
|
|
|
def _content_cols(self):
|
|
try:
|
|
lw = self.delegate.given_list_width
|
|
screen_cols = self.app.ui.loop.screen.get_cols_rows()[0]
|
|
browser_cols = screen_cols-lw-2
|
|
cols = browser_cols
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Could not calculate browser width, anchors will be inaccurate: {e}", RNS.LOG_WARNING)
|
|
RNS.trace_exception(e)
|
|
cols = 100
|
|
|
|
return max(40, cols)
|
|
|
|
def handle_lxmf_link(self, link_target):
|
|
try:
|
|
def san(name):
|
|
if self.app.config["textui"]["sanitize_names"]: return sanitize_name(name)
|
|
else: return strip_modifiers(name)
|
|
|
|
if not type(link_target) is str:
|
|
raise ValueError("Invalid data type for LXMF link")
|
|
|
|
if len(link_target) != (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
|
raise ValueError("Invalid length for LXMF link")
|
|
|
|
try:
|
|
bytes.fromhex(link_target)
|
|
|
|
except Exception as e:
|
|
raise ValueError("Could not decode destination hash from LXMF link")
|
|
|
|
existing_conversations = nomadnet.Conversation.conversation_list(self.app)
|
|
|
|
source_hash_text = link_target
|
|
display_name_data = RNS.Identity.recall_app_data(bytes.fromhex(source_hash_text))
|
|
|
|
display_name = None
|
|
if display_name_data != None:
|
|
display_name = san(LXMF.display_name_from_app_data(display_name_data))
|
|
|
|
if not source_hash_text in [c[0] for c in existing_conversations]:
|
|
entry = DirectoryEntry(bytes.fromhex(source_hash_text), display_name=display_name)
|
|
self.app.directory.remember(entry)
|
|
|
|
new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True)
|
|
self.app.ui.main_display.sub_displays.conversations_display.update_conversation_list()
|
|
|
|
self.app.ui.main_display.sub_displays.conversations_display.display_conversation(None, source_hash_text)
|
|
self.app.ui.main_display.show_conversations(None)
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while starting conversation from link. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
self.browser_footer = urwid.Text("Could not open LXMF link: "+str(e))
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
|
|
def handle_rrc_link(self, link_target):
|
|
try:
|
|
if not isinstance(link_target, str):
|
|
raise ValueError("invalid RRC link payload")
|
|
rest = link_target.strip()
|
|
if rest.startswith("/"):
|
|
rest = rest[1:]
|
|
hub_part, _, room = rest.partition("/")
|
|
hex_part, _, dest = hub_part.partition(":")
|
|
hex_part = hex_part.strip()
|
|
dest = dest.strip() or None
|
|
try:
|
|
hub_hash = bytes.fromhex(hex_part)
|
|
except Exception:
|
|
raise ValueError("invalid hub hash")
|
|
expected_len = RNS.Reticulum.TRUNCATED_HASHLENGTH // 8
|
|
if len(hub_hash) != expected_len:
|
|
raise ValueError("hub hash must be "+str(expected_len)+" bytes")
|
|
|
|
room = room.strip().lstrip("#").strip()
|
|
room_norm = room.lower() if room else None
|
|
|
|
existing = self.app.rrc.find_hub(hub_hash, dest_name=dest)
|
|
self.app.ui.main_display.show_channels(None)
|
|
channels = self.app.ui.main_display.sub_displays.channels_display
|
|
|
|
if existing is not None:
|
|
channels.update_list()
|
|
if room_norm:
|
|
channels._select_room(None, (existing, room_norm))
|
|
else:
|
|
channels._select_hub(None, existing)
|
|
return
|
|
|
|
|
|
channels.confirm_new_hub_dialog(hub_hash, dest, room_norm)
|
|
|
|
except Exception as e:
|
|
RNS.log("Could not open RRC link: "+str(e), RNS.LOG_ERROR)
|
|
self.browser_footer = urwid.Text("Could not open RRC link: "+str(e))
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
|
|
def micron_released_focus(self):
|
|
if self.delegate != None:
|
|
self.delegate.focus_lists()
|
|
|
|
def build_display(self):
|
|
self.browser_header = urwid.Text("")
|
|
self.browser_footer = urwid.Text("")
|
|
|
|
self.page_list = None
|
|
self.page_rows = []
|
|
self.page_partials = {}
|
|
self.page_images = {}
|
|
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.delegate = self
|
|
self.linebox = urwid.LineBox(self.frame, title="Remote Node")
|
|
self.display_widget = urwid.AttrMap(self.linebox, "browser_inactive")
|
|
|
|
def make_status_widget(self):
|
|
if self.response_progress > 0:
|
|
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])
|
|
else:
|
|
widget = urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text(self.status_text())])
|
|
|
|
return urwid.AttrMap(widget, "browser_controls")
|
|
|
|
def make_control_widget(self):
|
|
clipboard_copy_enabled = self.app.config["textui"]["clipboard_copy"]
|
|
copy_glyph = self.g.get("copy", "[C]")
|
|
lstr = self.g["node"]+" "+self.current_url()
|
|
if clipboard_copy_enabled:
|
|
lmax = self._content_cols()-len(copy_glyph)-len(copy_glyph)-1
|
|
else:
|
|
lmax = self._content_cols()-1
|
|
if len(lstr) > lmax: lstr = lstr[:lmax-1]+"…"
|
|
url_text = urwid.Text(lstr)
|
|
if clipboard_copy_enabled:
|
|
copy_icon = ClickableIcon(copy_glyph, on_click=lambda: self.copy_url())
|
|
copy_width = len(copy_glyph) + 2
|
|
header_row = urwid.Columns([
|
|
("weight", 1, url_text),
|
|
(copy_width, urwid.Padding(copy_icon, left=1, right=1)),
|
|
])
|
|
else:
|
|
header_row = url_text
|
|
return urwid.AttrMap(urwid.Pile([header_row, urwid.Divider(self.g["divider1"])]), "browser_controls")
|
|
|
|
def make_request_failed_widget(self):
|
|
def back_action(sender):
|
|
self.status = Browser.DONE
|
|
self.destination_hash = self.previous_destination_hash
|
|
self.path = self.previous_path
|
|
self.update_display()
|
|
|
|
columns = urwid.Columns([
|
|
(urwid.WEIGHT, 0.5, urwid.Text(" ")),
|
|
(8, urwid.Button("Back", on_press=back_action)),
|
|
(urwid.WEIGHT, 0.5, urwid.Text(" ")),
|
|
])
|
|
|
|
if len(self.attr_maps) > 0:
|
|
pile = urwid.Pile([
|
|
urwid.Text("!\n\n"+self.status_text()+"\n", align=urwid.CENTER),
|
|
columns
|
|
])
|
|
else:
|
|
pile = urwid.Pile([urwid.Text("!\n\n"+self.status_text(), align=urwid.CENTER)])
|
|
|
|
return urwid.Filler(pile, urwid.MIDDLE)
|
|
|
|
def update_display(self):
|
|
if self.status == Browser.DISCONECTED:
|
|
self.display_widget.set_attr_map({None: "browser_inactive"})
|
|
self.page_list = None
|
|
self.page_rows = []
|
|
self.page_partials = {}
|
|
self.page_images = {}
|
|
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_header = urwid.Text("")
|
|
self.linebox.set_title("Remote Node")
|
|
else:
|
|
self.display_widget.set_attr_map({None: "body_text"})
|
|
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:
|
|
remote_display_string = self.app.directory.simplest_display_str(self.destination_hash)
|
|
else:
|
|
remote_display_string = ""
|
|
|
|
if self.loopback != None and remote_display_string == RNS.prettyhexrep(self.loopback):
|
|
remote_display_string = self.app.node.name
|
|
|
|
self.linebox.set_title(remote_display_string)
|
|
|
|
if self.status == Browser.DONE:
|
|
self.browser_footer = self.make_status_widget()
|
|
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:
|
|
self.browser_body = self.make_request_failed_widget()
|
|
self.browser_footer = urwid.Text("")
|
|
|
|
elif self.status <= Browser.REQUEST_SENT:
|
|
if len(self.attr_maps) == 0:
|
|
self.browser_body = urwid.Filler(
|
|
urwid.Text("Retrieving\n["+self.current_url()+"]", align=urwid.CENTER),
|
|
urwid.MIDDLE,
|
|
)
|
|
|
|
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:
|
|
self.browser_body = self.make_request_failed_widget()
|
|
self.browser_footer = urwid.Text("")
|
|
|
|
elif self.status == Browser.REQUEST_TIMEOUT:
|
|
self.browser_body = self.make_request_failed_widget()
|
|
self.browser_footer = urwid.Text("")
|
|
|
|
else:
|
|
pass
|
|
|
|
self.frame.contents["body"] = (self.browser_body, self.frame.options())
|
|
self.frame.contents["header"] = (self.browser_header, self.frame.options())
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
def update_page_display(self):
|
|
self._purge_page_images()
|
|
self.page_rows = list(range(len(self.attr_maps)))
|
|
self.page_list = PageList(urwid.SimpleFocusListWalker(list(self.attr_maps)))
|
|
self.page_partials = {}
|
|
self.page_images = {}
|
|
self.browser_body = urwid.AttrMap(ScrollBar(self.page_list, thumb_char="\u2503", trough_char=" "), "scrollbar")
|
|
self.detect_partials()
|
|
if self.should_load_images(): self.detect_images()
|
|
self.init_folds()
|
|
|
|
def refresh_page_layout(self):
|
|
if self.page_list is not None: self.page_list.invalidate_layout()
|
|
|
|
def image_cache_path(self, url):
|
|
url_hash = self.url_hash(url)
|
|
if not url_hash: return None
|
|
else:
|
|
path = f"{self.app.cachepath}/images/{url_hash}"
|
|
return path
|
|
|
|
def resolve_image(self, url):
|
|
cachepath = self.image_cache_path(url)
|
|
if not cachepath: return None
|
|
else:
|
|
if os.path.exists(cachepath): return cachepath
|
|
else: return None
|
|
|
|
def _purge_page_images(self):
|
|
if not self.image_rendering_supported: return
|
|
try:
|
|
screen = getattr(getattr(self.app.ui, "loop", None), "screen", None)
|
|
if hasattr(screen, "purge_images"): screen.purge_images()
|
|
except Exception: pass
|
|
|
|
def should_load_images(self):
|
|
if self.image_mode == IMGLOAD_NEVER: return False
|
|
elif self.image_mode == IMGLOAD_MANUAL: return False
|
|
elif self.image_mode == IMGLOAD_ALWAYS: return True
|
|
elif self.image_mode == IMGLOAD_AUTO:
|
|
if self.destination_hash == self.loopback: return True
|
|
rtt_limit = 1.5; edr_limit = 10000
|
|
if self.link:
|
|
rtt = self.link.rtt
|
|
edr = self.link.get_expected_rate() or self.last_response_speed()
|
|
rtt_ok = rtt and rtt < rtt_limit
|
|
edr_ok = edr and edr > edr_limit
|
|
RNS.log(f"Checking image load conditions, RTT is {RNS.prettyshorttime(rtt) if rtt else 'unknown'}, EDR is {RNS.prettyspeed(edr) if edr else 'unknown'}", RNS.LOG_DEBUG)
|
|
if rtt_ok and edr_ok: return True
|
|
else: return False
|
|
|
|
return False
|
|
|
|
def load_images(self, force_reload=False):
|
|
if not self.image_rendering_supported: return
|
|
elif self.image_mode == IMGLOAD_NEVER: return
|
|
else: self.detect_images(force_reload=force_reload)
|
|
|
|
def detect_images(self, force_reload=False):
|
|
if not self.image_rendering_supported: return
|
|
for w in self.attr_maps:
|
|
o = w._original_widget
|
|
if hasattr(o, "_contained_image"): o = o._contained_image
|
|
if hasattr(o, "image_url"):
|
|
resolved_image = self.resolve_image(o.image_url)
|
|
if resolved_image and force_reload:
|
|
RNS.log(f"Clearing image cache for {o.image_url} ({resolved_image})", RNS.LOG_DEBUG)
|
|
try: os.unlink(resolved_image)
|
|
except Exception as e: RNS.log(f"Could not clear image cache for {o.image_url}: {e}", RNS.LOG_ERROR)
|
|
resolved_image = None
|
|
|
|
if force_reload: continue
|
|
elif resolved_image and self.image_rendering_supported: o.load(resolved_image, remote_source=True)
|
|
else:
|
|
image = { "widget": o, "id": o.image_id, "url": o.image_url, "refresh": None, "updated": None,
|
|
"update_requested": None, "request_id": None, "link": None, "failed": False, "pr_throttle": 0,
|
|
"progress_updated": None, "previous_progress": 0 }
|
|
|
|
self.page_images[o.image_id] = image
|
|
|
|
self.refresh_page_layout()
|
|
if force_reload: self.reload()
|
|
elif len(self.page_images) > 0: self.start_image_updater()
|
|
|
|
def start_image_updater(self):
|
|
if not self.image_rendering_supported: return
|
|
if not self.image_updater_running: self.update_images()
|
|
|
|
def update_images(self, loop=None, user_data=None):
|
|
if not self.image_rendering_supported: return
|
|
def job():
|
|
if self.image_updater_lock.locked(): return
|
|
with self.image_updater_lock:
|
|
self.image_updater_running = True
|
|
|
|
for iid in self.page_images:
|
|
try:
|
|
image = self.page_images[iid]
|
|
if image["failed"]: continue
|
|
if not image["updated"] or (image["refresh"] != None and time.time() > image["updated"]+image["refresh"]):
|
|
image["update_requested"] = time.time()
|
|
self.__load_image(image)
|
|
while not image["updated"] and not image["failed"] and len(self.page_images) > 0: time.sleep(0.2)
|
|
|
|
except Exception as e: RNS.log(f"Error updating page image: {e}", RNS.LOG_ERROR)
|
|
|
|
if len(self.page_images) > 0: self.app.ui.loop.set_alarm_in(1, self.update_images)
|
|
else: self.image_updater_running = False
|
|
|
|
threading.Thread(target=job, daemon=True).start()
|
|
|
|
def __load_image(self, image):
|
|
if image["failed"] == True: return
|
|
w = image["widget"]
|
|
url = image["url"]
|
|
try: image_destination_hash, path = self.parse_url(image["url"])
|
|
except Exception as e:
|
|
RNS.log(f"Could not parse image URL: {e}", RNS.LOG_ERROR)
|
|
image["failed"] = True
|
|
w.notice(f"Could not load image {url}: {e}")
|
|
return
|
|
|
|
if image_destination_hash == self.loopback:
|
|
local_image = path.replace("/media/", "")
|
|
local_path = f"{self.app.pagespath}/{local_image}"
|
|
if self.image_rendering_supported:
|
|
w.load(local_path)
|
|
self.refresh_page_layout()
|
|
image["updated"] = time.time()
|
|
return
|
|
|
|
if not RNS.Transport.has_path(image_destination_hash):
|
|
if time.time() <= image["pr_throttle"]: return
|
|
else:
|
|
image["pr_throttle"] = time.time()+15
|
|
RNS.log(f"Requesting path for image: {path}", RNS.LOG_DEBUG)
|
|
RNS.Transport.request_path(image_destination_hash)
|
|
pr_time = time.time()+self.path_timeout(image_destination_hash)
|
|
while not RNS.Transport.has_path(image_destination_hash):
|
|
if time.time() > pr_time: return
|
|
time.sleep(0.25)
|
|
|
|
if not image["link"]:
|
|
if self.link and self.link.destination.hash == image_destination_hash and self.link.status == RNS.Link.ACTIVE:
|
|
image["link"] = self.link
|
|
RNS.log(f"Re-using existing page link for image {url}", RNS.LOG_EXTREME)
|
|
|
|
if not image["link"]:
|
|
for pid in self.page_images:
|
|
other_image = self.page_images[pid]
|
|
if other_image["link"]:
|
|
existing_link = other_image["link"]
|
|
if existing_link.destination.hash == image_destination_hash and existing_link.status == RNS.Link.ACTIVE:
|
|
RNS.log(f"Re-using existing link for image {url}", RNS.LOG_EXTREME)
|
|
image["link"] = existing_link
|
|
break
|
|
|
|
if not image["link"] or (image["link"] and image["link"].status) == RNS.Link.CLOSED:
|
|
RNS.log(f"Establishing link for image: {image_destination_hash} / {path}", RNS.LOG_DEBUG)
|
|
identity = RNS.Identity.recall(image_destination_hash)
|
|
destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.app_name, self.aspects)
|
|
w.notice(f"Establishing link...")
|
|
|
|
def f_established(wd):
|
|
def established(link):
|
|
RNS.log(f"Link established for image: {path}", RNS.LOG_DEBUG)
|
|
wd.notice(f"Link established")
|
|
return established
|
|
|
|
def f_closed(wd):
|
|
def closed(link):
|
|
RNS.log(f"Link closed for image: {path}", RNS.LOG_DEBUG)
|
|
image["link"] = None
|
|
image["failed"] = True
|
|
wd.notice(f"Link closed during transfer")
|
|
return closed
|
|
|
|
image["link"] = RNS.Link(destination, established_callback = f_established(w), closed_callback = f_closed(w))
|
|
timeout = time.time()+max(self.timeout, image["link"].establishment_timeout)
|
|
while image["link"].status != RNS.Link.ACTIVE and time.time() < timeout: time.sleep(0.1)
|
|
|
|
if image["link"] and image["link"].status == RNS.Link.ACTIVE and image["request_id"] == None:
|
|
RNS.log(f"Sending request for image: {path}", RNS.LOG_DEBUG)
|
|
receipt = image["link"].request("/media", data=self.__get_image_request_data(image), response_callback = self.image_received,
|
|
failed_callback = self.image_failed, progress_callback = self.image_progressed)
|
|
w.notice(f"Request sent")
|
|
|
|
if receipt: image["request_id"] = receipt.request_id
|
|
else:
|
|
RNS.log(f"Image request failed", RNS.LOG_ERROR)
|
|
image["failed"] = True
|
|
w.notice(f"Request failed")
|
|
|
|
def __get_image_request_data(self, image):
|
|
destination_hash, path = self.parse_url(image["url"])
|
|
request_data = {"path": path, "key": None}
|
|
return request_data
|
|
|
|
def image_failed(self, request_receipt):
|
|
RNS.log("Loading page image failed", RNS.LOG_ERROR)
|
|
for iid in self.page_images:
|
|
image = self.page_images[iid]
|
|
if image["request_id"] == request_receipt.request_id:
|
|
try:
|
|
image["updated"] = time.time()
|
|
image["request_id"] = None
|
|
image["failed"] = True
|
|
url = image["url"]
|
|
w = image["widget"]
|
|
w.notice(f"Could not load image {url}")
|
|
except Exception as e:
|
|
RNS.log(f"Error in image failed callback: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
def image_progressed(self, request_receipt):
|
|
for iid in self.page_images:
|
|
i = self.page_images[iid]
|
|
if i["request_id"] == request_receipt.request_id:
|
|
try:
|
|
response_progress = request_receipt.progress
|
|
response_time = request_receipt.get_response_time()
|
|
response_size = request_receipt.response_size
|
|
response_transfer_size = request_receipt.response_transfer_size
|
|
|
|
w = i["widget"]
|
|
now = time.time()
|
|
if i["progress_updated"] == None: i["progress_updated"] = now
|
|
if now > i["progress_updated"]+0.5:
|
|
td = now - i["progress_updated"]
|
|
pd = response_progress - i["previous_progress"]
|
|
bd = pd*response_transfer_size
|
|
response_speed = (bd/td)*8
|
|
i["previous_progress"] = response_progress
|
|
i["progress_updated"] = now
|
|
stats = f"{round(response_progress*100, 2)}% ({RNS.prettysize(response_progress*response_size)} of {RNS.prettysize(response_size)}) - {RNS.prettyspeed(response_speed)}"
|
|
w.notice(stats)
|
|
|
|
except Exception as e: RNS.trace_exception(e)
|
|
|
|
def image_received(self, request_receipt):
|
|
if not request_receipt.response: self.image_failed(request_receipt)
|
|
for iid in self.page_images:
|
|
image = self.page_images[iid]
|
|
if image["request_id"] == request_receipt.request_id:
|
|
try:
|
|
image["updated"] = image["update_requested"]
|
|
image["request_id"] = None
|
|
w = image["widget"]
|
|
w.notice(f"Image loaded")
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Error while loading received image: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
image["failed"] = True
|
|
|
|
try:
|
|
url = image["url"]
|
|
cachepath = self.image_cache_path(url)
|
|
if isinstance(request_receipt.response, bytes):
|
|
with open(cachepath, "wb") as f: f.write(request_receipt.response)
|
|
|
|
elif type(request_receipt.response) == io.BufferedReader:
|
|
if request_receipt.metadata != None:
|
|
file_handle = request_receipt.response
|
|
file_destination = cachepath
|
|
shutil.move(file_handle.name, file_destination)
|
|
|
|
resolved_path = self.resolve_image(url)
|
|
if resolved_path and self.image_rendering_supported:
|
|
w.load(resolved_path, remote_source=True)
|
|
self.refresh_page_layout()
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while handling image response: "+str(e), RNS.LOG_ERROR)
|
|
image["failed"] = True
|
|
|
|
# Render an arbitrary markup buffer (used by the page editor preview) and
|
|
# return the content widget, with partials/folds/fields wired as usual
|
|
def render_markup_buffer(self, markup):
|
|
self.status = Browser.DONE
|
|
self.markup = markup
|
|
self.page_background_color = None
|
|
self.page_foreground_color = None
|
|
for tag, attr in (("#!bg=", "page_background_color"), ("#!fg=", "page_foreground_color")):
|
|
pos = markup.find(tag)
|
|
if pos >= 0:
|
|
endpos = markup.find("\n", pos)
|
|
if endpos < 0: endpos = len(markup)
|
|
if endpos-(pos+len(tag)) in (3, 6):
|
|
setattr(self, attr, markup[pos+len(tag):endpos])
|
|
try:
|
|
self.attr_maps = markup_to_attrmaps(strip_modifiers(markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
except Exception as e:
|
|
self.attr_maps = [urwid.AttrMap(urwid.Text(f"Could not render preview: {e}"), "browser_inactive")]
|
|
self.update_page_display()
|
|
return self.browser_body
|
|
|
|
def init_folds(self):
|
|
self.fold_sections = {}
|
|
self.collapsed_headings = set()
|
|
attrmaps = self.attr_maps
|
|
header_levels = getattr(attrmaps, "header_levels", None) or {}
|
|
row_levels = getattr(attrmaps, "row_levels", None) or {}
|
|
collapsibles = getattr(attrmaps, "collapsibles", None) or {}
|
|
if not collapsibles:
|
|
return
|
|
|
|
n = len(attrmaps)
|
|
for idx in sorted(collapsibles.keys()):
|
|
widget = self.collapsible_widget_at(idx)
|
|
if widget is None: continue
|
|
depth = header_levels.get(idx, 1)
|
|
end = n
|
|
for i in range(idx+1, n):
|
|
if row_levels.get(i, depth) < depth or \
|
|
(i in header_levels and header_levels.get(i, 1) <= depth):
|
|
end = i; break
|
|
self.fold_sections[widget] = (idx+1, end)
|
|
if collapsibles[idx]:
|
|
self.collapsed_headings.add(widget)
|
|
try: widget.set_collapsed(True)
|
|
except Exception: pass
|
|
|
|
if self.collapsed_headings:
|
|
self.rebuild_visible()
|
|
|
|
def collapsible_widget_at(self, idx):
|
|
try:
|
|
o = self.attr_maps[idx]._original_widget
|
|
if getattr(o, "is_collapsible_heading", False): return o
|
|
except Exception: pass
|
|
return None
|
|
|
|
def hidden_indices(self):
|
|
hidden = set()
|
|
for widget in getattr(self, "collapsed_headings", ()):
|
|
rng = getattr(self, "fold_sections", {}).get(widget)
|
|
if rng: hidden.update(range(rng[0], rng[1]))
|
|
return hidden
|
|
|
|
def rebuild_visible(self):
|
|
page = getattr(self, "page_list", None)
|
|
if page is None: return
|
|
hidden = self.hidden_indices()
|
|
self.page_rows = [i for i in range(len(self.attr_maps)) if i not in hidden]
|
|
focus = page.focus_position if page.body else 0
|
|
page.body[:] = [self.attr_maps[i] for i in self.page_rows]
|
|
page.invalidate_layout()
|
|
if page.body: page.set_focus(min(focus, len(page.body)-1))
|
|
|
|
def fold_changed(self, heading_widget):
|
|
if getattr(heading_widget, "collapsed", False):
|
|
self.collapsed_headings.add(heading_widget)
|
|
else:
|
|
self.collapsed_headings.discard(heading_widget)
|
|
self.rebuild_visible()
|
|
try:
|
|
for pos, w in enumerate(self.page_list.body):
|
|
if getattr(w, "_original_widget", None) is heading_widget:
|
|
self.page_list.set_focus(pos)
|
|
break
|
|
except Exception: pass
|
|
|
|
def reveal_index(self, idx):
|
|
changed = False
|
|
for widget in list(getattr(self, "collapsed_headings", ())):
|
|
rng = getattr(self, "fold_sections", {}).get(widget)
|
|
if rng and rng[0] <= idx < rng[1]:
|
|
self.collapsed_headings.discard(widget)
|
|
try: widget.set_collapsed(False)
|
|
except Exception: pass
|
|
changed = True
|
|
if changed: self.rebuild_visible()
|
|
|
|
def parse_url(self, url):
|
|
path = None
|
|
destination_hash = None
|
|
components = url.split(":")
|
|
if len(components) == 1:
|
|
if len(components[0]) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
|
try: destination_hash = bytes.fromhex(components[0])
|
|
except Exception as e: raise ValueError("Malformed URL")
|
|
path = Browser.DEFAULT_PATH
|
|
else: raise ValueError("Malformed URL")
|
|
elif len(components) == 2:
|
|
if len(components[0]) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
|
try: destination_hash = bytes.fromhex(components[0])
|
|
except Exception as e: raise ValueError("Malformed URL")
|
|
path = components[1]
|
|
if len(path) == 0: path = Browser.DEFAULT_PATH
|
|
else:
|
|
if len(components[0]) == 0:
|
|
if self.destination_hash != None:
|
|
destination_hash = self.destination_hash
|
|
path = components[1]
|
|
if len(path) == 0: path = Browser.DEFAULT_PATH
|
|
else: raise ValueError("Malformed URL")
|
|
else: raise ValueError("Malformed URL")
|
|
else: raise ValueError("Malformed URL")
|
|
|
|
return destination_hash, path
|
|
|
|
def detect_partials(self):
|
|
for w in self.attr_maps:
|
|
o = w._original_widget
|
|
if hasattr(o, "partial_hash"):
|
|
RNS.log(f"Found partial: {o.partial_hash} / {o.partial_url} / {o.partial_refresh}")
|
|
partial = {"hash": o.partial_hash, "id": o.partial_id, "url": o.partial_url, "fields": o.partial_fields,
|
|
"refresh": o.partial_refresh, "content": None, "updated": None, "update_requested": None, "request_id": None,
|
|
"destination": None, "link": None, "pile": o, "attr_maps": None, "failed": False, "pr_throttle": 0}
|
|
|
|
self.page_partials[o.partial_hash] = partial
|
|
|
|
if len(self.page_partials) > 0: self.start_partial_updater()
|
|
|
|
def partial_failed(self, request_receipt):
|
|
RNS.log("Loading page partial failed", RNS.LOG_ERROR)
|
|
for pid in self.page_partials:
|
|
partial = self.page_partials[pid]
|
|
if partial["request_id"] == request_receipt.request_id:
|
|
try:
|
|
partial["updated"] = time.time()
|
|
partial["request_id"] = None
|
|
partial["content"] = None
|
|
partial["attr_maps"] = None
|
|
url = partial["url"]
|
|
pile = partial["pile"]
|
|
pile.contents = [(urwid.Text(f"Could not load partial {url}: The resource transfer failed"), pile.options())]
|
|
self.refresh_page_layout()
|
|
except Exception as e:
|
|
RNS.log(f"Error in partial failed callback: {e}", RNS.LOG_ERROR)
|
|
RNS.trace_exception(e)
|
|
|
|
def partial_progressed(self, request_receipt):
|
|
pass
|
|
|
|
def partial_received(self, request_receipt):
|
|
for pid in self.page_partials:
|
|
partial = self.page_partials[pid]
|
|
if partial["request_id"] == request_receipt.request_id:
|
|
try:
|
|
partial["updated"] = partial["update_requested"]
|
|
partial["request_id"] = None
|
|
partial["content"] = request_receipt.response.decode("utf-8").rstrip()
|
|
partial["attr_maps"] = markup_to_attrmaps(strip_modifiers(partial["content"]), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
pile = partial["pile"]
|
|
pile.contents = [(e, pile.options()) for e in partial["attr_maps"]]
|
|
self.refresh_page_layout()
|
|
|
|
except Exception as e:
|
|
RNS.trace_exception(e)
|
|
|
|
def __load_partial(self, partial):
|
|
if partial["failed"] == True: return
|
|
try: partial_destination_hash, path = self.parse_url(partial["url"])
|
|
except Exception as e:
|
|
RNS.log(f"Could not parse partial URL: {e}", RNS.LOG_ERROR)
|
|
partial["failed"] = True
|
|
pile = partial["pile"]
|
|
url = partial["url"]
|
|
pile.contents = [(urwid.Text(f"Could not load partial {url}: {e}"), pile.options())]
|
|
self.refresh_page_layout()
|
|
return
|
|
|
|
if partial_destination_hash != self.loopback and not RNS.Transport.has_path(partial_destination_hash):
|
|
if time.time() <= partial["pr_throttle"]: return
|
|
else:
|
|
partial["pr_throttle"] = time.time()+15
|
|
RNS.log(f"Requesting path for partial: {partial_destination_hash} / {path}", RNS.LOG_EXTREME)
|
|
RNS.Transport.request_path(partial_destination_hash)
|
|
pr_time = time.time()+self.path_timeout(partial_destination_hash)
|
|
while not RNS.Transport.has_path(partial_destination_hash):
|
|
if time.time() > pr_time: return
|
|
time.sleep(0.25)
|
|
|
|
for pid in self.page_partials:
|
|
other_partial = self.page_partials[pid]
|
|
if other_partial["link"]:
|
|
existing_link = other_partial["link"]
|
|
if existing_link.destination.hash == partial_destination_hash and existing_link.status == RNS.Link.ACTIVE:
|
|
RNS.log(f"Re-using existing link: {existing_link}", RNS.LOG_EXTREME)
|
|
partial["link"] = existing_link
|
|
break
|
|
|
|
if not partial["link"] or partial["link"].status == RNS.Link.CLOSED:
|
|
RNS.log(f"Establishing link for partial: {partial_destination_hash} / {path}", RNS.LOG_EXTREME)
|
|
identity = RNS.Identity.recall(partial_destination_hash)
|
|
destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.app_name, self.aspects)
|
|
|
|
def established(link):
|
|
RNS.log(f"Link established for partial: {partial_destination_hash} / {path}", RNS.LOG_EXTREME)
|
|
|
|
def closed(link):
|
|
RNS.log(f"Link closed for partial: {partial_destination_hash} / {path}", RNS.LOG_EXTREME)
|
|
partial["link"] = None
|
|
|
|
partial["link"] = RNS.Link(destination, established_callback = established, closed_callback = closed)
|
|
timeout = time.time()+max(self.timeout, partial["link"].establishment_timeout)
|
|
while partial["link"].status != RNS.Link.ACTIVE and time.time() < timeout: time.sleep(0.1)
|
|
|
|
if partial["link"] and partial["link"].status == RNS.Link.ACTIVE and partial["request_id"] == None:
|
|
RNS.log(f"Sending request for partial: {partial_destination_hash} / {path}", RNS.LOG_EXTREME)
|
|
receipt = partial["link"].request(path, data=self.__get_partial_request_data(partial), response_callback = self.partial_received,
|
|
failed_callback = self.partial_failed, progress_callback = self.partial_progressed)
|
|
|
|
if receipt: partial["request_id"] = receipt.request_id
|
|
else: RNS.log(f"Partial request failed", RNS.LOG_ERROR)
|
|
|
|
def __get_partial_request_data(self, partial):
|
|
request_data = None
|
|
if partial["fields"] != None:
|
|
link_data = partial["fields"]
|
|
link_fields = []
|
|
request_data = {}
|
|
all_fields = True if "*" in link_data else False
|
|
|
|
for e in link_data:
|
|
if "=" in e:
|
|
c = e.split("=")
|
|
if len(c) == 2:
|
|
request_data["var_"+str(c[0])] = str(c[1])
|
|
else:
|
|
link_fields.append(e)
|
|
|
|
def recurse_down(w):
|
|
if isinstance(w, list):
|
|
for t in w:
|
|
recurse_down(t)
|
|
elif isinstance(w, tuple):
|
|
for t in w:
|
|
recurse_down(t)
|
|
elif hasattr(w, "contents"):
|
|
recurse_down(w.contents)
|
|
elif hasattr(w, "original_widget"):
|
|
recurse_down(w.original_widget)
|
|
elif hasattr(w, "_original_widget"):
|
|
recurse_down(w._original_widget)
|
|
else:
|
|
if hasattr(w, "field_name") and (all_fields or w.field_name in link_fields):
|
|
field_key = "field_" + w.field_name
|
|
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)
|
|
RNS.log("Including request data: "+str(request_data), RNS.LOG_EXTREME)
|
|
|
|
return request_data
|
|
|
|
def start_partial_updater(self):
|
|
if not self.updater_running: self.update_partials()
|
|
|
|
def handle_partial_updates(self, partial_ids):
|
|
RNS.log(f"Update partials: {partial_ids}")
|
|
def job():
|
|
for pid in self.page_partials:
|
|
try:
|
|
partial = self.page_partials[pid]
|
|
if partial["id"] in partial_ids:
|
|
partial["update_requested"] = time.time()
|
|
self.__load_partial(partial)
|
|
except Exception as e: RNS.log(f"Error updating page partial: {e}", RNS.LOG_ERROR)
|
|
|
|
threading.Thread(target=job, daemon=True).start()
|
|
|
|
def update_partials(self, loop=None, user_data=None):
|
|
with self.partial_updater_lock:
|
|
def job():
|
|
for pid in self.page_partials:
|
|
try:
|
|
partial = self.page_partials[pid]
|
|
if partial["failed"]: continue
|
|
if not partial["updated"] or (partial["refresh"] != None and time.time() > partial["updated"]+partial["refresh"]):
|
|
partial["update_requested"] = time.time()
|
|
self.__load_partial(partial)
|
|
except Exception as e: RNS.log(f"Error updating page partial: {e}", RNS.LOG_ERROR)
|
|
|
|
threading.Thread(target=job, daemon=True).start()
|
|
|
|
if len(self.page_partials) > 0:
|
|
self.updater_running = True
|
|
self.app.ui.loop.set_alarm_in(1, self.update_partials)
|
|
else:
|
|
self.updater_running = False
|
|
|
|
def identify(self):
|
|
if self.link != None:
|
|
if self.link.status == RNS.Link.ACTIVE:
|
|
self.link.identify(self.auth_identity)
|
|
|
|
|
|
def disconnect(self):
|
|
if self.link != None:
|
|
self.link.teardown()
|
|
|
|
self.request_data = None
|
|
self.attr_maps = []
|
|
self.status = Browser.DISCONECTED
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
|
|
self.history = []
|
|
self.history_ptr = 0
|
|
self.history_inc = False
|
|
self.history_dec = False
|
|
|
|
self.update_display()
|
|
|
|
|
|
def retrieve_url(self, url, request_data = None):
|
|
components = url.split("`")
|
|
if len(components) == 2:
|
|
url = components[0]
|
|
try:
|
|
link_fields_str = components[1]
|
|
if link_fields_str != "":
|
|
if not request_data: request_data = {}
|
|
for e in link_fields_str.split("|"):
|
|
if "=" in e:
|
|
c = e.split("=")
|
|
if len(c) == 2:
|
|
request_data["var_"+str(c[0])] = str(c[1])
|
|
|
|
except Exception as e:
|
|
RNS.log(f"Malformed URL: {e}", RNS.LOG_WARNING)
|
|
raise ValueError(f"Malformed URL: {e}")
|
|
|
|
self.previous_destination_hash = self.destination_hash
|
|
self.previous_path = self.path
|
|
|
|
destination_hash = None
|
|
path = None
|
|
|
|
components = url.split(":")
|
|
if len(components) == 1:
|
|
if len(components[0]) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
|
try: destination_hash = bytes.fromhex(components[0])
|
|
except Exception as e: raise ValueError("Malformed URL")
|
|
path = Browser.DEFAULT_PATH
|
|
else: raise ValueError("Malformed URL")
|
|
elif len(components) == 2:
|
|
if len(components[0]) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
|
try: destination_hash = bytes.fromhex(components[0])
|
|
except Exception as e: raise ValueError("Malformed URL")
|
|
path = components[1]
|
|
if len(path) == 0: path = Browser.DEFAULT_PATH
|
|
else:
|
|
if len(components[0]) == 0:
|
|
if self.destination_hash != None:
|
|
destination_hash = self.destination_hash
|
|
path = components[1]
|
|
if len(path) == 0: path = Browser.DEFAULT_PATH
|
|
else: raise ValueError("Malformed URL")
|
|
else: raise ValueError("Malformed URL")
|
|
else: raise ValueError("Malformed URL")
|
|
|
|
if destination_hash != None and path != None:
|
|
if path.startswith("/file/"):
|
|
if destination_hash != self.loopback:
|
|
if destination_hash == self.destination_hash:
|
|
self.download_file(destination_hash, path, data=request_data)
|
|
else:
|
|
RNS.log("Cannot request file download from a node that is not currently connected.", RNS.LOG_ERROR)
|
|
RNS.log("The requested URL was: "+str(url), RNS.LOG_ERROR)
|
|
else:
|
|
self.download_local_file(path)
|
|
else:
|
|
self.set_destination_hash(destination_hash)
|
|
self.set_path(path)
|
|
self.set_request_data(request_data)
|
|
self.load_page()
|
|
|
|
def set_destination_hash(self, destination_hash):
|
|
if len(destination_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
|
|
self.destination_hash = destination_hash
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def set_path(self, path):
|
|
self.path = path
|
|
|
|
def set_request_data(self, request_data):
|
|
self.request_data = request_data
|
|
|
|
def set_timeout(self, timeout):
|
|
self.timeout = timeout
|
|
|
|
def path_timeout(self, destination_hash):
|
|
return max(self.timeout + self.app.rns.get_first_hop_timeout(destination_hash), self.app.rns.get_medium_path_timeout())
|
|
|
|
def download_local_file(self, path):
|
|
try:
|
|
file_path = self.app.filespath+path.replace("/file", "", 1)
|
|
if os.path.isfile(file_path):
|
|
file_name = os.path.basename(file_path)
|
|
file_destination = self.app.downloads_path+"/"+file_name
|
|
|
|
counter = 0
|
|
while os.path.isfile(file_destination):
|
|
counter += 1
|
|
file_destination = self.app.downloads_path+"/"+file_name+"."+str(counter)
|
|
|
|
fs = open(file_path, "rb")
|
|
fd = open(file_destination, "wb")
|
|
fd.write(fs.read())
|
|
fd.close()
|
|
fs.close()
|
|
|
|
self.saved_file_name = file_destination.replace(self.app.downloads_path+"/", "", 1)
|
|
try: self.saved_file_size = os.path.getsize(file_destination)
|
|
except: self.saved_file_size = 0
|
|
|
|
self.update_display()
|
|
else:
|
|
RNS.log("The requested local download file does not exist: "+str(file_path), RNS.LOG_ERROR)
|
|
|
|
self.status = Browser.DONE
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
|
|
except Exception as e:
|
|
RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
|
|
def download_file(self, destination_hash, path, data=None):
|
|
if self.link == None or self.link.destination.hash != self.destination_hash:
|
|
if not RNS.Transport.has_path(self.destination_hash):
|
|
self.status = Browser.NO_PATH
|
|
self.update_display()
|
|
|
|
RNS.Transport.request_path(self.destination_hash)
|
|
self.status = Browser.PATH_REQUESTED
|
|
self.update_display()
|
|
|
|
pr_time = time.time()+self.path_timeout(self.destination_hash)
|
|
while not RNS.Transport.has_path(self.destination_hash):
|
|
if time.time() > pr_time:
|
|
self.request_timeout()
|
|
return
|
|
|
|
time.sleep(0.25)
|
|
|
|
self.status = Browser.ESTABLISHING_LINK
|
|
self.update_display()
|
|
|
|
identity = RNS.Identity.recall(self.destination_hash)
|
|
destination = RNS.Destination(
|
|
identity,
|
|
RNS.Destination.OUT,
|
|
RNS.Destination.SINGLE,
|
|
self.app_name,
|
|
self.aspects
|
|
)
|
|
|
|
self.link = RNS.Link(destination, established_callback = self.link_established, closed_callback = self.link_closed)
|
|
|
|
while self.status == Browser.ESTABLISHING_LINK:
|
|
time.sleep(0.1)
|
|
|
|
if self.status != Browser.LINK_ESTABLISHED:
|
|
return
|
|
|
|
self.update_display()
|
|
|
|
if self.link != None and self.link.destination.hash == self.destination_hash:
|
|
# Send the request
|
|
self.status = Browser.REQUESTING
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
self.saved_file_name = None
|
|
self.saved_file_size = 0
|
|
|
|
self.update_display()
|
|
receipt = self.link.request(
|
|
path,
|
|
data = data,
|
|
response_callback = self.file_received,
|
|
failed_callback = self.request_failed,
|
|
progress_callback = self.response_progressed
|
|
)
|
|
|
|
if receipt:
|
|
self.last_request_receipt = receipt
|
|
self.last_request_id = receipt.request_id
|
|
self.status = Browser.REQUEST_SENT
|
|
self.update_display()
|
|
else:
|
|
self.link.teardown()
|
|
|
|
def write_history(self):
|
|
entry = [self.destination_hash, self.path, self.request_data]
|
|
self.history.insert(self.history_ptr, entry)
|
|
self.history_ptr += 1
|
|
|
|
if len(self.history) > self.history_ptr:
|
|
self.history = self.history[:self.history_ptr]
|
|
|
|
def back(self):
|
|
target_ptr = self.history_ptr-1
|
|
if not self.history_inc and not self.history_dec:
|
|
if target_ptr > 0:
|
|
self.history_dec = True
|
|
entry = self.history[target_ptr-1]
|
|
url = RNS.hexrep(entry[0], delimit=False)+":"+entry[1]
|
|
self.history_ptr = target_ptr
|
|
self.retrieve_url(url, request_data=entry[2])
|
|
|
|
def forward(self):
|
|
target_ptr = self.history_ptr+1
|
|
if not self.history_inc and not self.history_dec:
|
|
if target_ptr <= len(self.history):
|
|
self.history_dec = True
|
|
entry = self.history[target_ptr-1]
|
|
url = RNS.hexrep(entry[0], delimit=False)+":"+entry[1]
|
|
self.history_ptr = target_ptr
|
|
self.retrieve_url(url, request_data=entry[2])
|
|
|
|
def reload(self):
|
|
if not self.reloading and self.status == Browser.DONE:
|
|
self.reloading = True
|
|
self.uncache_page(self.current_url())
|
|
self.load_page()
|
|
|
|
def copy_url(self):
|
|
url = self.current_url()
|
|
if not url:
|
|
return
|
|
|
|
if not osc52_copy(url):
|
|
return
|
|
|
|
try:
|
|
notice = urwid.AttrMap(urwid.Pile([urwid.Divider(self.g["divider1"]), urwid.Text("Copied URL to clipboard")]), "browser_controls")
|
|
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))
|
|
notice.set_attr_map({None: style_name})
|
|
self.browser_footer = notice
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
def restore_footer(loop, user_data):
|
|
if self.status == Browser.DONE:
|
|
self.browser_footer = self.make_status_widget()
|
|
else:
|
|
self.browser_footer = urwid.Text("")
|
|
if self.frame is not None:
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
self.app.ui.loop.set_alarm_in(2.0, restore_footer)
|
|
except Exception as e:
|
|
RNS.log("Could not update footer after URL copy: "+str(e), RNS.LOG_ERROR)
|
|
|
|
def close_dialogs(self):
|
|
options = self.delegate.columns.options(urwid.WEIGHT, self.delegate.right_area_width)
|
|
self.delegate.columns.contents[1] = (self.display_widget, options)
|
|
|
|
def url_dialog(self):
|
|
e_url = UrlEdit(caption="URL : ", edit_text=self.current_url())
|
|
|
|
def dismiss_dialog(sender):
|
|
self.close_dialogs()
|
|
|
|
def confirmed(sender):
|
|
try:
|
|
entered_url = e_url.get_edit_text().strip()
|
|
if not "`" in entered_url and ":" in entered_url:
|
|
pos = entered_url.find("|")
|
|
if pos > 0: entered_url = entered_url[:pos]+"`"+entered_url[pos+1:]
|
|
self.retrieve_url(entered_url)
|
|
except Exception as e:
|
|
self.browser_footer = urwid.Text("Could not open link: "+str(e))
|
|
self.frame.contents["footer"] = (self.browser_footer, self.frame.options())
|
|
|
|
self.close_dialogs()
|
|
|
|
dialog = UrlDialogLineBox(
|
|
urwid.Pile([
|
|
e_url,
|
|
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"
|
|
)
|
|
e_url.confirmed = confirmed
|
|
dialog.confirmed = confirmed
|
|
dialog.delegate = self
|
|
bottom = self.display_widget
|
|
|
|
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(urwid.WEIGHT, self.delegate.right_area_width)
|
|
self.delegate.columns.contents[1] = (overlay, options)
|
|
self.delegate.columns.focus_position = 1
|
|
|
|
def save_node_dialog(self):
|
|
if self.destination_hash != None:
|
|
try:
|
|
def dismiss_dialog(sender):
|
|
self.close_dialogs()
|
|
|
|
display_name = RNS.Identity.recall_app_data(self.destination_hash)
|
|
disp_str = ""
|
|
if display_name != None:
|
|
display_name = display_name.decode("utf-8")
|
|
disp_str = " \""+display_name+"\""
|
|
|
|
def confirmed(sender):
|
|
node_entry = DirectoryEntry(self.destination_hash, display_name=display_name, hosts_node=True)
|
|
self.app.directory.remember(node_entry)
|
|
self.app.ui.main_display.sub_displays.network_display.directory_change_callback()
|
|
|
|
self.close_dialogs()
|
|
|
|
dialog = UrlDialogLineBox(
|
|
urwid.Pile([
|
|
urwid.Text("Save connected node"+disp_str+" "+RNS.prettyhexrep(self.destination_hash)+" to Known Nodes?\n"),
|
|
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("Save", on_press=confirmed)),
|
|
])
|
|
]), title="Save Node"
|
|
)
|
|
|
|
dialog.confirmed = confirmed
|
|
dialog.delegate = self
|
|
bottom = self.display_widget
|
|
|
|
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(urwid.WEIGHT, self.delegate.right_area_width)
|
|
self.delegate.columns.contents[1] = (overlay, options)
|
|
self.delegate.columns.focus_position = 1
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
def load_page(self):
|
|
if self.request_data == None:
|
|
cached = self.get_cached(self.current_url())
|
|
else:
|
|
cached = None
|
|
|
|
if cached:
|
|
self.status = Browser.DONE
|
|
self.page_data = cached
|
|
self.markup = self.page_data.decode("utf-8")
|
|
|
|
self.page_background_color = None
|
|
bgpos = self.markup.find("#!bg=")
|
|
if bgpos >= 0:
|
|
endpos = self.markup.find("\n", bgpos)
|
|
if endpos-(bgpos+5) == 3:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
elif endpos-(bgpos+5) == 6:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
|
|
self.page_foreground_color = None
|
|
fgpos = self.markup.find("#!fg=")
|
|
if fgpos >= 0:
|
|
endpos = self.markup.find("\n", fgpos)
|
|
if endpos-(fgpos+5) == 3:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
elif endpos-(fgpos+5) == 6:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
|
|
try: self.attr_maps = markup_to_attrmaps(strip_modifiers(self.markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
except Exception as e: self.attr_maps = [urwid.AttrMap(urwid.Text(f"Could not render page: {e}"), "browser_inactive")]
|
|
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
self.saved_file_name = None
|
|
self.saved_file_size = 0
|
|
self.loaded_from_cache = True
|
|
|
|
self.update_display()
|
|
|
|
if not self.history_inc and not self.history_dec and not self.reloading:
|
|
self.write_history()
|
|
else:
|
|
self.history_dec = False
|
|
self.history_inc = False
|
|
self.reloading = False
|
|
|
|
if self.request_data and "var_anchor" in self.request_data:
|
|
def df(loop, user_data): self._jump_to_anchor(self.request_data["var_anchor"])
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.0, df)
|
|
|
|
else:
|
|
if self.destination_hash != self.loopback:
|
|
load_thread = threading.Thread(target=self.__load)
|
|
load_thread.setDaemon(True)
|
|
load_thread.start()
|
|
else:
|
|
RNS.log("Browser handling local page: "+str(self.path), RNS.LOG_DEBUG)
|
|
page_path = self.app.pagespath+self.path.replace("/page", "", 1)
|
|
|
|
page_data = b"The requested local page did not exist in the file system"
|
|
if os.path.isfile(page_path):
|
|
if os.access(page_path, os.X_OK):
|
|
if self.request_data != None:
|
|
env_map = self.request_data
|
|
else:
|
|
env_map = {}
|
|
|
|
if "PATH" in os.environ:
|
|
env_map["PATH"] = os.environ["PATH"]
|
|
|
|
generated = subprocess.run([page_path], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env_map)
|
|
page_data = generated.stdout
|
|
else:
|
|
file = open(page_path, "rb")
|
|
page_data = file.read()
|
|
file.close()
|
|
|
|
self.status = Browser.DONE
|
|
self.page_data = page_data
|
|
self.markup = self.page_data.decode("utf-8")
|
|
|
|
self.page_background_color = None
|
|
bgpos = self.markup.find("#!bg=")
|
|
if bgpos >= 0:
|
|
endpos = self.markup.find("\n", bgpos)
|
|
if endpos-(bgpos+5) == 3:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
elif endpos-(bgpos+5) == 6:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
|
|
self.page_foreground_color = None
|
|
fgpos = self.markup.find("#!fg=")
|
|
if fgpos >= 0:
|
|
endpos = self.markup.find("\n", fgpos)
|
|
if endpos-(fgpos+5) == 3:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
elif endpos-(fgpos+5) == 6:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
|
|
try: self.attr_maps = markup_to_attrmaps(strip_modifiers(self.markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
except Exception as e: self.attr_maps = [urwid.AttrMap(urwid.Text(f"Could not render page: {e}"), "browser_inactive")]
|
|
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
self.saved_file_name = None
|
|
self.saved_file_size = 0
|
|
self.loaded_from_cache = False
|
|
|
|
self.update_display()
|
|
|
|
if not self.history_inc and not self.history_dec and not self.reloading:
|
|
self.write_history()
|
|
else:
|
|
self.history_dec = False
|
|
self.history_inc = False
|
|
self.reloading = False
|
|
|
|
if self.request_data and "var_anchor" in self.request_data:
|
|
def df(loop, user_data): self._jump_to_anchor(self.request_data["var_anchor"])
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.0, df)
|
|
|
|
|
|
def __load(self):
|
|
# If an established link exists, but it doesn't match the target
|
|
# destination, we close and clear it.
|
|
if self.link != None and (self.link.destination.hash != self.destination_hash or self.link.status != RNS.Link.ACTIVE):
|
|
self.link.teardown()
|
|
self.link = None
|
|
|
|
# If no link to the destination exists, we create one.
|
|
if self.link == None:
|
|
if not RNS.Transport.has_path(self.destination_hash):
|
|
self.status = Browser.NO_PATH
|
|
self.update_display()
|
|
|
|
RNS.Transport.request_path(self.destination_hash)
|
|
self.status = Browser.PATH_REQUESTED
|
|
self.update_display()
|
|
|
|
pr_time = time.time()+self.path_timeout(self.destination_hash)
|
|
while not RNS.Transport.has_path(self.destination_hash):
|
|
if time.time() > pr_time:
|
|
self.request_timeout()
|
|
return
|
|
|
|
time.sleep(0.25)
|
|
|
|
self.status = Browser.ESTABLISHING_LINK
|
|
self.update_display()
|
|
|
|
identity = RNS.Identity.recall(self.destination_hash)
|
|
destination = RNS.Destination(
|
|
identity,
|
|
RNS.Destination.OUT,
|
|
RNS.Destination.SINGLE,
|
|
self.app_name,
|
|
self.aspects
|
|
)
|
|
|
|
self.link = RNS.Link(destination, established_callback = self.link_established, closed_callback = self.link_closed)
|
|
|
|
while self.status == Browser.ESTABLISHING_LINK:
|
|
time.sleep(0.1)
|
|
|
|
if self.status != Browser.LINK_ESTABLISHED:
|
|
return
|
|
|
|
self.update_display()
|
|
|
|
# Send the request
|
|
self.status = Browser.REQUESTING
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
self.saved_file_name = None
|
|
self.saved_file_size = 0
|
|
|
|
|
|
self.update_display()
|
|
receipt = self.link.request(
|
|
self.path,
|
|
data = self.request_data,
|
|
response_callback = self.response_received,
|
|
failed_callback = self.request_failed,
|
|
progress_callback = self.response_progressed
|
|
)
|
|
|
|
if receipt:
|
|
self.last_request_receipt = receipt
|
|
self.last_request_id = receipt.request_id
|
|
self.status = Browser.REQUEST_SENT
|
|
self.update_display()
|
|
else:
|
|
self.link.teardown()
|
|
|
|
|
|
|
|
def link_established(self, link):
|
|
self.status = Browser.LINK_ESTABLISHED
|
|
|
|
if self.app.directory.should_identify_on_connect(self.destination_hash):
|
|
RNS.log("Link established, identifying to remote system...", RNS.LOG_VERBOSE)
|
|
self.link.identify(self.app.identity)
|
|
|
|
|
|
def link_closed(self, link):
|
|
if self.status == Browser.DISCONECTED or self.status == Browser.DONE:
|
|
self.link = None
|
|
elif self.status == Browser.ESTABLISHING_LINK:
|
|
self.link_establishment_timeout()
|
|
else:
|
|
self.link = None
|
|
self.status = Browser.REQUEST_FAILED
|
|
self.update_display()
|
|
|
|
def link_establishment_timeout(self):
|
|
self.status = Browser.LINK_TIMEOUT
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
self.link = None
|
|
|
|
self.update_display()
|
|
|
|
|
|
def response_received(self, request_receipt):
|
|
try:
|
|
self.status = Browser.DONE
|
|
self.page_data = request_receipt.response
|
|
self.markup = self.page_data.decode("utf-8")
|
|
|
|
self.page_background_color = None
|
|
bgpos = self.markup.find("#!bg=")
|
|
if bgpos >= 0:
|
|
endpos = self.markup.find("\n", bgpos)
|
|
if endpos-(bgpos+5) == 3:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
elif endpos-(bgpos+5) == 6:
|
|
bg = self.markup[bgpos+5:endpos]
|
|
self.page_background_color = bg
|
|
|
|
self.page_foreground_color = None
|
|
fgpos = self.markup.find("#!fg=")
|
|
if fgpos >= 0:
|
|
endpos = self.markup.find("\n", fgpos)
|
|
if endpos-(fgpos+5) == 3:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
elif endpos-(fgpos+5) == 6:
|
|
fg = self.markup[fgpos+5:endpos]
|
|
self.page_foreground_color = fg
|
|
|
|
try: self.attr_maps = markup_to_attrmaps(strip_modifiers(self.markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
except Exception as e: self.attr_maps = [urwid.AttrMap(urwid.Text(f"Could not render page: {e}"), "browser_inactive")]
|
|
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.loaded_from_cache = False
|
|
|
|
# Simple header handling. Should be expanded when more
|
|
# header tags are added.
|
|
cache_time = Browser.DEFAULT_CACHE_TIME
|
|
if self.markup[:4] == "#!c=":
|
|
try:
|
|
endpos = self.markup.find("\n")
|
|
if endpos == -1:
|
|
endpos = len(self.markup)
|
|
cache_time = int(self.markup[4:endpos])
|
|
except Exception as e: RNS.log(f"Invalid page cache time header: {e}", RNS.LOG_DEBUG)
|
|
|
|
self.update_display()
|
|
|
|
if not self.history_inc and not self.history_dec and not self.reloading:
|
|
self.write_history()
|
|
else:
|
|
self.history_dec = False
|
|
self.history_inc = False
|
|
self.reloading = False
|
|
|
|
if cache_time == 0:
|
|
RNS.log("Received page "+str(self.current_url())+", not caching due to header.", RNS.LOG_DEBUG)
|
|
else:
|
|
RNS.log("Received page "+str(self.current_url())+", caching for %.3f hours." % (cache_time/60/60), RNS.LOG_DEBUG)
|
|
self.cache_page(cache_time)
|
|
|
|
if self.request_data and "var_anchor" in self.request_data:
|
|
def df(loop, user_data): self._jump_to_anchor(self.request_data["var_anchor"])
|
|
nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.0, df)
|
|
|
|
except Exception as e:
|
|
RNS.log("An error occurred while handling response. The contained exception was: "+str(e))
|
|
|
|
def uncache_page(self, url):
|
|
url_hash = self.url_hash(url)
|
|
files = os.listdir(self.app.cachepath)
|
|
for file in files:
|
|
if file.startswith(url_hash):
|
|
cachefile = self.app.cachepath+"/"+file
|
|
os.unlink(cachefile)
|
|
RNS.log("Removed "+str(cachefile)+" from cache.", RNS.LOG_DEBUG)
|
|
|
|
def get_cached(self, url):
|
|
url_hash = self.url_hash(url)
|
|
files = os.listdir(self.app.cachepath)
|
|
for file in files:
|
|
cachepath = self.app.cachepath+"/"+file
|
|
try:
|
|
components = file.split("_")
|
|
if len(components) == 2 and len(components[0]) == 64 and len(components[1]) > 0:
|
|
expires = float(components[1])
|
|
|
|
if time.time() > expires:
|
|
RNS.log("Removing stale cache entry "+str(file), RNS.LOG_DEBUG)
|
|
os.unlink(cachepath)
|
|
else:
|
|
if file.startswith(url_hash):
|
|
RNS.log("Found "+str(file)+" in cache.", RNS.LOG_DEBUG)
|
|
RNS.log("Returning cached page", RNS.LOG_DEBUG)
|
|
file = open(cachepath, "rb")
|
|
data = file.read()
|
|
file.close()
|
|
return data
|
|
|
|
except Exception as e:
|
|
RNS.log("Error while parsing cache entry "+str(cachepath)+", removing it.", RNS.LOG_ERROR)
|
|
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
try:
|
|
os.unlink(cachepath)
|
|
except Exception as e:
|
|
RNS.log("Additionally, an exception occurred while unlinking the entry: "+str(e), RNS.LOG_ERROR)
|
|
RNS.log("You will probably need to remove this entry manually by deleting the file: "+str(cachepath), RNS.LOG_ERROR)
|
|
|
|
|
|
return None
|
|
|
|
def clean_cache(self):
|
|
files = os.listdir(self.app.cachepath)
|
|
for file in files:
|
|
cachepath = self.app.cachepath+"/"+file
|
|
try:
|
|
components = file.split("_")
|
|
if len(components) == 2 and len(components[0]) == 64 and len(components[1]) > 0:
|
|
expires = float(components[1])
|
|
|
|
if time.time() > expires:
|
|
RNS.log("Removing stale cache entry "+str(file), RNS.LOG_DEBUG)
|
|
os.unlink(cachepath)
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
def cache_page(self, cache_time):
|
|
url_hash = self.url_hash(self.current_url())
|
|
if url_hash == None:
|
|
RNS.log("Could not cache page "+str(self.current_url()), RNS.LOG_ERROR)
|
|
else:
|
|
try:
|
|
self.uncache_page(self.current_url())
|
|
cache_expires = time.time()+cache_time
|
|
filename = url_hash+"_"+str(cache_expires)
|
|
cachefile = self.app.cachepath+"/"+filename
|
|
file = open(cachefile, "wb")
|
|
file.write(self.page_data)
|
|
file.close()
|
|
RNS.log("Cached page "+str(self.current_url())+" to "+str(cachefile), RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log("Could not write cache file for page "+str(self.current_url()), RNS.LOG_ERROR)
|
|
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
|
|
|
|
def file_received(self, request_receipt):
|
|
try:
|
|
if isinstance(request_receipt.response, bytes):
|
|
markup = request_receipt.response.decode("utf-8")
|
|
self.attr_maps = markup_to_attrmaps(strip_modifiers(markup), url_delegate=self, fg_color=self.page_foreground_color, bg_color=self.page_background_color)
|
|
|
|
elif type(request_receipt.response) == io.BufferedReader:
|
|
if request_receipt.metadata != None:
|
|
file_name = ConversationMessage.safe_attachment_name(request_receipt.metadata["name"])
|
|
file_handle = request_receipt.response
|
|
file_destination = self.app.downloads_path+"/"+file_name
|
|
|
|
counter = 0
|
|
while os.path.isfile(file_destination):
|
|
counter += 1
|
|
file_destination = self.app.downloads_path+"/"+file_name+"."+str(counter)
|
|
|
|
shutil.move(file_handle.name, file_destination)
|
|
|
|
self.saved_file_name = file_destination.replace(self.app.downloads_path+"/", "", 1)
|
|
try: self.saved_file_size = os.path.getsize(file_destination)
|
|
except: self.saved_file_size = 0
|
|
self.file_saved_at = time.time()
|
|
|
|
else:
|
|
file_name = request_receipt.response[0]
|
|
file_data = request_receipt.response[1]
|
|
file_destination_name = ConversationMessage.safe_attachment_name(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)
|
|
try: self.saved_file_size = os.path.getsize(file_destination)
|
|
except: self.saved_file_size = 0
|
|
|
|
self.status = Browser.DONE
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
|
|
self.update_display()
|
|
|
|
except Exception as e:
|
|
RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
|
|
|
|
|
def request_failed(self, request_receipt=None):
|
|
if request_receipt != None:
|
|
if request_receipt.request_id == self.last_request_id:
|
|
self.status = Browser.REQUEST_FAILED
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
|
|
self.update_display()
|
|
if self.link != None:
|
|
try: self.link.teardown()
|
|
except Exception as e: pass
|
|
else:
|
|
self.status = Browser.REQUEST_FAILED
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
|
|
self.update_display()
|
|
if self.link != None:
|
|
try:
|
|
self.link.teardown()
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
def request_timeout(self, request_receipt=None):
|
|
self.status = Browser.REQUEST_TIMEOUT
|
|
self.response_progress = 0
|
|
self.response_speed = None
|
|
self.progress_updated_at = None
|
|
self.previous_progress = 0
|
|
self.response_size = None
|
|
self.response_transfer_size = None
|
|
|
|
self.update_display()
|
|
if self.link != None:
|
|
try:
|
|
self.link.teardown()
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
def response_progressed(self, request_receipt):
|
|
self.response_progress = request_receipt.progress
|
|
self.response_time = request_receipt.get_response_time()
|
|
self.response_size = request_receipt.response_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+0.5:
|
|
td = now - self.progress_updated_at
|
|
pd = self.response_progress - self.previous_progress
|
|
bd = pd*self.response_transfer_size
|
|
self.response_speed = (bd/td)*8
|
|
self.previous_progress = self.response_progress
|
|
self.progress_updated_at = now
|
|
|
|
self.update_display()
|
|
|
|
def last_response_speed(self):
|
|
if self.response_transfer_size and self.response_time:
|
|
return (self.response_transfer_size/max(self.response_time, 0.0001))*8
|
|
else: return None
|
|
|
|
def status_text(self):
|
|
if self.status == Browser.DONE and self.response_transfer_size != None:
|
|
if self.response_time != None: response_time_str = "{:.2f}".format(self.response_time)
|
|
else: response_time_str = "None"
|
|
|
|
if self.saved_file_name == None: resp_size = self.response_size
|
|
else: resp_size = self.saved_file_size
|
|
|
|
if resp_size: stats_string = " "+self.g["page"]+size_str(resp_size)
|
|
else: stats_string = " "
|
|
if self.response_transfer_size:
|
|
stats_string += " "+self.g["arrow_d"]+size_str(self.response_transfer_size)+" in "+response_time_str
|
|
if self.response_transfer_size and self.response_time:
|
|
stats_string += "s "+self.g["speed"]+size_str(self.response_transfer_size/self.response_time, suffix="b")+"/s"
|
|
|
|
elif self.loaded_from_cache:
|
|
stats_string = " (cached)"
|
|
else:
|
|
stats_string = ""
|
|
|
|
if self.status == Browser.NO_PATH:
|
|
return "No path to destination known"
|
|
elif self.status == Browser.PATH_REQUESTED:
|
|
return "Path requested, waiting for path..."
|
|
elif self.status == Browser.ESTABLISHING_LINK:
|
|
return "Establishing link..."
|
|
elif self.status == Browser.LINK_TIMEOUT:
|
|
return "Link establishment timed out"
|
|
elif self.status == Browser.LINK_ESTABLISHED:
|
|
return "Link established"
|
|
elif self.status == Browser.REQUESTING:
|
|
return "Sending request..."
|
|
elif self.status == Browser.REQUEST_SENT:
|
|
return "Request sent, awaiting response..."
|
|
elif self.status == Browser.REQUEST_FAILED:
|
|
return "Request failed"
|
|
elif self.status == Browser.REQUEST_TIMEOUT:
|
|
return "Request timed out"
|
|
elif self.status == Browser.RECEIVING_RESPONSE:
|
|
return "Receiving response..."
|
|
elif self.status == Browser.DONE:
|
|
if self.saved_file_name == None: return "Done"+stats_string
|
|
else: return "Saved "+str(self.saved_file_name)+stats_string
|
|
elif self.status == Browser.DISCONECTED:
|
|
return "Disconnected"
|
|
else:
|
|
return "Browser Status Unknown"
|
|
|
|
|
|
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):
|
|
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-
|
|
# readable file size
|
|
def size_str(num, suffix='B'):
|
|
units = ['','K','M','G','T','P','E','Z']
|
|
last_unit = 'Y'
|
|
|
|
if suffix == 'b':
|
|
num *= 8
|
|
units = ['','K','M','G','T','P','E','Z']
|
|
last_unit = 'Y'
|
|
|
|
for unit in units:
|
|
if abs(num) < 1000.0:
|
|
if unit == "":
|
|
return "%.0f%s%s" % (num, unit, suffix)
|
|
else:
|
|
return "%.2f%s%s" % (num, unit, suffix)
|
|
num /= 1000.0
|
|
|
|
return "%.2f%s%s" % (num, last_unit, suffix)
|
|
|
|
class UrlDialogLineBox(urwid.LineBox):
|
|
def keypress(self, size, key):
|
|
if key == "esc":
|
|
self.delegate.close_dialogs()
|
|
else:
|
|
return super(UrlDialogLineBox, self).keypress(size, key)
|
|
|
|
class UrlEdit(ReadlineMixin, urwid.Edit):
|
|
def keypress(self, size, key):
|
|
if key == "enter":
|
|
self.confirmed(self)
|
|
else:
|
|
return super(UrlEdit, self).keypress(size, key)
|