Files
MeshTerm/meshterm/tools/about.py
T
Jean-Pierre MartineauandClaude Opus 5 cef7a764af How MeshTerm behaves gets a page of its own
Three kinds of "setting" were tangled into two files and one of them had no
screen at all. The radio's settings live on the radio and are edited on Device
config. Machine setup -- where the database is, which companion to talk to --
stays in config.toml, a text editor's job. Everything in between was app
behaviour: six keys sitting in config.toml with nothing to open them, and a
dozen more that were only module constants a reader could never reach.

Those are preferences now, and they have a page. core/preferences.py is a
PrefSpec registry in the shape of the device-config one -- group, type, bounds,
help, and a default -- and the page is built from the same parts as the device
editor, deliberately: one grouped lane-aligned list, current -> new staged in
warn, Apply/Back appearing only once something is staged, the shared discard
confirm on the way out. Two editors that stage values should not be two screens
to learn. What it adds is a Defaults section holding one row that stages every
changed preference back to its built-in value; it is drawn only while a reset
would do something, and it stages rather than writes, so it is as reviewable and
as Esc-discardable as any other change and the file is still only ever written
by the one action at the bottom. Its rows scroll sideways where the device
editor's don't: the explanation is the widest thing on the row and the only part
that runs past the edge, so <-> slide it under a pinned setting and value.

The file is preferences.yaml and holds only the values you disagree with the
code about -- grouped and commented, each entry above its help and its default.
That is what lets a later change to a default reach an install that never asked
to be exempt; delete a line and the code takes over again. A hand edit gone
wrong costs that line, never the session.

Every preference is wired to something that reads it. ctx.preferences is the
explicit path; preferences.current() is for the render layer, which has no
context to reach through, the same reason get_platform() exists.

The About pages come along into a This app section, the third scope after this
node and other nodes, with Preferences leading it -- the one row there that
changes MeshTerm rather than describing it. A section of one would have broken
the menu's own no-bucket-of-one rule, and the app's scope is the honest home for
both halves anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:36:40 -04:00

153 lines
5.1 KiB
Python

"""The About MeshTerm tools: the four written pages in the menu's last section.
*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. They share the *This app* section with Preferences,
which leads it: the section is the app's own scope, and the one row that *changes*
MeshTerm sits above the four that describe it.
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 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.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional
from ..context import AppContext
from .base import Tool, ToolResult, register
if TYPE_CHECKING:
from ..ui.markdown import MarkdownDoc
class _AboutTool(Tool):
"""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
subclass supplies :meth:`page`, and nothing more.
"""
category = "This app"
@staticmethod
def page() -> "MarkdownDoc":
"""Build this page's content.
Overridden by each page. Subclasses import their builder *inside* the override
rather than at module scope: the registry imports every tool module at startup
(see :func:`~meshterm.tools.load_all_tools`), and a page's content is only ever
wanted once someone actually opens it.
"""
raise NotImplementedError
async def prompt_params(self, ctx: AppContext) -> Optional[dict[str, Any]]:
"""Open the page; returning ``None`` completes the invocation with no run row.
The page transmits nothing and stores nothing, so — like the Trophy case and the
mesh walk — it lives here rather than in :meth:`run`, and logs no run of its own.
Args:
ctx: Shared application context.
Returns:
Always ``None``.
"""
from ..ui.about import open_about_page
await open_about_page(ctx, self.title, self.page())
return None
async def run(self, ctx: AppContext, params: dict[str, Any]) -> ToolResult:
"""Print the page — only reachable from the CLI (the menu opens the screen).
Args:
ctx: Shared application context.
params: Unused beyond the injected ``_run_id``.
Returns:
A :class:`ToolResult` naming the page that was shown.
"""
ctx.ui.show(self.page())
return ToolResult(summary={"page": self.name})
@register
class AboutMeshTermTool(_AboutTool):
"""What MeshTerm is, where it came from, and the terms it ships under."""
name = "about"
title = "About MeshTerm"
icon = "📖"
help = "What this is, and the terms it ships under"
order = 10 # the question a stranger asks first
@staticmethod
def page() -> "MarkdownDoc":
"""The *About MeshTerm* page."""
from ..ui.about import about_meshterm
return about_meshterm()
@register
class AboutAuthorTool(_AboutTool):
"""The person behind MeshTerm, on the mesh and off it."""
name = "about-author"
title = "About the author"
icon = "👤"
help = "The person behind MeshTerm, on and off the mesh"
order = 20 # who made the thing you just read about
@staticmethod
def page() -> "MarkdownDoc":
"""The *About the author* page."""
from ..ui.about import about_author
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."""
name = "support"
title = "Support MeshTerm"
icon = "💰"
help = "What keeps MeshTerm going, and how to help"
order = 30 # the ask, and only once the pages before it have earned it
@staticmethod
def page() -> "MarkdownDoc":
"""The *Support MeshTerm* page."""
from ..ui.about import support_project
return support_project()