Initial work on micron markup language. Work on guide section.

This commit is contained in:
Mark Qvist 2021-07-05 01:15:02 +02:00
parent 84e616a3ec
commit 72f623293e
6 changed files with 478 additions and 34 deletions

View File

@ -252,6 +252,11 @@ class NomadNetworkApp:
else:
self.config["textui"]["mouse_enabled"] = self.config["textui"].as_bool("mouse_enabled")
if not "hide_guide" in self.config["textui"]:
self.config["textui"]["hide_guide"] = False
else:
self.config["textui"]["hide_guide"] = self.config["textui"].as_bool("hide_guide")
if not "animation_interval" in self.config["textui"]:
self.config["textui"]["animation_interval"] = 1
else:
@ -373,6 +378,11 @@ mouse_enabled = True
# alias will be used.
editor = editor
# If you don't want the Guide section to
# show up in the menu, you can disable it.
hide_guide = no
[node]
enable_node = no

View File

@ -15,7 +15,8 @@ THEME_DARK = 0x01
THEME_LIGHT = 0x02
THEMES = {
THEME_DARK: [
THEME_DARK: {
"urwid_theme": [
# Style name # 16-color style # Monochrome style # 88, 256 and true-color style
('heading', 'light gray,underline', 'default', 'underline', 'g93,underline', 'default'),
('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'),
@ -37,9 +38,11 @@ THEMES = {
("list_trusted", "light green", "default", "default", "#6b2", "default"),
("list_focus_trusted", "black", "light gray", "standout", "#180", "#bbb"),
("list_unknown", "dark gray", "default", "default", "light gray", "default"),
("list_normal", "dark gray", "default", "default", "light gray", "default"),
("list_untrusted", "dark red", "default", "default", "dark red", "default"),
("list_focus_untrusted", "black", "light gray", "standout", "#810", "#bbb"),
]
],
}
}
GLYPHSETS = {
@ -86,7 +89,10 @@ class TextUI:
theme = self.app.config["textui"]["theme"]
mouse_enabled = self.app.config["textui"]["mouse_enabled"]
palette = THEMES[theme]
self.palette = THEMES[theme]["urwid_theme"]
for entry in nomadnet.ui.textui.MarkupParser.URWID_THEME:
self.palette.append(entry)
if self.app.config["textui"]["glyphs"] == "plain":
glyphset = "plain"
@ -101,9 +107,8 @@ class TextUI:
for glyph in GLYPHS:
self.glyphs[glyph[0]] = glyph[GLYPHSETS[glyphset]]
self.screen = urwid.raw_display.Screen()
self.screen.register_palette(palette)
self.screen.register_palette(self.palette)
self.main_display = Main.MainDisplay(self, self.app)

View File

@ -34,7 +34,7 @@ class ConfigDisplay():
self.editor_term.term.change_focus(True)
pile = urwid.Pile([
urwid.Text(("body_text", "\nTo change the configuration, edit the config file located at:\n\n"+self.app.configpath+"\n\nRestart Nomad Network for chanes to take effect\n"), align="center"),
urwid.Text(("body_text", "\nTo change the configuration, edit the config file located at:\n\n"+self.app.configpath+"\n\nRestart Nomad Network for changes to take effect\n"), align="center"),
urwid.Padding(urwid.Button("Open Editor", on_press=open_editor), width=15, align="center"),
])

214
nomadnet/ui/textui/Guide.py Normal file
View File

@ -0,0 +1,214 @@
import RNS
import urwid
import nomadnet
from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY
from .MarkupParser import markup_to_attrmaps
class GuideDisplayShortcuts():
def __init__(self, app):
self.app = app
g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Text(""), "shortcutbar")
class ListEntry(urwid.Text):
_selectable = True
signals = ["click"]
def keypress(self, size, key):
"""
Send 'click' signal on 'activate' command.
"""
if self._command_map[key] != urwid.ACTIVATE:
return key
self._emit('click')
def mouse_event(self, size, event, button, x, y, focus):
"""
Send 'click' signal on button 1 press.
"""
if button != 1 or not urwid.util.is_mouse_press(event):
return False
self._emit('click')
return True
class SelectText(urwid.Text):
_selectable = True
signals = ["click"]
def keypress(self, size, key):
"""
Send 'click' signal on 'activate' command.
"""
if self._command_map[key] != urwid.ACTIVATE:
return key
self._emit('click')
def mouse_event(self, size, event, button, x, y, focus):
"""
Send 'click' signal on button 1 press.
"""
if button != 1 or not urwid.util.is_mouse_press(event):
return False
self._emit('click')
return True
class GuideEntry(urwid.WidgetWrap):
def __init__(self, app, reader, topic_name):
self.app = app
self.reader = reader
g = self.app.ui.glyphs
widget = ListEntry(topic_name)
urwid.connect_signal(widget, "click", self.display_topic, topic_name)
style = "list_normal"
focus_style = "list_focus"
self.display_widget = urwid.AttrMap(widget, style, focus_style)
urwid.WidgetWrap.__init__(self, self.display_widget)
def display_topic(self, event, topic):
markup = TOPICS[topic]
attrmaps = markup_to_attrmaps(markup)
self.reader.set_content_widgets(attrmaps)
class TopicList(urwid.WidgetWrap):
def __init__(self, app, guide_display):
self.app = app
g = self.app.ui.glyphs
self.topic_list = [
GuideEntry(self.app, guide_display, "Introduction"),
GuideEntry(self.app, guide_display, "Conversations"),
GuideEntry(self.app, guide_display, "Markup"),
GuideEntry(self.app, guide_display, "Licenses & Credits"),
]
self.ilb = IndicativeListBox(
self.topic_list,
initialization_is_selection_change=False,
)
urwid.WidgetWrap.__init__(self, urwid.LineBox(self.ilb, title="Topics"))
def keypress(self, size, key):
if key == "up" and (self.ilb.first_item_is_selected()):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header")
return super(TopicList, self).keypress(size, key)
class GuideDisplay():
list_width = 0.33
def __init__(self, app):
self.app = app
g = self.app.ui.glyphs
topic_text = urwid.Text("\nNo topic selected", align="left")
self.left_area = TopicList(self.app, self)
self.right_area = urwid.LineBox(urwid.Filler(topic_text, "top"))
self.columns = urwid.Columns(
[
("weight", GuideDisplay.list_width, self.left_area),
("weight", 1-GuideDisplay.list_width, self.right_area)
],
dividechars=0, focus_column=0
)
self.shortcuts_display = GuideDisplayShortcuts(self.app)
self.widget = self.columns
def set_content_widgets(self, new_content):
options = self.columns.options(width_type="weight", width_amount=1-GuideDisplay.list_width)
pile = urwid.Pile(new_content)
content = urwid.LineBox(urwid.Filler(pile, "top"))
self.columns.contents[1] = (content, options)
def shortcuts(self):
return self.shortcuts_display
TOPIC_INTRODUCTION = '''>Nomad Network
Communicate Freely.
Nomad Network is built using Reticulum
-~
## Notable Features
- Encrypted messaging over packet-radio, LoRa, WiFi or anything else [Reticulum](https://github.com/markqvist/Reticulum) supports.
- Zero-configuration, minimal-infrastructure mesh communication
-
## Current Status
Pre-alpha. At this point Nomad Network is usable as a basic messaging client over Reticulum networks, but only the very core features have been implemented. Development is ongoing and current features being implemented are:
- Propagated messaging and discussion threads
- Connectable nodes that can host pages, files and other resources
- Collaborative information sharing and spatial map-style "wikis"
-
## Dependencies:
- Python 3
- RNS
- LXMF
```
To use Nomad Network on packet radio or LoRa, you will need to configure your Reticulum installation
to use any relevant packet radio TNCs or LoRa devices on your system. See the Reticulum documentation
for info.
## Caveat Emptor
Nomad Network is experimental software, and should be considered as such. While it has been built wit
h cryptography best-practices very foremost in mind, it _has not_ been externally security audited, a
nd there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit,
please do get in touch.
'''
TOPIC_CONVERSATIONS = '''Conversations
=============
Conversations in Nomad Network
'''
TOPIC_MARKUP = '''>Markup
Nomad Network supports a simple and functional markup language called micron. It has a lean markup structure that adds very little overhead, and is still readable as plain text, but offers basic formatting and text structuring, ideal for displaying in a terminal.
Lorem ipsum dolor sit amet.
>>Encoding
`F222`BdddAll uM source files are encoded as UTF-8, and clients supporting uM display should support UTF-8.
``
>>>Sections and `F900Headings`f
You can define an arbitrary number of sections and sub-sections, each with their own heading
-
Dividers inside section will adhere to section indents
>>>>
If no heading text is defined, the section will appear as a sub-section without a header.
<-
Horizontal dividers can be inserted
Text `F2cccan`f be `_underlined`_, `!bold`! or `*italic`*. You `F000`B2cccan`b`f also `_`*`!combine formatting``!
'''
TOPICS = {
"Introduction": TOPIC_INTRODUCTION,
"Conversations": TOPIC_CONVERSATIONS,
"Markup": TOPIC_MARKUP,
}

View File

@ -7,6 +7,7 @@ from .Directory import *
from .Config import *
from .Map import *
from .Log import *
from .Guide import *
import urwid
class SubDisplays():
@ -18,6 +19,7 @@ class SubDisplays():
self.config_display = ConfigDisplay(self.app)
self.map_display = MapDisplay(self.app)
self.log_display = LogDisplay(self.app)
self.guide_display = GuideDisplay(self.app)
self.active_display = self.conversations_display
@ -113,6 +115,10 @@ class MainDisplay():
self.sub_displays.active_display = self.sub_displays.log_display
self.update_active_sub_display()
def show_guide(self, user_data):
self.sub_displays.active_display = self.sub_displays.guide_display
self.update_active_sub_display()
def update_active_sub_display(self):
self.frame.contents["body"] = (self.sub_displays.active().widget, None)
self.update_active_shortcuts()
@ -149,10 +155,15 @@ class MenuDisplay():
button_map = (7, MenuButton("Map", on_press=handler.show_map))
button_log = (7, MenuButton("Log", on_press=handler.show_log))
button_config = (10, MenuButton("Config", on_press=handler.show_config))
button_guide = (9, MenuButton("Guide", on_press=handler.show_guide))
button_quit = (8, MenuButton("Quit", on_press=handler.quit))
# buttons = [menu_text, button_conversations, button_node, button_directory, button_map]
if self.app.config["textui"]["hide_guide"]:
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit]
else:
buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_guide, button_quit]
columns = MenuColumns(buttons, dividechars=1)
columns.handler = handler

View File

@ -0,0 +1,204 @@
import nomadnet
import urwid
import re
URWID_THEME = [
# Style name 16-color style Monochrome style # 88, 256 and true-color style
('plain', 'light gray', 'default', 'default', '#ddd', 'default'),
('heading1', 'black', 'light gray', 'standout', '#222', '#bbb'),
('heading2', 'black', 'light gray', 'standout', '#111', '#999'),
('heading3', 'black', 'light gray', 'standout', '#000', '#777'),
('f_underline', 'default,underline', 'default', 'default,underline', 'default,underline', 'default'),
('f_bold', 'default,bold', 'default', 'default,bold', 'default,bold', 'default'),
('f_italic', 'default,italics', 'default', 'default,italics', 'default,italics', 'default'),
]
SYNTH_STYLES = []
SECTION_INDENT = 2
INDENT_RIGHT = 1
def markup_to_attrmaps(markup):
attrmaps = []
global_style = ""
state = {
"depth": 0,
"fg_color": "default",
"bg_color": "default",
"formatting": {
"bold": False,
"underline": False,
"italic": False,
"strikethrough": False,
"blink": False,
}
}
# Split entire document into lines for
# processing.
lines = markup.split("\n");
for line in lines:
if len(line) > 0:
display_widget = parse_line(line, state)
else:
display_widget = urwid.Text("")
if global_style == "":
global_style = "plain"
if display_widget != None:
attrmap = urwid.AttrMap(display_widget, global_style)
attrmaps.append(attrmap)
return attrmaps
def parse_line(line, state):
first_char = line[0]
# Check if the command is an escape
if first_char == "\\":
line = line[1:]
# Check for section heading reset
elif first_char == "<":
state["depth"] = 0
return parse_line(line[1:], state)
# Check for section headings
elif first_char == ">":
i = 0
while i < len(line) and line[i] == ">":
i += 1
state["depth"] = i
for j in range(1, i+1):
wanted_style = "heading"+str(i)
if any(s[0]==wanted_style for s in URWID_THEME):
style = wanted_style
line = line[state["depth"]:]
if len(line) > 0:
line = " "*left_indent(state)+line
return urwid.AttrMap(urwid.Text(line), style)
else:
return None
# Check for horizontal dividers
elif first_char == "-":
if len(line) == 2:
divider_char = line[1]
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))
output = make_output(state, line)
if state["depth"] == 0:
return urwid.Text(output)
else:
return urwid.Padding(urwid.Text(output), left=left_indent(state), right=right_indent(state))
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 make_style(state):
def mono_color(fg, bg):
return "default"
def low_color(color):
# TODO: Implement
return "default"
def high_color(color):
if color == "default":
return color
else:
return "#"+color
bold = state["formatting"]["bold"]
underline = state["formatting"]["underline"]
italic = state["formatting"]["italic"]
fg = state["fg_color"]
bg = state["bg_color"]
format_string = ""
if bold:
format_string += ",bold"
if underline:
format_string += ",underline"
if italic:
format_string += ",italics"
name = ""+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))
SYNTH_STYLES.append(name)
return name
def make_output(state, line):
output = []
part = ""
mode = "text"
skip = 0
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:
color = line[i+1:i+4]
state["fg_color"] = color
skip = 3
elif c == "f":
state["fg_color"] = "default"
elif c == "B":
if len(line) > i+4:
color = line[i+1:i+4]
state["bg_color"] = color
skip = 3
elif c == "b":
state["bg_color"] = "default"
elif c == "`":
state["formatting"]["bold"] = False
state["formatting"]["underline"] = False
state["formatting"]["italic"] = False
state["fg_color"] = "default"
state["bg_color"] = "default"
mode = "text"
if len(part) > 0:
output.append(make_part(state, part))
elif mode == "text":
if c == "`":
mode = "formatting"
if len(part) > 0:
output.append(make_part(state, part))
part = ""
else:
part += c
if i == len(line)-1:
output.append(make_part(state, part))
return output