From 47afeb2d3b05c3756adfb3fc9082ca46f8ffde1a Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 19 May 2026 18:15:40 +0200 Subject: [PATCH] Use micron engine for RRC rendering. Added support for links with request vars. --- nomadnet/ui/textui/Channels.py | 94 ++++++++++++++++++++++++------ nomadnet/ui/textui/MicronParser.py | 14 +++-- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/nomadnet/ui/textui/Channels.py b/nomadnet/ui/textui/Channels.py index 513c88f..00162f9 100644 --- a/nomadnet/ui/textui/Channels.py +++ b/nomadnet/ui/textui/Channels.py @@ -10,6 +10,27 @@ import nomadnet from nomadnet.RRC import RRCHub from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox from nomadnet.ui.textui.MicronParser import LinkableText, LinkSpec +from .MicronParser import markup_to_attrmaps +from nomadnet.util import strip_modifiers + + +theme_dark = { "ts": "888", + "nick_self": "6c5", + "nick_peer": "3cd", + "notice": "fd3", + "error": "f55", + "system": "888", + "mention": "fb4", + "link": "79d", } + +theme_light = { "ts": "888", + "nick_self": "3a0", + "nick_peer": "077", + "notice": "a70", + "error": "a22", + "system": "888", + "mention": "c50", + "link": "79d", } class _ChatLinkableText(LinkableText): @@ -95,7 +116,7 @@ def _scan_mentions(text, own_nick): yield m.start(), m.end(), "mention", None -def _body_markup(body, body_attr="body_text", own_nick=None): +def _body_markup(body, body_attr="body_text", own_nick=None, check_links=True): spans = list(_scan_links(body)) spans.extend(_scan_mentions(body, own_nick)) spans.sort(key=lambda s: s[0]) @@ -120,8 +141,12 @@ def _body_markup(body, body_attr="body_text", own_nick=None): out.append(("irc_mention", body[start:end])) else: base = _LINK_ATTRS[kind] - out.append((LinkSpec(kind+":"+target, base, cm=256), body[start:end])) - has_links = True + if check_links: + out.append((LinkSpec(kind+":"+target, base, cm=256), body[start:end])) + has_links = True + else: + out.append((f"link_{kind}", body[start:end])) + has_links = True pos = end if pos < len(body): out.append((body_attr, body[pos:])) @@ -786,6 +811,10 @@ def _ts_prefix(ts_ms): t = _format_ts(ts_ms) if ts_ms else " " return ("irc_ts", " ["+t+"] ") +def _ts_prefix_raw(ts_ms): + t = _format_ts(ts_ms) if ts_ms else " " + return "["+t+"] " + class _ChatLinkDelegate: def __init__(self, display, hub): @@ -801,18 +830,23 @@ class _ChatLinkDelegate: pass def handle_link(self, target, fields=None): - if target is None: - return - kind, _, payload = target.partition(":") + if target is None: return try: - if kind == "room": - self._open_room(payload) - elif kind == "lxmf": - self._open_lxmf(payload) + components = target.split("://") + if len(components) < 2: return + kind = components[0] + payload = components[1] + if kind == "room": self._open_room(payload.lstrip("#")) + elif kind == "lxmf": self._open_lxmf(payload.lstrip("lxmf@")) elif kind == "page": - self._open_page(payload) - except Exception as e: - RNS.log("Chat link handler failed: "+str(e), RNS.LOG_ERROR) + final_url = payload + if fields: + final_url += "`" + for f in fields: final_url += f"{f}|" + final_url.rstrip("|") + self._open_page(final_url) + else: RNS.log(f"Invalid URL: {target}", RNS.LOG_WARNING) + except Exception as e: RNS.log("Chat link handler failed: "+str(e), RNS.LOG_ERROR) def _open_room(self, room): room = (room or "").strip().lower() @@ -863,8 +897,8 @@ class _ChatLinkDelegate: except Exception as e: RNS.log("Could not open page link: "+str(e), RNS.LOG_ERROR) - def _message_widget(app, hub, m, link_delegate=None): + t = theme_dark if app.config["textui"]["theme"] == nomadnet.ui.TextUI.THEME_DARK else theme_light g = app.ui.glyphs own_nick = None try: @@ -913,12 +947,38 @@ def _message_widget(app, hub, m, link_delegate=None): nick_attr = "irc_nick_self" if own else "irc_nick_peer" body = m.text or "" - spans, has_links = _body_markup(body, body_attr="body_text", own_nick=None if own else own_nick) - markup = [_ts_prefix(m.ts), (nick_attr, "<"+sender+">"), ("body_text", " ")] + spans - final_widget = _wrap_text(markup, link_delegate if has_links else None) + spans, has_links = _body_markup(body, body_attr="body_text", own_nick=None if own else own_nick, check_links=False) + ld = link_delegate if has_links else None + + message_body = "" + for span in spans: + ms = span[0] + mb = span[1] + if ms.startswith("irc_mention"): message_body += f"`!`F{t['mention']}{mb}`f`!" + elif ms.startswith("link_"): + kind = ms[len("link_"):] + label = mb.split("`")[0] + url = f"{kind}://{mb}" + link_mu = f"`_`F{t['link']}`[{label}`{url}]`f`_" + message_body += link_mu + else: message_body += mb + + nick_attr = f"`F{t['nick_self']}" if own else f"`F{t['nick_peer']}" + irc_ts = f"`F{t['ts']}" + + prefix_micron = f"{irc_ts}{_ts_prefix_raw(m.ts)}`f {nick_attr}<{sender}>`f " + + rendered = _render_body(f"{prefix_micron}{message_body}", link_delegate=ld) + final_widget = urwid.Padding(urwid.Pile(rendered), left=1) + final_widget.msg = m return final_widget +def _render_body(markup, link_delegate=None): + try: return markup_to_attrmaps(strip_modifiers(markup), url_delegate=link_delegate, link_class=_ChatLinkableText) + except Exception as e: + RNS.trace_exception(e) + return [] def _wrap_text(markup, link_delegate): if link_delegate is not None: diff --git a/nomadnet/ui/textui/MicronParser.py b/nomadnet/ui/textui/MicronParser.py index a043f99..bf1ed30 100644 --- a/nomadnet/ui/textui/MicronParser.py +++ b/nomadnet/ui/textui/MicronParser.py @@ -60,7 +60,11 @@ def default_state(fg=None, bg=None): } return state -def markup_to_attrmaps(markup, url_delegate = None, fg_color=None, bg_color=None): +def markup_to_attrmaps(markup, url_delegate = None, fg_color=None, bg_color=None, link_class=None): + global LINK_CLASS + if link_class: LINK_CLASS = link_class + else: LINK_CLASS = LinkableText + global SELECTED_STYLES if nomadnet.NomadNetworkApp.get_shared_instance().config["textui"]["theme"] == nomadnet.ui.TextUI.THEME_DARK: SELECTED_STYLES = STYLES_DARK @@ -290,7 +294,7 @@ def parse_line(line, state, url_delegate): for o in output: if isinstance(o, tuple): if url_delegate != None: - tw = LinkableText(o, align=state["align"], delegate=url_delegate) + tw = LINK_CLASS(o, align=state["align"], delegate=url_delegate) tw.in_columns = True else: tw = urwid.Text(o, align=state["align"]) @@ -343,7 +347,7 @@ def parse_line(line, state, url_delegate): else: if url_delegate != None: - text_widget = LinkableText(output, align=state["align"], delegate=url_delegate) + text_widget = LINK_CLASS(output, align=state["align"], delegate=url_delegate) else: text_widget = urwid.Text(output, align=state["align"]) @@ -973,4 +977,6 @@ class LinkableText(urwid.Text): return True except Exception as e: - return False \ No newline at end of file + return False + +LINK_CLASS = LinkableText \ No newline at end of file