2021-07-06 09:23:37 +02:00
import nomadnet
import urwid
2023-02-09 21:18:14 +01:00
import random
2026-05-20 16:16:24 -04:00
import re
2021-08-27 19:58:14 +02:00
import time
2025-12-29 19:47:08 +01:00
import RNS
2021-08-27 19:58:14 +02:00
from urwid . util import is_mouse_press
from urwid . text_layout import calc_coords
2026-05-22 20:17:47 -04:00
from . ReadlineEdit import ReadlineEdit
2026-05-04 01:18:37 +02:00
from RNS . Utilities . rngit . util import MarkdownToMicron
2026-09-02 22:48:31 +02:00
from nomadnet . util import STRIP_CONTROL_RE
2026-09-05 22:15:10 +02:00
try : from . images import ImageWidget
except Exception : ImageWidget = None
2021-07-06 09:23:37 +02:00
2021-09-17 21:16:30 +02:00
DEFAULT_FG_DARK = " ddd "
DEFAULT_FG_LIGHT = " 222 "
2021-08-28 21:30:27 +02:00
DEFAULT_BG = " default "
2021-09-17 21:16:30 +02:00
SELECTED_STYLES = None
STYLES_DARK = {
" plain " : { " fg " : DEFAULT_FG_DARK , " bg " : DEFAULT_BG , " bold " : False , " underline " : False , " italic " : False } ,
2021-07-06 09:23:37 +02:00
" heading1 " : { " fg " : " 222 " , " bg " : " bbb " , " bold " : False , " underline " : False , " italic " : False } ,
" heading2 " : { " fg " : " 111 " , " bg " : " 999 " , " bold " : False , " underline " : False , " italic " : False } ,
" heading3 " : { " fg " : " 000 " , " bg " : " 777 " , " bold " : False , " underline " : False , " italic " : False } ,
}
2021-09-17 21:16:30 +02:00
STYLES_LIGHT = {
" plain " : { " fg " : DEFAULT_FG_LIGHT , " bg " : DEFAULT_BG , " bold " : False , " underline " : False , " italic " : False } ,
" heading1 " : { " fg " : " 000 " , " bg " : " 777 " , " bold " : False , " underline " : False , " italic " : False } ,
" heading2 " : { " fg " : " 111 " , " bg " : " aaa " , " bold " : False , " underline " : False , " italic " : False } ,
" heading3 " : { " fg " : " 222 " , " bg " : " ccc " , " bold " : False , " underline " : False , " italic " : False } ,
}
2021-07-06 09:23:37 +02:00
SYNTH_STYLES = [ ]
2021-08-27 19:58:14 +02:00
SYNTH_SPECS = { }
2021-07-06 09:23:37 +02:00
SECTION_INDENT = 2
INDENT_RIGHT = 1
2026-05-04 01:18:37 +02:00
MAX_TABLE_WIDTH = 100
2021-07-06 09:23:37 +02:00
2026-06-08 05:00:46 -04:00
DEFAULT_FOLD_GLYPHS = ( " ▾ " , " ▸ " )
def default_fold_glyphs ( ) :
try :
g = nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . glyphs
return ( g . get ( " fold_open " , DEFAULT_FOLD_GLYPHS [ 0 ] ) , g . get ( " fold_closed " , DEFAULT_FOLD_GLYPHS [ 1 ] ) )
except Exception :
return DEFAULT_FOLD_GLYPHS
2025-05-11 20:59:26 +02:00
def default_state ( fg = None , bg = None ) :
2026-05-22 11:33:13 +02:00
global SELECTED_STYLES
ensure_selected_styles ( )
2025-05-11 20:59:26 +02:00
if fg == None : fg = SELECTED_STYLES [ " plain " ] [ " fg " ]
if bg == None : bg = DEFAULT_BG
2021-07-06 09:23:37 +02:00
state = {
" literal " : False ,
2026-05-04 01:18:37 +02:00
" table_mode " : False ,
" table_buffer " : [ ] ,
" table_align " : None ,
" table_maxwidth " : MAX_TABLE_WIDTH ,
2021-07-06 09:23:37 +02:00
" depth " : 0 ,
2025-05-11 20:59:26 +02:00
" fg_color " : fg ,
" bg_color " : bg ,
2021-07-06 09:23:37 +02:00
" formatting " : {
" bold " : False ,
" underline " : False ,
" italic " : False ,
" strikethrough " : False ,
" blink " : False ,
} ,
" default_align " : " left " ,
" align " : " left " ,
2025-05-11 20:59:26 +02:00
" default_fg " : fg ,
" default_bg " : bg ,
2026-05-20 16:16:24 -04:00
" anchors " : { } ,
" pending_anchors " : [ ] ,
" header_rows " : [ ] ,
2021-07-06 09:23:37 +02:00
}
2025-05-11 20:59:26 +02:00
return state
2026-05-20 16:16:24 -04:00
_MICRON_STRIP_RE = re . compile (
r " `[FB]T[0-9a-fA-F] {6} "
r " |`[FB][0-9a-fA-F] {3} "
r " |`:[A-Za-z0-9_ \ -]* "
r " |`[!*_=fbacrl`<> { ] "
)
def slugify_micron ( text ) :
if text is None : return " "
stripped = _MICRON_STRIP_RE . sub ( " " , text )
s = re . sub ( r " [^A-Za-z0-9]+ " , " - " , stripped ) . strip ( " - " ) . lower ( )
return s
class _AttrMapList ( list ) :
""" list subclass that allows attaching parser metadata like `anchors`. """
pass
2026-05-22 11:33:13 +02:00
def ensure_selected_styles ( ) :
global SELECTED_STYLES
theme = nomadnet . NomadNetworkApp . get_shared_instance ( ) . config [ " textui " ] [ " theme " ]
if theme == nomadnet . ui . TextUI . THEME_DARK : SELECTED_STYLES = STYLES_DARK
else : SELECTED_STYLES = STYLES_LIGHT
2026-05-20 16:16:24 -04:00
def markup_to_attrmaps ( markup , url_delegate = None , fg_color = None , bg_color = None , link_class = None , anchors = None ) :
2026-09-02 22:48:31 +02:00
markup = STRIP_CONTROL_RE . sub ( " " , markup )
2026-05-19 18:15:40 +02:00
global LINK_CLASS
if link_class : LINK_CLASS = link_class
else : LINK_CLASS = LinkableText
2025-05-11 20:59:26 +02:00
global SELECTED_STYLES
2026-05-22 11:33:13 +02:00
ensure_selected_styles ( )
2025-05-11 20:59:26 +02:00
2026-05-20 16:16:24 -04:00
attrmaps = _AttrMapList ( )
2025-05-11 20:59:26 +02:00
fgc = None ; bgc = DEFAULT_BG
if bg_color != None : bgc = bg_color
if fg_color != None : fgc = fg_color
state = default_state ( fgc , bgc )
2026-05-20 16:16:24 -04:00
if anchors is None :
anchors = state [ " anchors " ]
else :
state [ " anchors " ] = anchors
2021-07-06 09:23:37 +02:00
# Split entire document into lines for
# processing.
lines = markup . split ( " \n " ) ;
for line in lines :
if len ( line ) > 0 :
2023-02-14 15:26:25 +01:00
display_widgets = parse_line ( line , state , url_delegate )
2021-07-06 09:23:37 +02:00
else :
2023-02-14 15:26:25 +01:00
display_widgets = [ urwid . Text ( " " ) ]
2026-05-20 16:16:24 -04:00
2023-02-14 15:26:25 +01:00
if display_widgets != None and len ( display_widgets ) != 0 :
2026-05-20 16:16:24 -04:00
row_index = len ( attrmaps )
pending = state . get ( " pending_anchors " ) or [ ]
if pending :
for name in pending :
if name and name not in anchors :
anchors [ name ] = row_index
state [ " pending_anchors " ] = [ ]
if state . get ( " _header_pending " ) :
state [ " header_rows " ] . append ( row_index )
2026-06-08 05:00:46 -04:00
state . setdefault ( " header_levels " , { } ) [ row_index ] = state [ " depth " ]
cp = state . get ( " _collapsible_pending " )
if cp is not None :
state . setdefault ( " collapsibles " , { } ) [ row_index ] = bool ( cp [ " collapsed " ] )
state [ " _collapsible_pending " ] = None
2026-05-20 16:16:24 -04:00
state [ " _header_pending " ] = False
2026-06-08 05:00:46 -04:00
row_levels = state . setdefault ( " row_levels " , { } )
2023-02-14 15:26:25 +01:00
for display_widget in display_widgets :
2026-06-08 05:00:46 -04:00
row_levels [ len ( attrmaps ) ] = state [ " depth " ]
2023-02-14 15:26:25 +01:00
attrmap = urwid . AttrMap ( display_widget , make_style ( state ) )
attrmaps . append ( attrmap )
2021-07-06 09:23:37 +02:00
2026-05-20 16:16:24 -04:00
try :
setattr ( attrmaps , " anchors " , anchors )
setattr ( attrmaps , " header_rows " , list ( state . get ( " header_rows " , [ ] ) ) )
2026-06-08 05:00:46 -04:00
setattr ( attrmaps , " header_levels " , dict ( state . get ( " header_levels " , { } ) ) )
setattr ( attrmaps , " row_levels " , dict ( state . get ( " row_levels " , { } ) ) )
setattr ( attrmaps , " collapsibles " , dict ( state . get ( " collapsibles " , { } ) ) )
setattr ( attrmaps , " fold_glyphs " , state . get ( " fold_glyphs " ) or default_fold_glyphs ( ) )
2026-05-20 16:16:24 -04:00
except Exception :
pass
2021-07-06 09:23:37 +02:00
return attrmaps
2025-12-29 19:47:08 +01:00
def parse_partial ( line ) :
try :
endpos = line . find ( " } " )
if endpos == - 1 : return None
else :
partial_data = line [ 0 : endpos ]
2025-12-29 22:21:51 +01:00
partial_id = None
2025-12-29 19:47:08 +01:00
partial_components = partial_data . split ( " ` " )
if len ( partial_components ) == 1 :
partial_url = partial_components [ 0 ]
partial_refresh = None
partial_fields = " "
elif len ( partial_components ) == 2 :
partial_url = partial_components [ 0 ]
partial_refresh = float ( partial_components [ 1 ] )
partial_fields = " "
2025-12-29 20:24:12 +01:00
elif len ( partial_components ) == 3 :
partial_url = partial_components [ 0 ]
partial_refresh = float ( partial_components [ 1 ] )
partial_fields = partial_components [ 2 ]
2025-12-29 19:47:08 +01:00
else :
partial_url = " "
partial_fields = " "
partial_refresh = None
2025-12-29 22:21:51 +01:00
if partial_refresh != None and partial_refresh < 1 : partial_refresh = None
2025-12-29 19:47:08 +01:00
2025-12-29 20:24:12 +01:00
pf = partial_fields . split ( " | " )
2025-12-29 22:21:51 +01:00
if len ( pf ) > 0 :
partial_fields = pf
for f in pf :
if f . startswith ( " pid= " ) :
pcs = f . split ( " = " )
partial_id = pcs [ 1 ]
2025-12-29 20:24:12 +01:00
2025-12-29 19:47:08 +01:00
if len ( partial_url ) :
pile = urwid . Pile ( [ urwid . Text ( f " ⧖ " ) ] )
partial_descriptor = " | " . join ( partial_components )
2025-12-29 22:21:51 +01:00
pile . partial_id = partial_id
pile . partial_hash = RNS . hexrep ( RNS . Identity . full_hash ( partial_descriptor . encode ( " utf-8 " ) ) , delimit = False )
2025-12-29 19:47:08 +01:00
pile . partial_url = partial_url
pile . partial_fields = partial_fields
pile . partial_refresh = partial_refresh
return [ pile ]
except Exception as e : return None
2026-09-05 22:15:10 +02:00
def parse_image ( line , state , url_delegate ) :
try :
endpos = line . rfind ( " ) " )
if endpos < = 0 : return None
image_data = line [ : endpos ]
fields = image_data . split ( " ` " )
if len ( fields ) < 2 : return None
alt_text = fields [ 0 ] . strip ( )
image_url = fields [ - 1 ] . strip ( )
properties = fields [ 1 : - 1 ]
width = None
height = None
align = None
for prop in properties :
if " = " not in prop : continue
key , _ , value = prop . partition ( " = " )
key = key . strip ( )
value = value . strip ( )
if key == " w " : width = value
elif key == " h " : height = value
elif key == " a " : align = value
2026-09-05 23:31:29 +02:00
if align == " c " : align = " center "
if align == " l " : align = " left "
if align == " r " : align = " right "
2026-09-05 22:15:10 +02:00
widget = None
if image_url :
try : path = url_delegate . resolve_image ( image_url )
except Exception as e :
RNS . trace_exception ( e ) # TODO: Remove
path = None
if ImageWidget is not None :
try :
if not ImageWidget . placeholder_glyph : ImageWidget . placeholder_glyph = f " { url_delegate . g [ ' image ' ] } "
widget = ImageWidget ( path , name = alt_text )
for prop_name , prop_value in ( ( " width " , width ) , ( " height " , height ) , ( " align " , align ) ) :
if prop_value is not None :
try :
if isinstance ( prop_value , str ) and prop_value . isdigit ( ) : prop_value = int ( prop_value )
setattr ( widget , prop_name , prop_value )
except Exception as e :
RNS . trace_exception ( e ) # TODO: Remove
pass
except Exception as e :
RNS . trace_exception ( e ) # TODO: Remove
widget = None
if widget is None :
placeholder = alt_text if alt_text else ( image_url or " image " )
widget = urwid . Text ( f " [ { url_delegate . g [ ' warning ' ] } Error loading image: { placeholder } ] " )
widget . image_url = image_url
widget . image_alt = alt_text
if state [ " depth " ] == 0 : return [ widget ]
else :
padded = urwid . Padding ( widget , left = left_indent ( state ) , right = right_indent ( state ) )
padded . _contained_image = widget
return [ padded ]
except Exception as e :
# TODO: Remove
RNS . trace_exception ( e )
return None
2026-05-04 01:18:37 +02:00
def render_table ( lines , state , url_delegate ) :
if len ( lines ) < 2 : return None
if state [ " table_maxwidth " ] : max_width = state [ " table_maxwidth " ]
else : max_width = MAX_TABLE_WIDTH
if state [ " table_align " ] : align = state [ " table_align " ]
else : align = None
converter = MarkdownToMicron ( max_width = max_width )
micron_lines = converter . format_table_raw ( lines , align = align )
widgets = [ ]
for line in micron_lines :
# Disable table mode to avoid recursion
was_table_mode = state [ " table_mode " ]
state [ " table_mode " ] = False
line_widgets = parse_line ( line , state , url_delegate )
state [ " table_mode " ] = was_table_mode
if line_widgets : widgets . extend ( line_widgets )
return widgets if widgets else None
2026-06-07 19:39:23 -04:00
class FormColumns ( urwid . Columns ) :
MIN_FIELD_WIDTH = 4
2026-06-08 05:00:46 -04:00
@staticmethod
2026-06-07 19:39:23 -04:00
def _natural_width ( widget , maxcol , focused ) :
# mirror the width urwid.Columns itself computes for a PACK column
if not isinstance ( widget , urwid . Widget ) :
return 0
sizing = widget . sizing ( )
if sizing & frozenset ( ( urwid . FIXED , urwid . FLOW ) ) :
width = 0
if urwid . FIXED in sizing :
width = widget . pack ( ( ) , focused ) [ 0 ]
if urwid . FLOW in sizing and ( not width or width > maxcol ) :
width = widget . pack ( ( maxcol , ) , focused ) [ 0 ]
return width
return widget . pack ( ( maxcol , ) , focused ) [ 0 ]
def column_widths ( self , size , focus = False ) :
contents = self . contents
field_idxs = [ i for i , ( w , ( t , n , b ) ) in enumerate ( contents ) if t == urwid . GIVEN ]
if not field_idxs :
return super ( ) . column_widths ( size , focus )
maxcol = size [ 0 ]
nat = [ ]
for i , ( w , ( t , width , b ) ) in enumerate ( contents ) :
if t == urwid . GIVEN :
nat . append ( width )
elif t == urwid . PACK :
nat . append ( self . _natural_width ( w , maxcol , focus and i == self . focus_position ) )
else :
return super ( ) . column_widths ( size , focus )
dividers = self . dividechars * max ( 0 , len ( nat ) - 1 )
field_set = set ( field_idxs )
fixed = sum ( w for i , w in enumerate ( nat ) if i not in field_set )
requested = sum ( nat [ i ] for i in field_idxs )
available = maxcol - dividers - fixed
if available > = requested :
return super ( ) . column_widths ( size , focus )
if available < self . MIN_FIELD_WIDTH * len ( field_idxs ) :
return super ( ) . column_widths ( size , focus )
overflow = requested - available
slack = { i : max ( 0 , nat [ i ] - self . MIN_FIELD_WIDTH ) for i in field_idxs }
total_slack = sum ( slack . values ( ) )
if total_slack < overflow :
return super ( ) . column_widths ( size , focus )
widths = list ( nat )
removed = { i : min ( slack [ i ] , overflow * slack [ i ] / / total_slack ) for i in field_idxs }
shortfall = overflow - sum ( removed . values ( ) )
# Hand out any rounding remainder one column at a time.
pending = [ i for i in field_idxs if removed [ i ] < slack [ i ] ]
k = 0
while shortfall > 0 and pending :
i = pending [ k % len ( pending ) ]
removed [ i ] + = 1
shortfall - = 1
if removed [ i ] > = slack [ i ] :
pending = [ j for j in pending if removed [ j ] < slack [ j ] ]
k = 0
else :
k + = 1
for i in field_idxs :
widths [ i ] = nat [ i ] - removed [ i ]
return widths
2026-06-08 05:00:46 -04:00
class MicronField ( ReadlineEdit ) :
def __init__ ( self , * args , min_rows = 1 , * * kwargs ) :
try : self . _min_rows = max ( 1 , int ( min_rows ) )
except ( TypeError , ValueError ) : self . _min_rows = 1
super ( ) . __init__ ( * args , * * kwargs )
def rows ( self , size , focus = False ) :
return max ( self . _min_rows , super ( ) . rows ( size , focus ) )
def render ( self , size , focus = False ) :
canv = super ( ) . render ( size , focus )
if canv . rows ( ) < self . _min_rows :
canv = urwid . CompositeCanvas ( canv )
canv . pad_trim_top_bottom ( 0 , self . _min_rows - canv . rows ( ) )
return canv
class CollapsibleHeading ( urwid . WidgetWrap ) :
_selectable = True
ignore_focus = False
def __init__ ( self , segments , align , heading_style , depth ,
collapsed = False , glyphs = None , delegate = None ) :
self . _segments = list ( segments )
self . align = align
self . heading_style = heading_style
self . depth = depth
self . collapsed = bool ( collapsed )
self . glyphs = glyphs if glyphs is not None else default_fold_glyphs ( )
self . delegate = delegate
self . is_collapsible_heading = True
super ( ) . __init__ ( self . _build ( ) )
def selectable ( self ) :
return True
def _glyph ( self ) :
return self . glyphs [ 1 ] if self . collapsed else self . glyphs [ 0 ]
def _glyph_col ( self ) :
if self . _segments and isinstance ( self . _segments [ 0 ] , str ) :
return len ( self . _segments [ 0 ] )
return 0
def _build ( self ) :
segs = list ( self . _segments )
marker = self . _glyph ( ) + " "
if segs and isinstance ( segs [ 0 ] , str ) :
segs . insert ( 1 , marker )
else :
segs . insert ( 0 , marker )
return urwid . AttrMap ( urwid . Text ( segs , align = self . align ) , self . heading_style )
def set_collapsed ( self , collapsed ) :
collapsed = bool ( collapsed )
if collapsed != self . collapsed :
self . collapsed = collapsed
self . _w = self . _build ( )
self . _invalidate ( )
def toggle ( self ) :
self . collapsed = not self . collapsed
self . _w = self . _build ( )
self . _invalidate ( )
if self . delegate is not None and hasattr ( self . delegate , " fold_changed " ) :
self . delegate . fold_changed ( self )
def keypress ( self , size , key ) :
if self . _command_map [ key ] == urwid . ACTIVATE or key == " " :
self . toggle ( )
return None
return key
def mouse_event ( self , size , event , button , x , y , focus ) :
if button == 1 and is_mouse_press ( event ) :
self . toggle ( )
return True
return False
def render ( self , size , focus = False ) :
canv = super ( ) . render ( size , focus )
if focus :
canv = urwid . CompositeCanvas ( canv )
gx = self . _glyph_col ( )
if self . align == " left " and gx < size [ 0 ] :
canv . cursor = ( gx , 0 )
return canv
2021-08-27 19:58:14 +02:00
def parse_line ( line , state , url_delegate ) :
2025-05-11 18:43:13 +02:00
pre_escape = False
2021-07-06 09:23:37 +02:00
if len ( line ) > 0 :
first_char = line [ 0 ]
# Check for literals
if len ( line ) == 2 and line == " `= " :
state [ " literal " ] ^ = True
return None
# Only parse content if not in literal state
if not state [ " literal " ] :
2026-06-08 05:00:46 -04:00
collapsible_heading = False
collapsed_initial = False
if ( line . startswith ( " `+ " ) or line . startswith ( " `- " ) ) and line [ 2 : 3 ] == " > " :
collapsible_heading = True
collapsed_initial = line [ 1 ] == " - "
line = line [ 2 : ]
first_char = line [ 0 ]
2025-12-30 12:16:24 +01:00
# Apply markup sanitization
if first_char == " > " and " `< " in line :
# Remove heading status from lines containing fields
line = line . lstrip ( " > " )
first_char = line [ 0 ]
2021-07-06 09:23:37 +02:00
# Check if the command is an escape
if first_char == " \\ " :
line = line [ 1 : ]
2025-05-11 18:43:13 +02:00
pre_escape = True
2021-07-06 09:23:37 +02:00
2021-09-17 18:45:08 +02:00
# Check for comments
elif first_char == " # " :
2026-06-08 05:00:46 -04:00
parts = line . split ( )
if parts and parts [ 0 ] == " #!fold " :
if len ( parts ) > = 3 : state [ " fold_glyphs " ] = ( parts [ 1 ] , parts [ 2 ] )
elif len ( parts ) == 2 : state [ " fold_glyphs " ] = ( parts [ 1 ] , parts [ 1 ] )
2021-09-17 18:45:08 +02:00
return None
2026-05-04 01:18:37 +02:00
# Check for tables
if line . startswith ( " `t " ) :
line = line [ 2 : ]
align = line [ 0 ] if len ( line ) and line [ 0 ] in [ " l " , " c " , " r " ] else None
max_width = None
if align : line = line [ 1 : ]
if len ( line ) :
try : max_width = int ( line )
except : pass
if state [ " table_mode " ] :
widgets = render_table ( state [ " table_buffer " ] , state , url_delegate )
state [ " table_mode " ] = False
state [ " table_buffer " ] = [ ]
state [ " table_align " ] = None
state [ " table_maxwidth " ] = MAX_TABLE_WIDTH
return widgets
else :
state [ " table_mode " ] = True
state [ " table_buffer " ] = [ ]
state [ " table_align " ] = align
state [ " table_maxwidth " ] = max_width
return None
# Buffer the line if in table mode
if state [ " table_mode " ] :
state [ " table_buffer " ] . append ( line )
return None
2025-12-29 19:47:08 +01:00
# Check for partials
elif line . startswith ( " ` { " ) :
return parse_partial ( line [ 2 : ] )
2026-09-05 22:15:10 +02:00
# Check for images
elif line . startswith ( " `( " ) :
return parse_image ( line [ 2 : ] , state , url_delegate )
2021-07-06 09:23:37 +02:00
# Check for section heading reset
elif first_char == " < " :
state [ " depth " ] = 0
2021-08-27 19:58:14 +02:00
return parse_line ( line [ 1 : ] , state , url_delegate )
2021-07-06 09:23:37 +02:00
# Check for section headings
elif first_char == " > " :
i = 0
while i < len ( line ) and line [ i ] == " > " :
i + = 1
state [ " depth " ] = i
2026-05-20 16:16:24 -04:00
2021-07-06 09:23:37 +02:00
for j in range ( 1 , i + 1 ) :
wanted_style = " heading " + str ( i )
2021-09-17 21:16:30 +02:00
if wanted_style in SELECTED_STYLES :
style = SELECTED_STYLES [ wanted_style ]
2021-07-06 09:23:37 +02:00
line = line [ state [ " depth " ] : ]
if len ( line ) > 0 :
latched_style = state_to_style ( state )
style_to_state ( style , state )
heading_style = make_style ( state )
2021-08-27 19:58:14 +02:00
output = make_output ( state , line , url_delegate )
2026-05-20 16:16:24 -04:00
2021-07-06 09:23:37 +02:00
style_to_state ( latched_style , state )
2026-05-20 16:16:24 -04:00
slug = slugify_micron ( line )
if slug :
state . setdefault ( " pending_anchors " , [ ] ) . append ( slug )
state [ " _header_pending " ] = True
2021-07-06 09:23:37 +02:00
if len ( output ) > 0 :
first_style = output [ 0 ] [ 0 ]
heading_style = first_style
output . insert ( 0 , " " * left_indent ( state ) )
2026-06-08 05:00:46 -04:00
if collapsible_heading :
state [ " _collapsible_pending " ] = { " collapsed " : collapsed_initial }
glyphs = state . get ( " fold_glyphs " ) or default_fold_glyphs ( )
return [ CollapsibleHeading ( output , state [ " align " ] , heading_style ,
state [ " depth " ] , collapsed_initial , glyphs , url_delegate ) ]
2023-02-14 15:26:25 +01:00
return [ urwid . AttrMap ( urwid . Text ( output , align = state [ " align " ] ) , heading_style ) ]
2021-07-06 09:23:37 +02:00
else :
return None
else :
return None
# Check for horizontal dividers
elif first_char == " - " :
if len ( line ) == 2 :
divider_char = line [ 1 ]
2025-06-25 23:48:26 +03:00
# Control characters don't make sense here and otherwise crash nomadnet
2026-09-27 14:30:47 +02:00
if ord ( divider_char ) < 32 : divider_char = " \u2500 "
# Check if the divider char is renderable
try : urwid . Divider ( divider_char ) . render ( ( 30 , ) )
except Exception as e : divider_char = " \u2500 "
else : divider_char = " \u2500 "
if state [ " depth " ] == 0 : return [ urwid . Divider ( divider_char ) ]
else : return [ urwid . Padding ( urwid . Divider ( divider_char ) , left = left_indent ( state ) , right = right_indent ( state ) ) ]
2021-07-06 09:23:37 +02:00
2025-05-11 18:43:13 +02:00
output = make_output ( state , line , url_delegate , pre_escape )
2021-07-06 09:23:37 +02:00
if output != None :
2023-02-14 15:26:25 +01:00
text_only = True
for o in output :
if not isinstance ( o , tuple ) :
text_only = False
break
if not text_only :
widgets = [ ]
for o in output :
if isinstance ( o , tuple ) :
if url_delegate != None :
2026-05-19 18:15:40 +02:00
tw = LINK_CLASS ( o , align = state [ " align " ] , delegate = url_delegate )
2023-02-15 11:53:34 +01:00
tw . in_columns = True
2023-02-14 15:26:25 +01:00
else :
tw = urwid . Text ( o , align = state [ " align " ] )
2024-01-18 11:58:22 +01:00
widgets . append ( ( urwid . PACK , tw ) )
2023-02-14 15:26:25 +01:00
else :
if o [ " type " ] == " field " :
2023-02-14 16:07:30 +01:00
fw = o [ " width " ]
2023-02-14 15:26:25 +01:00
fd = o [ " data " ]
fn = o [ " name " ]
fs = o [ " style " ]
2026-06-08 05:00:46 -04:00
frows = o . get ( " rows " , 1 )
2023-02-14 16:07:30 +01:00
fmask = " * " if o [ " masked " ] else None
2026-06-08 05:00:46 -04:00
f = MicronField ( caption = " " , edit_text = fd , align = state [ " align " ] , multiline = True , mask = fmask , min_rows = frows )
2023-02-14 15:26:25 +01:00
f . field_name = fn
fa = urwid . AttrMap ( f , fs )
widgets . append ( ( fw , fa ) )
2024-11-25 18:15:42 -05:00
elif o [ " type " ] == " checkbox " :
fn = o [ " name " ]
fv = o [ " value " ]
flabel = o [ " label " ]
fs = o [ " style " ]
2024-11-26 19:58:53 -05:00
fprechecked = o . get ( " prechecked " , False )
f = urwid . CheckBox ( flabel , state = fprechecked )
2024-11-25 18:15:42 -05:00
f . field_name = fn
f . field_value = fv
fa = urwid . AttrMap ( f , fs )
widgets . append ( ( urwid . PACK , fa ) )
elif o [ " type " ] == " radio " :
fn = o [ " name " ]
fv = o [ " value " ]
flabel = o [ " label " ]
fs = o [ " style " ]
2024-11-26 19:58:53 -05:00
fprechecked = o . get ( " prechecked " , False )
if " radio_groups " not in state :
2024-11-25 18:15:42 -05:00
state [ " radio_groups " ] = { }
2024-11-26 19:58:53 -05:00
if fn not in state [ " radio_groups " ] :
2024-11-25 18:15:42 -05:00
state [ " radio_groups " ] [ fn ] = [ ]
group = state [ " radio_groups " ] [ fn ]
2024-11-26 19:58:53 -05:00
f = urwid . RadioButton ( group , flabel , state = fprechecked , user_data = fv )
2024-11-25 18:15:42 -05:00
f . field_name = fn
f . field_value = fv
fa = urwid . AttrMap ( f , fs )
widgets . append ( ( urwid . PACK , fa ) )
2023-02-14 15:26:25 +01:00
2024-11-26 19:58:53 -05:00
2026-06-07 19:39:23 -04:00
columns_widget = FormColumns ( widgets , dividechars = 0 )
2023-02-14 15:26:25 +01:00
text_widget = columns_widget
# text_widget = urwid.Text("<"+output+">", align=state["align"])
2021-08-27 19:58:14 +02:00
else :
2023-02-14 15:26:25 +01:00
if url_delegate != None :
2026-05-19 18:15:40 +02:00
text_widget = LINK_CLASS ( output , align = state [ " align " ] , delegate = url_delegate )
2023-02-14 15:26:25 +01:00
else :
text_widget = urwid . Text ( output , align = state [ " align " ] )
2021-08-27 19:58:14 +02:00
2021-07-06 09:23:37 +02:00
if state [ " depth " ] == 0 :
2023-02-14 15:26:25 +01:00
return [ text_widget ]
2021-07-06 09:23:37 +02:00
else :
2023-02-14 15:26:25 +01:00
return [ urwid . Padding ( text_widget , left = left_indent ( state ) , right = right_indent ( state ) ) ]
2021-07-06 09:23:37 +02:00
else :
return None
else :
return None
def left_indent ( state ) :
return ( state [ " depth " ] - 1 ) * SECTION_INDENT
def right_indent ( state ) :
return ( state [ " depth " ] - 1 ) * SECTION_INDENT
def make_part ( state , part ) :
return ( make_style ( state ) , part )
def state_to_style ( state ) :
return { " fg " : state [ " fg_color " ] , " bg " : state [ " bg_color " ] , " bold " : state [ " formatting " ] [ " bold " ] , " underline " : state [ " formatting " ] [ " underline " ] , " italic " : state [ " formatting " ] [ " italic " ] }
def style_to_state ( style , state ) :
if style [ " fg " ] != None :
state [ " fg_color " ] = style [ " fg " ]
if style [ " bg " ] != None :
state [ " bg_color " ] = style [ " bg " ]
if style [ " bold " ] != None :
state [ " formatting " ] [ " bold " ] = style [ " bold " ]
if style [ " underline " ] != None :
state [ " formatting " ] [ " underline " ] = style [ " underline " ]
if style [ " italic " ] != None :
state [ " formatting " ] [ " italic " ] = style [ " italic " ]
def make_style ( state ) :
def mono_color ( fg , bg ) :
return " default "
2022-02-26 17:10:38 +01:00
2021-07-06 09:23:37 +02:00
def low_color ( color ) :
2022-02-26 17:10:38 +01:00
try :
result = " default "
if color == " default " :
result = " default "
elif len ( color ) == 6 :
r = str ( color [ 0 ] )
g = str ( color [ 2 ] )
b = str ( color [ 4 ] )
color = r + g + b
if len ( color ) == 3 :
t = 7
if color [ 0 ] == " g " :
val = int ( color [ 1 : 2 ] )
2025-12-29 14:58:29 +01:00
if val < 25 : result = " black "
elif val < 50 : result = " dark gray "
elif val < 75 : result = " light gray "
else : result = " white "
2022-02-26 17:10:38 +01:00
else :
r = int ( color [ 0 ] , 16 )
g = int ( color [ 1 ] , 16 )
b = int ( color [ 2 ] , 16 )
if r == g == b :
val = int ( color [ 0 ] , 16 ) * 6
2025-12-29 14:58:29 +01:00
if val < 12 : result = " black "
elif val < 50 : result = " dark gray "
elif val < 80 : result = " light gray "
else : result = " white "
2022-02-26 17:10:38 +01:00
else :
if r == b :
if r > g :
2025-12-29 14:58:29 +01:00
if r > t : result = " light magenta "
else : result = " dark magenta "
2022-02-26 17:10:38 +01:00
else :
2025-12-29 14:58:29 +01:00
if g > t : result = " light green "
else : result = " dark green "
2022-02-26 17:10:38 +01:00
if b == g :
if b > r :
2025-12-29 14:58:29 +01:00
if b > t : result = " light cyan "
else : result = " dark cyan "
2022-02-26 17:10:38 +01:00
else :
2025-12-29 14:58:29 +01:00
if r > t : result = " light red "
else : result = " dark red "
2022-02-26 17:10:38 +01:00
if g == r :
if g > b :
2025-12-29 14:58:29 +01:00
if g > t : result = " yellow "
else : result = " brown "
2022-02-26 17:10:38 +01:00
else :
2025-12-29 14:58:29 +01:00
if b > t : result = " light blue "
else : result = " dark blue "
2022-02-26 17:10:38 +01:00
if r > g and r > b :
2025-12-29 14:58:29 +01:00
if r > t : result = " light red "
else : result = " dark red "
2022-02-26 17:10:38 +01:00
if g > r and g > b :
2025-12-29 14:58:29 +01:00
if g > t : result = " light green "
else : result = " dark green "
2022-02-26 17:10:38 +01:00
if b > g and b > r :
2025-12-29 14:58:29 +01:00
if b > t : result = " light blue "
else : result = " dark blue "
2022-02-26 17:10:38 +01:00
except Exception as e :
result = " default "
return result
2021-07-06 09:23:37 +02:00
def high_color ( color ) :
2022-02-26 17:10:38 +01:00
def parseval_hex ( char ) :
return hex ( max ( 0 , min ( int ( char , 16 ) , 16 ) ) ) [ 2 : ]
def parseval_dec ( char ) :
return str ( max ( 0 , min ( int ( char ) , 9 ) ) )
2021-07-06 09:23:37 +02:00
if color == " default " :
2022-02-26 17:10:38 +01:00
return " default "
2021-07-06 09:23:37 +02:00
else :
2022-02-26 17:10:38 +01:00
if len ( color ) == 6 :
try :
v1 = parseval_hex ( color [ 0 ] )
v2 = parseval_hex ( color [ 1 ] )
v3 = parseval_hex ( color [ 2 ] )
v4 = parseval_hex ( color [ 3 ] )
v5 = parseval_hex ( color [ 4 ] )
v6 = parseval_hex ( color [ 5 ] )
color = " # " + v1 + v2 + v3 + v4 + v5 + v6
except Exception as e :
return " default "
return color
elif len ( color ) == 3 :
if color [ 0 ] == " g " :
try :
v1 = parseval_dec ( color [ 1 ] )
v2 = parseval_dec ( color [ 2 ] )
except Exception as e :
return " default "
return " g " + v1 + v2
else :
try :
v1 = parseval_hex ( color [ 0 ] )
v2 = parseval_hex ( color [ 1 ] )
v3 = parseval_hex ( color [ 2 ] )
color = v1 + v2 + v3
except Exception as e :
return " default "
r = color [ 0 ]
g = color [ 1 ]
b = color [ 2 ]
return " # " + r + r + g + g + b + b
2021-07-06 09:23:37 +02:00
bold = state [ " formatting " ] [ " bold " ]
underline = state [ " formatting " ] [ " underline " ]
italic = state [ " formatting " ] [ " italic " ]
fg = state [ " fg_color " ]
bg = state [ " bg_color " ]
format_string = " "
2025-12-29 14:58:29 +01:00
if bold : format_string + = " ,bold "
if underline : format_string + = " ,underline "
if italic : format_string + = " ,italics "
2021-07-06 09:23:37 +02:00
name = " micron_ " + fg + " _ " + bg + " _ " + format_string
if not name in SYNTH_STYLES :
screen = nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . screen
screen . register_palette_entry ( name , low_color ( fg ) + format_string , low_color ( bg ) , mono_color ( fg , bg ) + format_string , high_color ( fg ) + format_string , high_color ( bg ) )
2021-08-27 19:58:14 +02:00
synth_spec = screen . _palette [ name ]
2021-07-06 09:23:37 +02:00
SYNTH_STYLES . append ( name )
2021-08-27 19:58:14 +02:00
if not name in SYNTH_SPECS :
SYNTH_SPECS [ name ] = synth_spec
2021-07-06 09:23:37 +02:00
return name
2025-05-11 18:43:13 +02:00
def make_output ( state , line , url_delegate , pre_escape = False ) :
2021-07-06 09:23:37 +02:00
output = [ ]
if state [ " literal " ] :
if line == " \\ `= " :
line = " `= "
output . append ( make_part ( state , line ) )
else :
part = " "
mode = " text "
2025-05-11 18:43:13 +02:00
escape = pre_escape
2021-07-06 09:23:37 +02:00
skip = 0
2025-05-11 18:43:13 +02:00
2021-07-06 09:23:37 +02:00
for i in range ( 0 , len ( line ) ) :
c = line [ i ]
if skip > 0 :
skip - = 1
else :
if mode == " formatting " :
if c == " _ " :
state [ " formatting " ] [ " underline " ] ^ = True
elif c == " ! " :
state [ " formatting " ] [ " bold " ] ^ = True
elif c == " * " :
state [ " formatting " ] [ " italic " ] ^ = True
elif c == " F " :
if len ( line ) > = i + 4 :
2026-04-06 18:47:06 +02:00
if line [ i + 1 ] == " T " and len ( line ) > = i + 8 :
color = line [ i + 2 : i + 8 ]
state [ " fg_color " ] = color
skip = 7
else :
color = line [ i + 1 : i + 4 ]
state [ " fg_color " ] = color
skip = 3
2021-07-06 09:23:37 +02:00
elif c == " f " :
2025-05-11 20:59:26 +02:00
state [ " fg_color " ] = state [ " default_fg " ]
2021-07-06 09:23:37 +02:00
elif c == " B " :
if len ( line ) > = i + 4 :
2026-04-06 18:47:06 +02:00
if line [ i + 1 ] == " T " and len ( line ) > = i + 8 :
color = line [ i + 2 : i + 8 ]
state [ " bg_color " ] = color
skip = 7
else :
color = line [ i + 1 : i + 4 ]
state [ " bg_color " ] = color
skip = 3
2021-07-06 09:23:37 +02:00
elif c == " b " :
2025-05-11 20:59:26 +02:00
state [ " bg_color " ] = state [ " default_bg " ]
2021-07-06 09:23:37 +02:00
elif c == " ` " :
state [ " formatting " ] [ " bold " ] = False
state [ " formatting " ] [ " underline " ] = False
state [ " formatting " ] [ " italic " ] = False
2025-05-11 20:59:26 +02:00
state [ " fg_color " ] = state [ " default_fg " ]
state [ " bg_color " ] = state [ " default_bg " ]
2021-07-06 09:23:37 +02:00
state [ " align " ] = state [ " default_align " ]
elif c == " c " :
2025-12-22 12:51:31 +01:00
if state [ " align " ] != " center " : state [ " align " ] = " center "
2021-07-06 09:23:37 +02:00
elif c == " l " :
2025-12-22 12:51:31 +01:00
if state [ " align " ] != " left " : state [ " align " ] = " left "
2021-07-06 09:23:37 +02:00
elif c == " r " :
2025-12-22 12:51:31 +01:00
if state [ " align " ] != " right " : state [ " align " ] = " right "
2021-07-06 09:23:37 +02:00
elif c == " a " :
state [ " align " ] = state [ " default_align " ]
2026-05-20 16:16:24 -04:00
elif c == " : " :
# Anchor declaration: `:anchor-name (terminated by any non-name char). The anchor is a zero width position narker bound to the current line by markup_to_attrmaps
name_start = i + 1
name_end = name_start
while name_end < len ( line ) and ( line [ name_end ] . isalnum ( ) or line [ name_end ] in " _- " ) :
name_end + = 1
anchor_name = line [ name_start : name_end ]
if anchor_name :
state . setdefault ( " pending_anchors " , [ ] ) . append ( anchor_name )
skip = ( name_end - i ) - 1
if skip < 0 : skip = 0
2024-11-25 18:15:42 -05:00
elif c == ' < ' :
if len ( part ) > 0 :
output . append ( make_part ( state , part ) )
part = " "
2023-02-14 19:46:21 +01:00
try :
2024-11-25 18:15:42 -05:00
field_start = i + 1 # position after '<'
backtick_pos = line . find ( ' ` ' , field_start )
if backtick_pos == - 1 :
pass # No '`', invalid field
2023-02-09 21:18:14 +01:00
else :
2024-11-25 18:15:42 -05:00
field_content = line [ field_start : backtick_pos ]
2023-02-14 19:46:21 +01:00
field_masked = False
field_width = 24
2026-06-08 05:00:46 -04:00
field_rows = 1
2024-11-25 18:15:42 -05:00
field_type = " field "
field_name = field_content
field_value = " "
field_data = " "
2026-06-08 05:00:46 -04:00
field_prechecked = False
2024-11-25 18:15:42 -05:00
# check if field_content contains '|'
if ' | ' in field_content :
f_components = field_content . split ( ' | ' )
2023-02-14 19:46:21 +01:00
field_flags = f_components [ 0 ]
field_name = f_components [ 1 ]
2024-11-25 18:15:42 -05:00
# handle field type indicators
if ' ^ ' in field_flags :
field_type = " radio "
field_flags = field_flags . replace ( " ^ " , " " )
elif ' ? ' in field_flags :
field_type = " checkbox "
field_flags = field_flags . replace ( " ? " , " " )
elif ' ! ' in field_flags :
2023-02-14 19:46:21 +01:00
field_flags = field_flags . replace ( " ! " , " " )
field_masked = True
2024-11-25 18:15:42 -05:00
# Handle field width
2023-02-14 19:46:21 +01:00
if len ( field_flags ) > 0 :
2026-06-08 05:00:46 -04:00
width_flag , _ , rows_flag = field_flags . partition ( " x " )
2024-11-25 18:15:42 -05:00
try :
2026-06-08 05:00:46 -04:00
field_width = min ( int ( width_flag ) , 256 )
2024-11-25 18:15:42 -05:00
except ValueError :
pass # Ignore invalid width
2026-06-08 05:00:46 -04:00
if rows_flag :
try :
field_rows = max ( 1 , min ( int ( rows_flag ) , 256 ) )
except ValueError :
pass
2024-11-25 18:15:42 -05:00
2024-11-26 19:58:53 -05:00
# Check for value and pre-checked flag
2024-11-25 18:15:42 -05:00
if len ( f_components ) > 2 :
field_value = f_components [ 2 ]
2024-11-26 19:58:53 -05:00
else :
field_value = " "
if len ( f_components ) > 3 :
if f_components [ 3 ] == ' * ' :
field_prechecked = True
2023-02-14 19:46:21 +01:00
else :
2024-11-26 19:58:53 -05:00
# No '|', so field_name is field_content
2024-11-25 18:15:42 -05:00
field_name = field_content
2024-11-26 19:58:53 -05:00
field_type = " field "
field_masked = False
field_width = 24
field_value = " "
field_prechecked = False
2024-11-25 18:15:42 -05:00
# Find the closing '>' character
field_end = line . find ( ' > ' , backtick_pos )
if field_end == - 1 :
pass # No closing '>', invalid field
else :
field_data = line [ backtick_pos + 1 : field_end ]
# Now, we have all field data
if field_type in [ " checkbox " , " radio " ] :
# for checkboxes and radios, field_data is the label
output . append ( {
" type " : field_type ,
" name " : field_name ,
" value " : field_value if field_value else field_data ,
" label " : field_data ,
2024-11-26 19:58:53 -05:00
" prechecked " : field_prechecked ,
2024-11-25 18:15:42 -05:00
" style " : make_style ( state )
} )
else :
# For text fields field_data is the initial text
output . append ( {
" type " : " field " ,
" name " : field_name ,
" width " : field_width ,
2026-06-08 05:00:46 -04:00
" rows " : field_rows ,
2024-11-25 18:15:42 -05:00
" masked " : field_masked ,
" data " : field_data ,
" style " : make_style ( state )
} )
skip = field_end - i
2023-02-14 19:46:21 +01:00
except Exception as e :
pass
2024-11-25 18:15:42 -05:00
2021-08-27 19:58:14 +02:00
elif c == " [ " :
endpos = line [ i : ] . find ( " ] " )
if endpos == - 1 :
pass
else :
link_data = line [ i + 1 : i + endpos ]
skip = endpos
link_components = link_data . split ( " ` " )
if len ( link_components ) == 1 :
link_label = " "
2023-02-14 19:46:21 +01:00
link_fields = " "
2021-08-27 19:58:14 +02:00
link_url = link_data
elif len ( link_components ) == 2 :
link_label = link_components [ 0 ]
link_url = link_components [ 1 ]
2023-02-14 19:46:21 +01:00
link_fields = " "
elif len ( link_components ) == 3 :
link_label = link_components [ 0 ]
link_url = link_components [ 1 ]
link_fields = link_components [ 2 ]
2021-08-27 19:58:14 +02:00
else :
link_url = " "
link_label = " "
2023-02-14 19:46:21 +01:00
link_fields = " "
2021-08-27 19:58:14 +02:00
if len ( link_url ) != 0 :
if link_label == " " :
link_label = link_url
# First generate output until now
if len ( part ) > 0 :
output . append ( make_part ( state , part ) )
cm = nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . colormode
specname = make_style ( state )
speclist = SYNTH_SPECS [ specname ]
if cm == 1 :
orig_spec = speclist [ 0 ]
elif cm == 16 :
orig_spec = speclist [ 1 ]
elif cm == 88 :
orig_spec = speclist [ 2 ]
elif cm == 256 :
orig_spec = speclist [ 3 ]
elif cm == 2 * * 24 :
orig_spec = speclist [ 4 ]
if url_delegate != None :
2025-12-22 12:42:32 +01:00
linkspec = LinkSpec ( link_url , orig_spec , cm = cm )
2023-02-14 19:46:21 +01:00
if link_fields != " " :
2023-02-15 10:09:25 +01:00
lf = link_fields . split ( " | " )
if len ( lf ) > 0 :
linkspec . link_fields = lf
2023-02-14 19:46:21 +01:00
2021-08-27 19:58:14 +02:00
output . append ( ( linkspec , link_label ) )
else :
2025-12-29 19:47:08 +01:00
output . append ( make_part ( state , link_label ) )
2021-08-27 19:58:14 +02:00
2021-07-06 09:23:37 +02:00
mode = " text "
if len ( part ) > 0 :
output . append ( make_part ( state , part ) )
elif mode == " text " :
if c == " \\ " :
2022-10-20 21:12:16 +02:00
if escape :
part + = c
escape = False
else :
escape = True
2021-07-06 09:23:37 +02:00
elif c == " ` " :
if escape :
part + = c
escape = False
else :
mode = " formatting "
if len ( part ) > 0 :
output . append ( make_part ( state , part ) )
part = " "
else :
part + = c
2025-05-11 18:43:13 +02:00
escape = False
2021-07-06 09:23:37 +02:00
if i == len ( line ) - 1 :
if len ( part ) > 0 :
output . append ( make_part ( state , part ) )
if len ( output ) > 0 :
return output
else :
2021-08-27 19:58:14 +02:00
return None
class LinkSpec ( urwid . AttrSpec ) :
2025-12-22 12:42:32 +01:00
def __init__ ( self , link_target , orig_spec , cm = 256 ) :
2021-08-27 19:58:14 +02:00
self . link_target = link_target
2023-02-14 19:46:21 +01:00
self . link_fields = None
2021-08-27 19:58:14 +02:00
2025-12-22 12:42:32 +01:00
super ( ) . __init__ ( orig_spec . foreground , orig_spec . background , colors = cm )
2021-08-27 19:58:14 +02:00
class LinkableText ( urwid . Text ) :
ignore_focus = False
_selectable = True
signals = [ " click " , " change " ]
def __init__ ( self , text , align = None , cursor_position = 0 , delegate = None ) :
2024-01-18 10:22:38 +01:00
super ( ) . __init__ ( text , align = align )
2021-08-27 19:58:14 +02:00
self . delegate = delegate
self . _cursor_position = 0
2026-09-09 19:32:41 +02:00
self . _cursor_promised = None
self . _cursor_promised_at = 0
2026-05-06 14:18:58 +02:00
self . key_timeout = 2
2023-02-15 11:53:34 +01:00
self . in_columns = False
2021-08-27 19:58:14 +02:00
if self . delegate != None :
self . delegate . last_keypress = 0
2023-02-14 19:46:21 +01:00
def handle_link ( self , link_target , link_fields ) :
2021-08-27 19:58:14 +02:00
if self . delegate != None :
2023-02-14 19:46:21 +01:00
self . delegate . handle_link ( link_target , link_fields )
2021-08-27 19:58:14 +02:00
def find_next_part_pos ( self , pos , part_positions ) :
for position in part_positions :
if position > pos :
return position
return pos
def find_prev_part_pos ( self , pos , part_positions ) :
nextpos = pos
for position in part_positions :
if position < pos :
nextpos = position
return nextpos
def find_item_at_pos ( self , pos ) :
total = 0
text , parts = self . get_text ( )
for i , info in enumerate ( parts ) :
style , length = info
if total < = pos < length + total :
return style
total + = length
return None
2021-09-11 15:39:46 +02:00
def peek_link ( self ) :
item = self . find_item_at_pos ( self . _cursor_position )
if item != None :
if isinstance ( item , LinkSpec ) :
if self . delegate != None :
2026-05-02 02:01:26 +02:00
self . delegate . marked_link ( item . link_target , item . link_fields )
2021-09-11 15:39:46 +02:00
else :
if self . delegate != None :
self . delegate . marked_link ( None )
2021-08-27 19:58:14 +02:00
def keypress ( self , size , key ) :
part_positions = [ 0 ]
parts = [ ]
total = 0
text , parts = self . get_text ( )
for i , info in enumerate ( parts ) :
style_name , length = info
part_positions . append ( length + total )
total + = length
if self . delegate != None :
self . delegate . last_keypress = time . time ( )
self . _invalidate ( )
nomadnet . NomadNetworkApp . get_shared_instance ( ) . ui . loop . set_alarm_in ( self . key_timeout , self . kt_event )
if self . _command_map [ key ] == urwid . ACTIVATE :
item = self . find_item_at_pos ( self . _cursor_position )
if item != None :
if isinstance ( item , LinkSpec ) :
2023-02-14 19:46:21 +01:00
self . handle_link ( item . link_target , item . link_fields )
2021-08-27 19:58:14 +02:00
elif key == " up " :
self . _cursor_position = 0
return key
elif key == " down " :
self . _cursor_position = 0
return key
elif key == " right " :
2022-05-17 18:37:29 +02:00
old = self . _cursor_position
2021-08-27 19:58:14 +02:00
self . _cursor_position = self . find_next_part_pos ( self . _cursor_position , part_positions )
2023-02-15 11:53:34 +01:00
2022-05-17 18:37:29 +02:00
if self . _cursor_position == old :
2023-02-15 11:53:34 +01:00
if self . in_columns :
return " right "
else :
self . _cursor_position = 0
return " down "
2022-05-17 18:37:29 +02:00
2021-08-27 19:58:14 +02:00
self . _invalidate ( )
elif key == " left " :
if self . _cursor_position > 0 :
2023-02-15 11:53:34 +01:00
if self . in_columns :
return " left "
else :
self . _cursor_position = self . find_prev_part_pos ( self . _cursor_position , part_positions )
self . _invalidate ( )
2021-08-27 19:58:14 +02:00
else :
if self . delegate != None :
self . delegate . micron_released_focus ( )
else :
return key
def kt_event ( self , loop , user_data ) :
self . _invalidate ( )
2026-09-09 19:32:41 +02:00
def cursor_visible ( self ) :
return self . delegate == None or time . time ( ) < self . delegate . last_keypress + self . key_timeout
2021-08-27 19:58:14 +02:00
def render ( self , size , focus = False ) :
2024-01-18 10:22:38 +01:00
c = super ( ) . render ( size , focus )
2026-09-09 19:32:41 +02:00
if focus :
if time . time ( ) < self . _cursor_promised_at + 0.5 :
coords = self . _cursor_promised
else :
coords = self . get_cursor_coords ( size )
if coords != None :
c = urwid . CompositeCanvas ( c )
c . cursor = coords
if self . delegate != None :
self . peek_link ( )
2021-08-27 19:58:14 +02:00
return c
def get_cursor_coords ( self , size ) :
2026-09-09 19:32:41 +02:00
self . _cursor_promised_at = time . time ( )
self . _cursor_promised = None
if not self . cursor_visible ( ) or self . _cursor_position > len ( self . text ) :
return None
( maxcol , ) = size
trans = self . get_line_translation ( maxcol )
x , y = calc_coords ( self . text , trans , self . _cursor_position )
if maxcol < = x :
return None
self . _cursor_promised = ( x , y )
return x , y
2021-08-27 19:58:14 +02:00
def mouse_event ( self , size , event , button , x , y , focus ) :
2023-08-14 18:02:39 +02:00
try :
if button != 1 or not is_mouse_press ( event ) :
return False
else :
( maxcol , ) = size
translation = self . get_line_translation ( maxcol )
line_offset = 0
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
if self . align == " center " :
line_offset = translation [ y ] [ 1 ] [ 1 ] - translation [ y ] [ 0 ] [ 0 ]
if x < translation [ y ] [ 0 ] [ 0 ] :
x = translation [ y ] [ 0 ] [ 0 ]
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
if x > translation [ y ] [ 1 ] [ 0 ] + translation [ y ] [ 0 ] [ 0 ] :
x = translation [ y ] [ 1 ] [ 0 ] + translation [ y ] [ 0 ] [ 0 ]
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
elif self . align == " right " :
line_offset = translation [ y ] [ 1 ] [ 1 ] - translation [ y ] [ 0 ] [ 0 ]
if x < translation [ y ] [ 0 ] [ 0 ] :
x = translation [ y ] [ 0 ] [ 0 ]
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
else :
line_offset = translation [ y ] [ 0 ] [ 1 ]
if x > translation [ y ] [ 0 ] [ 0 ] :
x = translation [ y ] [ 0 ] [ 0 ]
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
pos = line_offset + x
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
self . _cursor_position = pos
item = self . find_item_at_pos ( self . _cursor_position )
2022-05-17 18:37:29 +02:00
2023-08-14 18:02:39 +02:00
if item != None :
if isinstance ( item , LinkSpec ) :
self . handle_link ( item . link_target , item . link_fields )
2021-08-27 19:58:14 +02:00
2023-08-14 18:02:39 +02:00
self . _invalidate ( )
self . _emit ( " change " )
return True
2021-08-27 19:58:14 +02:00
2023-08-14 18:02:39 +02:00
except Exception as e :
2026-05-19 18:15:40 +02:00
return False
LINK_CLASS = LinkableText