diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 3204b26..31bf540 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -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 diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py index 63b1689..012f87d 100644 --- a/nomadnet/ui/TextUI.py +++ b/nomadnet/ui/TextUI.py @@ -15,31 +15,34 @@ THEME_DARK = 0x01 THEME_LIGHT = 0x02 THEMES = { - THEME_DARK: [ - # 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'), - ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'), - ('body_text', 'white', 'default', 'default', '#ddd', 'default'), - ('error_text', 'dark red', 'default', 'default', 'dark red', 'default'), - ('warning_text', 'yellow', 'default', 'default', '#ba4', 'default'), - ('inactive_text', 'dark gray', 'default', 'default', 'dark gray', 'default'), - ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'), - ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'), - ("msg_header_ok", 'black', 'light green', 'standout', '#111', '#6b2'), - ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'), - ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'), - ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'), - ("msg_header_failed", 'black', 'dark gray', 'standout', 'black', 'dark gray'), - ("msg_warning_untrusted", 'black', 'dark red', 'standout', '#111', 'dark red'), - ("list_focus", "black", "light gray", "standout", "#111", "#bbb"), - ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"), - ("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_untrusted", "dark red", "default", "default", "dark red", "default"), - ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#bbb"), - ] + 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'), + ('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'), + ('body_text', 'white', 'default', 'default', '#ddd', 'default'), + ('error_text', 'dark red', 'default', 'default', 'dark red', 'default'), + ('warning_text', 'yellow', 'default', 'default', '#ba4', 'default'), + ('inactive_text', 'dark gray', 'default', 'default', 'dark gray', 'default'), + ('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'), + ('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'), + ("msg_header_ok", 'black', 'light green', 'standout', '#111', '#6b2'), + ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'), + ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'), + ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'), + ("msg_header_failed", 'black', 'dark gray', 'standout', 'black', 'dark gray'), + ("msg_warning_untrusted", 'black', 'dark red', 'standout', '#111', 'dark red'), + ("list_focus", "black", "light gray", "standout", "#111", "#bbb"), + ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"), + ("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 = { @@ -81,12 +84,15 @@ class TextUI: urwid.set_encoding("UTF-8") - intro_timeout = self.app.config["textui"]["intro_time"] - colormode = self.app.config["textui"]["colormode"] - theme = self.app.config["textui"]["theme"] - mouse_enabled = self.app.config["textui"]["mouse_enabled"] + intro_timeout = self.app.config["textui"]["intro_time"] + colormode = self.app.config["textui"]["colormode"] + 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) diff --git a/nomadnet/ui/textui/Config.py b/nomadnet/ui/textui/Config.py index c2b75cd..6a0ec1f 100644 --- a/nomadnet/ui/textui/Config.py +++ b/nomadnet/ui/textui/Config.py @@ -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"), ]) diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py new file mode 100644 index 0000000..893a84a --- /dev/null +++ b/nomadnet/ui/textui/Guide.py @@ -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, +} \ No newline at end of file diff --git a/nomadnet/ui/textui/Main.py b/nomadnet/ui/textui/Main.py index de0fff7..8a6956a 100644 --- a/nomadnet/ui/textui/Main.py +++ b/nomadnet/ui/textui/Main.py @@ -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] - buttons = [menu_text, button_conversations, button_network, button_log, button_config, button_quit] + 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 diff --git a/nomadnet/ui/textui/MarkupParser.py b/nomadnet/ui/textui/MarkupParser.py new file mode 100644 index 0000000..9aeec63 --- /dev/null +++ b/nomadnet/ui/textui/MarkupParser.py @@ -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 \ No newline at end of file