mirror of
https://github.com/markqvist/NomadNet.git
synced 2024-12-24 06:39:34 -05:00
Initial work on micron markup language. Work on guide section.
This commit is contained in:
parent
84e616a3ec
commit
72f623293e
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
214
nomadnet/ui/textui/Guide.py
Normal 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,
|
||||
}
|
@ -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
|
||||
|
||||
|
204
nomadnet/ui/textui/MarkupParser.py
Normal file
204
nomadnet/ui/textui/MarkupParser.py
Normal 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
|
Loading…
Reference in New Issue
Block a user