Files
MeshTerm/meshterm/ui/specimen.py
T
Jean-Pierre MartineauandClaude Opus 5 8a80d47815 F-key lane: a jump rides the Shift half of the page it follows
Home and End are on the PicoCalc's keyboard. Paging is not. So the
jump-to-either-end pair was never the unreachable thing that earned it
F1/F2 -- it only ever wanted a chip for consistency with the pager it
belongs to. Now it rides that pager: Shift+F5 is Top because F5 is
Page up, Shift+F4 is Bottom because F4 is Page down. One axis, one pair
of keys, the second-grade verb sitting behind the first.

That frees F1-F3 on every screen, and the lane is the only place the
device can advertise anything -- there is no hint line to read a chord
off. So the freed slots went to the affordances that were invisible
there:

  select lists  Sect down/up, the section jumps. Ctrl+PgUp on a
                keyboard with no PgUp is the one binding in the app
                that was physically unreachable. Plus Delete, lit on
                exactly the rows the key acts on.
  map           Region (Home, renamed -- it frames the ground the mesh
                covers, it does not "reset" anything), You (new: Ctrl+L,
                recentre on our own node at the zoom you chose), and
                Frame for the find matches.
  node page     the tab it would switch you to.
  time machine  the window span it would take you to -- w was
                undiscoverable on the device.

The map is the one exception to the new rule: Region answers "where
should I be looking", not "how far down the body am I", so it keeps F1
beside the other two destinations rather than hiding behind a pager it
has nothing to do with.

Screens that never scroll (every dialog, the busy splash, the reorder
list) now say so with an empty lane instead of offering a pager that
does nothing. Screens whose Home/End mean something else say what:
Latest/Oldest in a chat, Newest/Oldest in the packet viewer, You on the
walk (where End is bound to nothing, so its slot stays blank).

The map's hint line finally fits 72: its basemap status was never a key
hint, so it moved to the title's status chain where the standards put
those -- which is what took the map off the gallery's pre-existing
overflow list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:56:42 -04:00

155 lines
6.4 KiB
Python

"""The visual-language specimen: every mark, icon, colour and fold on one screen.
``meshterm specimen`` prints this through the platform's real machinery — the active
theme, the ``glyph()`` icon funnel, the render-boundary fold, the heat and SNR scales —
so what it shows *is* what the platform draws, not a mock-up of it. On the PicoCalc
console it doubles as the font/palette acceptance card (all nine P3 marks, the node
glyphs, braille, the 16 programmed slots); on a desktop terminal it shows the same
content in the regular platform's emoji-and-truecolor dress. Sized to the PicoCalc
floor: every line ≤ 53 cells, the whole card ≤ 24 rows.
"""
from __future__ import annotations
from rich.console import RenderableType
from rich.text import Text
from ..platforms import get_platform
from .packet_viewer import KIND_ICONS, PAYLOAD_ICONS
from .theme import _VT_SLOTS, fold_text, glyph, name_style, snr_style
from .tui.fkeys import FPair, lane_text
from .widgets import _recency_style, channel_glyph
#: Demo identities for the name-colour row: ``(name, key)``. The keys are synthetic, and
#: chosen so their first bytes land in three different sectors of the hue wheel — on the
#: console that means three different palette slots (see ``theme._NODE_SLOT_HEXES``).
_DEMO_NAMES = (("Alice", "a1b2"), ("YUL-Poly", "3d63"), ("Lounge", "cc10"))
def _palette_row() -> Text:
"""The 16 slots as filled blocks, dim bank then bright bank.
On PicoCalc the blocks address the console's real slots (``color(N)``), so the row
shows exactly what ``setvtrgb`` programmed; on the regular platform they carry the
design's RGB values instead — a preview of the same palette in truecolor.
"""
row = Text()
for slot, _, hex_ in _VT_SLOTS:
style = f"color({slot})" if not get_platform().truecolor else hex_
row.append("██", style=style)
return row
def _ages_row() -> Text:
"""The heat scale over its own anchors — one age just inside each of the seven steps."""
row = Text()
anchors = (
("now", 0), ("20m", 1200), ("3h", 10800), ("2d", 172800),
("2w", 1209600), ("3mo", 7776000), ("never", None),
)
for label, secs in anchors:
if row.plain:
row.append(" ")
row.append(label, style=_recency_style(secs))
return row
def specimen_lines() -> list[RenderableType]:
"""The specimen card, one renderable per line (print through the themed console)."""
platform = get_platform()
lines: list[RenderableType] = []
lines.append(Text.assemble(("Specimen", "accent"), (" · ", "muted"), platform.name))
lines.append(Text())
lines.append(_palette_row())
legend = Text("stock palette · 8 muted · 9-14 node hues · 15 you", style="muted")
lines.append(legend)
lines.append(Text())
status = Text("status ")
for mark, style in (("✓", "ok"), ("✗", "err"), ("⚠", "warn"), ("●", "err"), ("○", "muted")):
status.append(mark + " ", style=style)
lines.append(status)
# The node-type marks in the theme's own ``type.*`` entries — the same styles every
# typed node in the app is drawn in (ui.widgets._NODE_GLYPHS), so this row *is* the
# marker language rather than a restatement of it. The star and the unknown ring have
# no type of their own: they take the ``you`` white and ``muted`` grey they carry
# everywhere else.
nodes = Text("nodes ")
nodes.append("★ ", style="you")
for mark, style in (
("● ", "type.node"), ("▲ ", "type.repeater"), ("■ ", "type.room"),
("◉ ", "type.sensor"), ("○", "muted"),
):
nodes.append(mark, style=style)
lines.append(nodes)
classes = Text("classes ")
for icon in KIND_ICONS.values():
classes.append(glyph(icon) + " ", style="brand")
classes.append(" payloads ", style="muted")
for typename in ("REQ", "RESPONSE", "TEXT_MSG", "GRP_TXT", "GRP_DATA", "ANON_REQ"):
classes.append(glyph(PAYLOAD_ICONS[typename]) + " ")
for typename in ("PATH", "TRACE", "MULTIPART", "CONTROL"):
classes.append(glyph(PAYLOAD_ICONS[typename]) + " ")
lines.append(classes)
channels = Text("channels ")
channels.append(channel_glyph("#public", None) + " name-derived ", style="brand")
channels.append(glyph("🌐") + " public ")
channels.append(glyph("🔒") + " private")
lines.append(channels)
concepts = Text("concepts ")
for icon in ("📡", "🕒", "🔄", "💾", "📂", "🔑", "🗑", "✎", "⚡", "⭐", "📤", "📨"):
concepts.append(glyph(icon) + " ")
for icon in ("🔔", "🔕", "📱", "🔗", "🏆", "⌨"):
concepts.append(glyph(icon) + " ")
lines.append(concepts)
lines.append(Text("keys ⌫ ⇧ … ↕ ↔ ↻ ⌖ ❯ ▸"))
lines.append(Text())
names = Text("names ")
for name, key in _DEMO_NAMES:
names.append(name + " ", style=name_style(name, key))
names.append("me", style="you")
lines.append(names)
lines.append(Text("heard ").append_text(_ages_row()))
snr = Text("snr ")
for reading in (8.0, -2.0, -12.0):
snr.append(f"{reading:+.1f} ", style=snr_style(reading))
lines.append(snr)
# What the render boundary makes of stored text: accents fold on PicoCalc (the raw
# form would be tofu there — the console font has no accented Latin), Cyrillic is
# font-native and passes through on both platforms.
folded = fold_text("café Montréal")
lines.append(Text.assemble("text ", (folded, "brand"), (" · ", "muted"), "привет мир"))
lines.append(Text())
chart = Text("chart ")
chart.append("⡀⡄⡆⣆⣦⣶⣷⣿", style="snr.good")
chart.append("⣶⣤⣀", style="snr.ok")
chart.append("⡀⠄⠂", style="snr.bad")
chart.append(" braille", style="muted")
lines.append(chart)
# The F-key lane, on the one platform that draws it: both fills, plus a dim slot and
# an unassigned one, so the card shows every state a chip can be in. Built through
# lane_text itself — this is the acceptance screen, so it must be the real renderer.
if platform.footer_fkeys:
lane = (
FPair("Region", "home"),
FPair("You", "locate", enabled=False),
None,
FPair("Page ↓", "pagedown", "Bottom", "end"),
FPair("Page ↑", "pageup", "Top", "home"),
)
lines.append(Text())
lines.append(lane_text(lane))
lines.append(lane_text(lane, shifted=True))
return lines