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>
153 lines
5.1 KiB
Python
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()
|