The community has a page, and the page is its invite

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) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-02 08:38:44 -04:00
1 parent 155c0b5b21
commit 6fc4e7afc9
6 files changed
+74 -15

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
## Join the Discord
Questions, ideas, bug reports, and mesh talk.
- *https://discord.gg/AZwe5Uvb3S*
```qr
https://discord.gg/AZwe5Uvb3S
```
+26 -7
View File
@@ -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":
+11 -1
View File
@@ -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",
]
+15 -5
View File
@@ -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)
+12 -1
View File
@@ -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),
]
+1 -1
View File
@@ -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.