From 6fc4e7afc9a8bfcdff89ec33acfdebfec7a6bfca Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Wed, 2 Sep 2026 08:38:44 -0400 Subject: [PATCH] The community has a page, and the page is its invite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fourth About page, `Join Discord`, sitting between the author and the ask: what is this, who made it, where is everyone, how do I help. Like its three siblings it is written markdown under assets/pages, reads nothing and transmits nothing, and keeps a CLI face — `meshterm discord`. It is short on purpose. The invite link, and the code for it in a `qr` fence, because on a PicoCalc there is no clipboard and the code is how a link leaves the screen at all. Co-Authored-By: Claude Opus 5 (1M context) --- meshterm/assets/pages/discord.md | 9 +++++++++ meshterm/tools/about.py | 33 +++++++++++++++++++++++++------- meshterm/ui/about.py | 12 +++++++++++- tests/test_about.py | 20 ++++++++++++++----- tests/test_gallery.py | 13 ++++++++++++- tests/test_menus.py | 2 +- 6 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 meshterm/assets/pages/discord.md diff --git a/meshterm/assets/pages/discord.md b/meshterm/assets/pages/discord.md new file mode 100644 index 0000000..dbacb0f --- /dev/null +++ b/meshterm/assets/pages/discord.md @@ -0,0 +1,9 @@ +## Join the Discord + +Questions, ideas, bug reports, and mesh talk. + +- *https://discord.gg/AZwe5Uvb3S* + +```qr +https://discord.gg/AZwe5Uvb3S +``` diff --git a/meshterm/tools/about.py b/meshterm/tools/about.py index b7947db..80ad370 100644 --- a/meshterm/tools/about.py +++ b/meshterm/tools/about.py @@ -1,14 +1,15 @@ -"""The About MeshTerm tools: the three pages under the menu's last section. +"""The About MeshTerm tools: the four pages under the menu's last section. -*About MeshTerm*, *About the author*, *Support MeshTerm* — in that order, which is -the order a stranger asks the questions in: what is this, who made it, how do I help. +*About MeshTerm*, *About the author*, *Join Discord*, *Support MeshTerm* — in that +order, which is the order a stranger asks the questions in: what is this, who made it, +where is everyone, how do I help. Each is a page rather than a feature: it reads nothing, transmits nothing, and needs no device, so it opens straight to its screen (see :mod:`meshterm.ui.about`) instead of prompting for anything first. Each also keeps a CLI face — ``meshterm about``, ``meshterm about-author``, -``meshterm support`` — printing the same page to the terminal, so the answers are -reachable from a shell without launching the full-screen session. +``meshterm discord``, ``meshterm support`` — printing the same page to the terminal, so +the answers are reachable from a shell without launching the full-screen session. The pages themselves are written in markdown under ``meshterm/assets/pages`` (see :mod:`meshterm.ui.about`); nothing here knows what any of them say. @@ -26,7 +27,7 @@ if TYPE_CHECKING: class _AboutTool(Tool): - """Shared behaviour for the three About pages: open the screen, or print the page. + """Shared behaviour for the four About pages: open the screen, or print the page. The pages differ only in their title, icon, and content, so everything else — the menu-only screen open, the printing CLI face, the run row — lives here once. A @@ -113,6 +114,24 @@ class AboutAuthorTool(_AboutTool): return about_author() +@register +class JoinDiscordTool(_AboutTool): + """The community server: one invite link, and a QR of it for a phone to read.""" + + name = "discord" + title = "Join Discord" + icon = "🔗" + help = "The invite link to the MeshTerm community server" + order = 25 # where everyone else is, once you know what this is and who made it + + @staticmethod + def page() -> "MarkdownDoc": + """The *Join Discord* page.""" + from ..ui.about import join_discord + + return join_discord() + + @register class SupportProjectTool(_AboutTool): """What keeps MeshTerm going, and the ways — paid and unpaid — to help it along.""" @@ -121,7 +140,7 @@ class SupportProjectTool(_AboutTool): title = "Support MeshTerm" icon = "💰" help = "What keeps MeshTerm going, and how to help" - order = 30 # the ask, and only once the first two have earned it + order = 30 # the ask, and only once the pages before it have earned it @staticmethod def page() -> "MarkdownDoc": diff --git a/meshterm/ui/about.py b/meshterm/ui/about.py index f128d68..76690a2 100644 --- a/meshterm/ui/about.py +++ b/meshterm/ui/about.py @@ -1,6 +1,6 @@ """The About MeshTerm pages: the app, the person who wrote it, and how to help it along. -Three read-only pages hung under the main menu's *About MeshTerm* section — the one +Four read-only pages hung under the main menu's *About MeshTerm* section — the one section that names a subject rather than a doing, because "what is this thing, and who made it" is a question the other five can't answer. Each opens as a full screen, scrolls if the terminal is short, and is left with Esc; nothing here touches the radio or the @@ -72,6 +72,15 @@ def about_author() -> MarkdownDoc: return _page("author") +def join_discord() -> MarkdownDoc: + """The *Join Discord* page: the invite link, and a QR of it to point a phone at. + + A page rather than a popup because there is nothing to decide — it is a link, and on + a headless console the QR *is* how the link gets off the screen. + """ + return _page("discord") + + def support_project() -> MarkdownDoc: """The *Support MeshTerm* page: what keeps it going, and how to chip in. @@ -166,6 +175,7 @@ __all__ = [ "AboutPage", "about_author", "about_meshterm", + "join_discord", "open_about_page", "support_project", ] diff --git a/tests/test_about.py b/tests/test_about.py index dce51d9..01c0df1 100644 --- a/tests/test_about.py +++ b/tests/test_about.py @@ -1,4 +1,4 @@ -"""About MeshTerm tests: the three pages, their placeholders, and their menu section. +"""About MeshTerm tests: the four pages, their placeholders, and their menu section. The pages carry no state and no controls, so what is worth pinning is what they *say* and how they are framed: the live package facts they must never drift from, the @@ -18,6 +18,7 @@ from meshterm.ui.about import ( AboutPage, about_author, about_meshterm, + join_discord, support_project, ) @@ -126,18 +127,27 @@ def test_the_licence_section_states_the_terms_and_the_credit_it_owes() -> None: assert owed in prose, owed +def test_the_discord_page_carries_the_invite_link_and_a_code_to_scan_it_with() -> None: + """The link is the whole page, and on a console with no clipboard the QR is how it + leaves the screen — so both have to survive the render.""" + text = _page(join_discord) + + assert "https://discord.gg/AZwe5Uvb3S" in text + assert "█" in text, "the scannable code didn't render" + + def test_pages_are_written_markdown_that_ships_with_the_package() -> None: """Filling a page in is editing its ``.md`` file — no Python has to follow.""" from meshterm.ui.about import _PAGES - for name in ("about", "author", "support"): + for name in ("about", "author", "discord", "support"): source = (_PAGES / f"{name}.md").read_text(encoding="utf-8") assert source.lstrip().startswith("#"), f"{name}.md doesn't open with a heading" def test_no_package_placeholder_survives_into_the_rendered_page() -> None: """The live facts are filled in as the page opens, so none of the braces reach a reader.""" - for builder in (about_meshterm, about_author, support_project): + for builder in (about_meshterm, about_author, join_discord, support_project): text = _page(builder) assert "{" not in text and "}" not in text, text @@ -194,7 +204,7 @@ def test_menu_rows_carry_the_icon_and_the_titles_do_not() -> None: """Each page has a menu icon; none of it leaks into the title the screen draws.""" from meshterm.tools import get_tool - for name in ("about", "about-author", "support"): + for name in ("about", "about-author", "discord", "support"): tool = get_tool(name) assert tool is not None assert tool.icon, f"{name} has no menu icon" @@ -211,7 +221,7 @@ def test_every_page_icon_has_a_picocalc_glyph() -> None: set_platform(PICOCALC) try: - for name in ("about", "about-author", "support"): + for name in ("about", "about-author", "discord", "support"): tool = get_tool(name) assert tool is not None compact = glyph(tool.icon) diff --git a/tests/test_gallery.py b/tests/test_gallery.py index a5375e8..dddd6ed 100644 --- a/tests/test_gallery.py +++ b/tests/test_gallery.py @@ -54,7 +54,13 @@ from meshterm.services.message_paths import Arrival from meshterm.services.monitor_service import ACTIVITY_BUCKETS from meshterm.services.records import CATEGORY_BY_ID from meshterm.services.topology import MeshTopology, build_topology -from meshterm.ui.about import AboutPage, about_author, about_meshterm, support_project +from meshterm.ui.about import ( + AboutPage, + about_author, + about_meshterm, + join_discord, + support_project, +) from meshterm.ui.chat import ChatScreen from meshterm.ui.contactlist import SORT_COLUMNS, SORT_OPENS_ASCENDING from meshterm.ui.contacts_screen import ContactsScreen @@ -493,6 +499,10 @@ def _support_project(cols: int, rows: int) -> Screen: return AboutPage("Support MeshTerm", support_project()) +def _join_discord(cols: int, rows: int) -> Screen: + return AboutPage("Join Discord", join_discord()) + + _ENTRIES: list[_Entry] = [ _Entry("dashboard", _dashboard), _Entry("contacts", _contacts), @@ -515,6 +525,7 @@ _ENTRIES: list[_Entry] = [ _Entry("tx_sweep", _tx_sweep), _Entry("about_meshterm", _about_meshterm), _Entry("about_author", _about_author), + _Entry("join_discord", _join_discord), _Entry("support_project", _support_project), ] diff --git a/tests/test_menus.py b/tests/test_menus.py index 65b4db3..2554b6c 100644 --- a/tests/test_menus.py +++ b/tests/test_menus.py @@ -167,7 +167,7 @@ def test_main_menu_sections_answer_the_menus_own_question() -> None: "Explore": ["map", "walk", "trace", "trace-path", "records"], "This node": ["info", "config", "device-actions", "advert"], "Other nodes": ["repeater-admin", "tx-optimize"], - "About MeshTerm": ["about", "about-author", "support"], + "About MeshTerm": ["about", "about-author", "discord", "support"], } # No section is so big it stops being a grouping (the old Mesh bucket held seven of # nineteen rows), and none is a bucket of one.