Refactor app initialization and enhance logging; add Reticulum logs and improve error handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
import pathlib
|
||||
|
||||
import RNS
|
||||
|
||||
@@ -17,22 +18,20 @@ class AnnounceService:
|
||||
"""
|
||||
|
||||
def __init__(self, update_callback):
|
||||
# Accept all announce aspects
|
||||
self.aspect_filter = "nomadnetwork.node"
|
||||
self.receive_path_responses = True
|
||||
self.announces: list[Announce] = []
|
||||
self.update_callback = update_callback
|
||||
# Initialize Reticulum transport once
|
||||
config_dir = pathlib.Path(__file__).resolve().parents[2] / "config"
|
||||
try:
|
||||
RNS.Reticulum()
|
||||
except OSError:
|
||||
# Already initialized
|
||||
RNS.Reticulum(str(config_dir))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
# Register self as announce handler
|
||||
RNS.Transport.register_announce_handler(self)
|
||||
RNS.log("AnnounceService: registered announce handler")
|
||||
|
||||
def received_announce(self, destination_hash, announced_identity, app_data):
|
||||
# Called by RNS when an announce is received
|
||||
RNS.log(f"AnnounceService: received announce from {destination_hash.hex()}")
|
||||
ts = int(time.time())
|
||||
display_name = None
|
||||
if app_data:
|
||||
@@ -41,12 +40,8 @@ class AnnounceService:
|
||||
except:
|
||||
pass
|
||||
announce = Announce(destination_hash.hex(), display_name, ts)
|
||||
# Deduplicate and move announce to top
|
||||
# Remove any existing announces with same destination_hash
|
||||
self.announces = [ann for ann in self.announces if ann.destination_hash != announce.destination_hash]
|
||||
# Insert new announce at front of list
|
||||
self.announces.insert(0, announce)
|
||||
# Notify UI of new announce
|
||||
if self.update_callback:
|
||||
self.update_callback(self.announces)
|
||||
|
||||
|
||||
@@ -2,25 +2,36 @@ import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import pathlib
|
||||
import datetime
|
||||
|
||||
import flet as ft
|
||||
from flet import AppView, Page
|
||||
|
||||
from ren_browser.ui.ui import build_ui
|
||||
import RNS
|
||||
|
||||
# Current renderer name
|
||||
RENDERER = "plaintext"
|
||||
|
||||
ERROR_LOGS: list[str] = []
|
||||
|
||||
def log_error(msg: str):
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
ERROR_LOGS.append(f"[{timestamp}] {msg}")
|
||||
|
||||
async def main(page: Page):
|
||||
# Build the main UI layout
|
||||
build_ui(page)
|
||||
loader = ft.Column(
|
||||
[ft.ProgressRing(), ft.Text("Initializing reticulum network")],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
expand=True,
|
||||
)
|
||||
page.add(loader)
|
||||
page.update()
|
||||
|
||||
def init_ret():
|
||||
config_dir = pathlib.Path(__file__).resolve().parents[1] / "config"
|
||||
try:
|
||||
RNS.Reticulum(str(config_dir))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
page.controls.clear()
|
||||
build_ui(page)
|
||||
page.update()
|
||||
|
||||
page.run_thread(init_ret)
|
||||
|
||||
def run():
|
||||
global RENDERER
|
||||
@@ -32,7 +43,6 @@ def run():
|
||||
RENDERER = args.renderer
|
||||
|
||||
if args.web:
|
||||
# Run web mode on optional fixed port
|
||||
if args.port is not None:
|
||||
ft.app(main, view=AppView.WEB_BROWSER, port=args.port)
|
||||
else:
|
||||
|
||||
20
ren_browser/logs.py
Normal file
20
ren_browser/logs.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import datetime
|
||||
import RNS
|
||||
APP_LOGS: list[str] = []
|
||||
ERROR_LOGS: list[str] = []
|
||||
RET_LOGS: list[str] = []
|
||||
_original_RNS_log = RNS.log
|
||||
def log_ret(msg, *args, **kwargs):
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
RET_LOGS.append(f"[{timestamp}] {msg}")
|
||||
return _original_RNS_log(msg, *args, **kwargs)
|
||||
RNS.log = log_ret
|
||||
|
||||
def log_error(msg: str):
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
ERROR_LOGS.append(f"[{timestamp}] {msg}")
|
||||
APP_LOGS.append(f"[{timestamp}] ERROR: {msg}")
|
||||
|
||||
def log_app(msg: str):
|
||||
timestamp = datetime.datetime.now().isoformat()
|
||||
APP_LOGS.append(f"[{timestamp}] {msg}")
|
||||
@@ -1,5 +1,6 @@
|
||||
import threading
|
||||
import time
|
||||
import pathlib
|
||||
|
||||
import RNS
|
||||
from dataclasses import dataclass
|
||||
@@ -17,34 +18,29 @@ class PageFetcher:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize Reticulum with default config (singleton)
|
||||
config_dir = pathlib.Path(__file__).resolve().parents[2] / "config"
|
||||
try:
|
||||
RNS.Reticulum()
|
||||
except OSError:
|
||||
# Already initialized
|
||||
RNS.Reticulum(str(config_dir))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
def fetch_page(self, req: PageRequest) -> str:
|
||||
RNS.log(f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}")
|
||||
"""
|
||||
Download page content for the given PageRequest.
|
||||
Placeholder implementation: replace with real network logic.
|
||||
"""
|
||||
# Establish path and identity
|
||||
dest_bytes = bytes.fromhex(req.destination_hash)
|
||||
# Request path if needed, with timeout
|
||||
if not RNS.Transport.has_path(dest_bytes):
|
||||
RNS.Transport.request_path(dest_bytes)
|
||||
start = time.time()
|
||||
# Wait up to 30 seconds for path discovery
|
||||
while not RNS.Transport.has_path(dest_bytes):
|
||||
if time.time() - start > 30:
|
||||
raise Exception(f"No path to destination {req.destination_hash}")
|
||||
time.sleep(0.1)
|
||||
# Recall identity
|
||||
identity = RNS.Identity.recall(dest_bytes)
|
||||
if not identity:
|
||||
raise Exception('Identity not found')
|
||||
# Create client destination and announce so the server learns our path
|
||||
destination = RNS.Destination(
|
||||
identity,
|
||||
RNS.Destination.OUT,
|
||||
@@ -54,7 +50,6 @@ class PageFetcher:
|
||||
)
|
||||
link = RNS.Link(destination)
|
||||
|
||||
# Prepare sync fetch
|
||||
result = {'data': None}
|
||||
ev = threading.Event()
|
||||
|
||||
@@ -69,10 +64,10 @@ class PageFetcher:
|
||||
def on_failed(_):
|
||||
ev.set()
|
||||
|
||||
# Set up request on link establishment
|
||||
link.set_link_established_callback(
|
||||
lambda l: l.request(req.page_path, req.field_data, response_callback=on_response, failed_callback=on_failed)
|
||||
)
|
||||
# Wait for response or timeout
|
||||
ev.wait(timeout=15)
|
||||
return result['data'] or 'No content received'
|
||||
data_str = result['data'] or 'No content received'
|
||||
RNS.log(f"PageFetcher: received data for {req.destination_hash}:{req.page_path}")
|
||||
return data_str
|
||||
|
||||
@@ -5,7 +5,6 @@ def render_plaintext(content: str) -> ft.Control:
|
||||
"""
|
||||
Fallback plaintext renderer: displays raw text safely in a monospace, selectable control.
|
||||
"""
|
||||
# Use monospace font and make text selectable
|
||||
return ft.Text(
|
||||
content,
|
||||
selectable=True,
|
||||
|
||||
@@ -10,30 +10,21 @@ class TabsManager:
|
||||
def __init__(self, page: ft.Page):
|
||||
import ren_browser.app as app_module
|
||||
self.page = page
|
||||
# State: list of tabs and current index
|
||||
self.manager = SimpleNamespace(tabs=[], index=0)
|
||||
# UI components
|
||||
self.tab_bar = ft.Row(spacing=4)
|
||||
# Add padding inside content area to avoid text touching the border
|
||||
self.content_container = ft.Container(expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(10))
|
||||
|
||||
# Initialize with default "Home" tab only, using selected renderer
|
||||
default_content = render_micron("Welcome to Ren Browser") if app_module.RENDERER == "micron" else render_plaintext("Welcome to Ren Browser")
|
||||
self._add_tab_internal("Home", default_content)
|
||||
# Action buttons
|
||||
self.add_btn = ft.IconButton(ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click)
|
||||
self.close_btn = ft.IconButton(ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click)
|
||||
# Append add and close buttons
|
||||
self.tab_bar.controls.extend([self.add_btn, self.close_btn])
|
||||
# Select the first tab
|
||||
self.select_tab(0)
|
||||
|
||||
def _add_tab_internal(self, title: str, content: ft.Control):
|
||||
idx = len(self.manager.tabs)
|
||||
# Create per-tab URL bar and GO button
|
||||
url_field = ft.TextField(label="URL", value=title, expand=True)
|
||||
go_btn = ft.IconButton(ft.Icons.OPEN_IN_BROWSER, tooltip="Load URL", on_click=lambda e, i=idx: self._on_tab_go(e, i))
|
||||
# Wrap the content in a Column: initial content only (URL bar is handled externally)
|
||||
content_control = content
|
||||
tab_content = ft.Column(
|
||||
expand=True,
|
||||
@@ -41,7 +32,6 @@ class TabsManager:
|
||||
content_control,
|
||||
],
|
||||
)
|
||||
# Store tab data
|
||||
self.manager.tabs.append({
|
||||
"title": title,
|
||||
"url_field": url_field,
|
||||
@@ -49,7 +39,6 @@ class TabsManager:
|
||||
"content_control": content_control,
|
||||
"content": tab_content,
|
||||
})
|
||||
# Create stylable tab button container
|
||||
btn = ft.Container(
|
||||
content=ft.Text(title),
|
||||
on_click=lambda e, i=idx: self.select_tab(i),
|
||||
@@ -57,63 +46,50 @@ class TabsManager:
|
||||
border_radius=5,
|
||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
||||
)
|
||||
# Insert before the add and close buttons
|
||||
insert_pos = max(0, len(self.tab_bar.controls) - 2)
|
||||
self.tab_bar.controls.insert(insert_pos, btn)
|
||||
|
||||
def _on_add_click(self, e):
|
||||
title = f"Tab {len(self.manager.tabs) + 1}"
|
||||
# Render new tab content based on selected renderer
|
||||
content_text = f"Content for {title}"
|
||||
import ren_browser.app as app_module
|
||||
content = render_micron(content_text) if app_module.RENDERER == "micron" else render_plaintext(content_text)
|
||||
self._add_tab_internal(title, content)
|
||||
# Select the new tab
|
||||
self.select_tab(len(self.manager.tabs) - 1)
|
||||
self.page.update()
|
||||
|
||||
def _on_close_click(self, e):
|
||||
# Do not allow closing all tabs
|
||||
if len(self.manager.tabs) <= 1:
|
||||
return
|
||||
idx = self.manager.index
|
||||
# Remove tab data and button
|
||||
self.manager.tabs.pop(idx)
|
||||
self.tab_bar.controls.pop(idx)
|
||||
# Reassign on_click handlers to correct indices
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
control.on_click = lambda e, i=i: self.select_tab(i)
|
||||
# Adjust selected index
|
||||
new_idx = min(idx, len(self.manager.tabs) - 1)
|
||||
self.select_tab(new_idx)
|
||||
self.page.update()
|
||||
|
||||
def select_tab(self, idx: int):
|
||||
self.manager.index = idx
|
||||
# Highlight active tab and dim others
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
if i == idx:
|
||||
control.bgcolor = ft.Colors.PRIMARY_CONTAINER
|
||||
else:
|
||||
control.bgcolor = ft.Colors.SURFACE_CONTAINER_HIGHEST
|
||||
# Update displayed content
|
||||
self.content_container.content = self.manager.tabs[idx]["content"]
|
||||
self.page.update()
|
||||
|
||||
def _on_tab_go(self, e, idx: int):
|
||||
"""Handle loading a new URL in a specific tab (placeholder logic)."""
|
||||
tab = self.manager.tabs[idx]
|
||||
url = tab["url_field"].value.strip()
|
||||
if not url:
|
||||
return
|
||||
# Placeholder: update the content_control using selected renderer
|
||||
placeholder_text = f"Loading content for {url}"
|
||||
import ren_browser.app as app_module
|
||||
new_control = render_micron(placeholder_text) if app_module.RENDERER == "micron" else render_plaintext(placeholder_text)
|
||||
tab["content_control"] = new_control
|
||||
# Replace the content control in the tab's Column
|
||||
tab["content"].controls[0] = new_control
|
||||
# Refresh the displayed content if this tab is active
|
||||
if self.manager.index == idx:
|
||||
self.content_container.content = tab["content"]
|
||||
self.page.update()
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import flet as ft
|
||||
import pathlib
|
||||
from ren_browser.app import ERROR_LOGS
|
||||
from ren_browser.logs import ERROR_LOGS, RET_LOGS
|
||||
|
||||
def open_settings_tab(page: ft.Page, tab_manager):
|
||||
# Locate Reticulum config file
|
||||
config_path = pathlib.Path(__file__).resolve().parents[2] / "config" / "config"
|
||||
try:
|
||||
config_text = config_path.read_text()
|
||||
except Exception as ex:
|
||||
config_text = f"Error reading config: {ex}"
|
||||
# Config editor
|
||||
config_field = ft.TextField(
|
||||
label="Reticulum config",
|
||||
value=config_text,
|
||||
expand=True,
|
||||
multiline=True,
|
||||
)
|
||||
# Save button handler
|
||||
def on_save_config(ev):
|
||||
try:
|
||||
config_path.write_text(config_field.value)
|
||||
@@ -25,7 +22,6 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
page.snack_bar = ft.SnackBar(ft.Text(f"Error saving config: {ex}"), open=True)
|
||||
page.update()
|
||||
save_btn = ft.ElevatedButton("Save and Restart", on_click=on_save_config)
|
||||
# Error logs viewer
|
||||
error_text = "\n".join(ERROR_LOGS) or "No errors logged."
|
||||
error_field = ft.TextField(
|
||||
label="Error Logs",
|
||||
@@ -34,21 +30,29 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
multiline=True,
|
||||
read_only=True,
|
||||
)
|
||||
# Placeholder for content switching
|
||||
ret_text = "\n".join(RET_LOGS) or "No Reticulum logs."
|
||||
ret_field = ft.TextField(
|
||||
label="Reticulum logs",
|
||||
value=ret_text,
|
||||
expand=True,
|
||||
multiline=True,
|
||||
read_only=True,
|
||||
)
|
||||
content_placeholder = ft.Container(expand=True)
|
||||
# View switch handlers
|
||||
def show_config(ev):
|
||||
content_placeholder.content = config_field
|
||||
page.update()
|
||||
def show_errors(ev):
|
||||
content_placeholder.content = error_field
|
||||
page.update()
|
||||
def show_ret_logs(ev):
|
||||
content_placeholder.content = ret_field
|
||||
page.update()
|
||||
btn_config = ft.ElevatedButton("Config", on_click=show_config)
|
||||
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors)
|
||||
button_row = ft.Row(controls=[btn_config, btn_errors])
|
||||
# Initialize to config view
|
||||
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs)
|
||||
button_row = ft.Row(controls=[btn_config, btn_errors, btn_ret])
|
||||
content_placeholder.content = config_field
|
||||
# Assemble settings UI
|
||||
settings_content = ft.Column(
|
||||
expand=True,
|
||||
controls=[
|
||||
@@ -60,4 +64,4 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
tab_manager._add_tab_internal("Settings", settings_content)
|
||||
idx = len(tab_manager.manager.tabs) - 1
|
||||
tab_manager.select_tab(idx)
|
||||
page.update()
|
||||
page.update()
|
||||
|
||||
Reference in New Issue
Block a user