Files
MeshTerm/meshterm/ui/about.py
T
Jean-Pierre MartineauandClaude Opus 5 71b16ec7b7 The annotations, imports, and dead locals answer to ruff
`ruff check --fix` over the whole tree, ahead of putting the linter in CI:
1977 mechanical fixes, overwhelmingly `Optional[X]` → `X | None` (1186 in the
package alone) and the quoted annotations that `from __future__ import
annotations` already made unnecessary, plus import sorting and unused imports.
Nothing here changes behaviour — the suite is 1885 green before and after.

Four findings were real rather than stylistic and are fixed by hand: a dead
`cells` range in `MapCanvas`, a dead slot dict in the heat-ladder test, a
`ProgressScreen` task handle nobody read, and a tab loop binding a `label` it
never used. The two `F821`s were quoted forward references to names imported
inside their function bodies — now declared under `TYPE_CHECKING`, which keeps
`test_performance`'s local import (it guards the startup import graph) while
letting the linter resolve the name.

412 findings remain and are deliberately untouched: they need a decision about
the rule, not an edit to the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:26:12 -04:00

181 lines
7.4 KiB
Python

"""The About MeshTerm pages: the app, the person who wrote it, and how to help it along.
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
database, so a page reads the same with no device attached at all.
Unlike every other screen in the app, these are **written** rather than composed: their
content is markdown, living beside the wordmark in ``meshterm/assets/pages``, drawn
through :mod:`meshterm.ui.markdown` in the app's own visual language (see that module
for what each construct becomes). Filling a page in means editing its ``.md`` file —
no screen, menu row, or CLI face has to follow — and everything markdown offers is
available while doing it: sections, sub-headings, lists, quotes, links, tables.
All four are written. The ``##`` headings are the shape each page was written into,
and they are load-bearing beyond the eye: the screen records where each one landed while
rendering, which is what lets it pin the heading a reader is under and step the section
jump from one to the next — so a page is navigable the way a grouped list is.
What is *not* written into those files: the version, the author, and the copyright span.
Each page names them with a ``{version}`` / ``{author}`` / ``{copyright}`` placeholder
that is filled from :mod:`meshterm` itself (:func:`~meshterm.copyright_notice`) as the
page is opened, so the About page can never drift from the package it describes.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from .. import __author__, __version__, copyright_notice
from .markdown import MarkdownDoc, render_markdown
from .tui import ScrollScreen
from .tui.render import render_lines
if TYPE_CHECKING:
from ..context import AppContext
#: Where the written pages live — beside the wordmark, in the package's assets, so they
#: ship with the wheel and can be edited without touching Python.
_PAGES = Path(__file__).resolve().parent.parent / "assets" / "pages"
def _page(name: str) -> MarkdownDoc:
"""Load ``assets/pages/<name>.md``, fill in the live package facts, and render it.
Args:
name: The page file's stem.
Returns:
The rendered page.
"""
source = (_PAGES / f"{name}.md").read_text(encoding="utf-8")
for token, value in (
("{version}", __version__),
("{author}", __author__),
("{copyright}", copyright_notice()),
):
source = source.replace(token, value)
return render_markdown(source)
def about_meshterm() -> MarkdownDoc:
"""The *About MeshTerm* page: what the app is, where it came from, its terms."""
return _page("about")
def about_author() -> MarkdownDoc:
"""The *About the author* page: the person behind MeshTerm, on the mesh and off it."""
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.
Deliberately two-sided — money is one way to help and not the only one, so the page
keeps a section for the other kind rather than folding it into a donate link.
"""
return _page("support")
class AboutPage(ScrollScreen):
"""One written page: a read-only body, drawn full-screen, left with Esc.
Nearly all of it is the shared read-only screen — the pager, the derived footer hint
that only names the pager when there *is* something to page, the PicoCalc F-key lane
that dims on the same gate — with two things added, both of which come from the body
being a *document* rather than a block:
* It renders block by block and records where each ``##`` section starts, so a
heading pins to the top row while its prose scrolls under it and ``^PgUp``/``^PgDn``
step section by section (the same landmark machinery a grouped select list uses).
* Its lane claims the left-hand ``Sect ↑`` / ``Sect ↓`` pair once the page has more
than one section, because on the console the lane is the only place a chord can
advertise itself.
Only the framing differs from a result window: these are full screens rather than
floating views, so Esc reads *back*.
"""
def __init__(self, title: str, doc: MarkdownDoc) -> None:
"""Show ``doc`` as the page under ``title``.
Args:
title: The page's heading — sentence case, no icon (icons live in the menu
rows that open these pages, never in a title).
doc: The rendered page, from one of the builders above.
"""
super().__init__(doc, title=title, floating=False)
self._doc = doc
@property
def fkey_lane(self):
"""The shared pager lane, plus the section step on a page that has sections.
A left-hand pair rises toward F1, so ``Sect ↑`` sits outside ``Sect ↓`` — the
order a grouped select list uses on the same two keys, dispatching the same two
actions. Both are lit only while the page actually scrolls: with the whole page
on screen there is no section to step *to*.
"""
from .tui.fkeys import FPair, default_lane
lane = list(default_lane(nav=self.content_overflows))
if len(self._doc.sections) >= 2:
lane[0] = FPair("Sect ↑", "ctrl_pageup", enabled=self.content_overflows)
lane[1] = FPair("Sect ↓", "ctrl_pagedown", enabled=self.content_overflows)
return lane
def render_body(self, width: int) -> list[str]:
"""Render the page a block at a time, noting where its section headings land."""
lines: list[str] = []
self._sticky_headers = []
for block in self._doc.blocks:
# A blank separator renders to no lines at all on its own (it is one empty
# Text, and an empty render has nothing to split); inside the document's
# Group it would be the empty line it is meant to be, so it stays one here.
rendered = render_lines(block.renderable, width) or [""]
if block.heading:
self._sticky_headers.append((len(lines), rendered))
lines.extend(rendered)
self._scroll_total = max(1, len(lines))
return lines
async def open_about_page(ctx: AppContext, title: str, doc: MarkdownDoc) -> None:
"""Open one About page full-screen and hold it until the reader backs out.
Args:
ctx: The shared application context (must be running the interactive TUI).
title: The page's heading.
doc: The page's content.
Raises:
RuntimeError: If called outside the interactive menu (no full-screen session).
"""
from .surface import TuiUi
if not isinstance(ctx.ui, TuiUi): # pragma: no cover - guarded by the menu-only caller
raise RuntimeError("the About pages are only available in the menu")
await ctx.ui.session.run_screen(AboutPage(title, doc))
__all__ = [
"AboutPage",
"about_author",
"about_meshterm",
"join_discord",
"open_about_page",
"support_project",
]