The PicoCalc draws its path lines as chips

The console font gains the two powerline chevrons, drawn into the next
two donor slots (U+2559 and U+255B, which nothing draws), in both the
6x12 and the 6x8 builds. With them in its inventory, the PicoCalc's
verdict is core, so its routes are ribbons of chips like the
Cardputer's.

A chip there can only be filled from the eight background slots, since
under a 512-glyph font the VT has no bright backgrounds. So a node's
chip is the dim twin of its hue (theme.DIM_TWIN), and its label is the
hue itself: the colour the name has in every list and in arrow mode. A
hash label and an annotation are that colour throughout. A keyless chip
is white on light grey, the same pair one bank down. Our star, a faded
hop and the composer's slot have no dark grey to stand on, so they
stand on the page, the slot as a white + in a break of the ribbon. Two
chips of one colour join on a thin chevron in that colour.

Bold is brightness on the console, and Rich merges a selected row's
bold into every span on it, which turned the dim fills bright. Every
chip style there says "not bold"; the tests read the rendered row back
through the emulator's VT to hold that.

The new font takes effect once the font script runs again on the Lyra
(as root). Until then the PicoCalc's installed font lacks the chevrons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-06 03:14:12 -04:00
1 parent c07f15f4ec
commit 69d9836546
9 files changed
+350 -71

No files matched your search

+6 -2
View File
@@ -765,8 +765,12 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT
`fontset.CARDPUTER_ZERO_CODEPOINTS`, named by `Platform.font` as the PicoCalc's is; the fold
derives everything from the platform's font (accents, emoji, the `?` net) and quantizes
colour only where `truecolor` is off. The path line's chips-or-arrows verdict reads the
same font (`termfont`, after `MESHTERM_POWERLINE` and before any terminal probe): Terminus
has the chevrons, so the Cardputer draws chips; the PicoCalc's 512 glyphs don't, so arrows.
same font (`termfont`, after `MESHTERM_POWERLINE` and before any terminal probe), and both
handhelds have the chevrons: Terminus natively, the PicoCalc's font in two donor slots. On
the PicoCalc a chip can only be filled from the eight background slots, so a node's chip
is the dim twin of its hue with the label in the hue itself (`theme.DIM_TWIN`), every chip
span says `not bold` (bold is brightness), and our `★`, a faded hop and the composer's
slot stand on the page, because there is no dark-grey background (`pathline._slot_colours`).
- **The Cardputer's exit must fit its launcher's grace.** A held Esc there is the
launcher's own way out (SIGTERM at 3 s, SIGKILL 3 s later, letting go or not), which is
where hold-to-quit's timing comes from (see **Navigation**). MeshTerm's whole exit was
+7 -4
View File
@@ -26,7 +26,9 @@ from __future__ import annotations
#: Inclusive ``(first, last)`` codepoint ranges present in the font. 519 codepoints over
#: 512 glyphs — aliases (the rounded corners onto the square ones, ``⋯`` onto ``…``) and
#: shared slots give a few glyphs more than one codepoint. Verified against a live table
#: dump of the installed font (2026-08-01, post-P3 build).
#: dump of the installed font (2026-08-01, post-P3 build). On 2026-10-06, the two powerline
#: chevrons of the path chips (U+E0B0, U+E0B1) took the donor slots of U+2559 and U+255B.
#: No live dump checked that build yet.
# fmt: off
FONT_RANGES: tuple[tuple[int, int], ...] = (
(0x0020, 0x007F), (0x00A0, 0x00A0), (0x00A7, 0x00A7), (0x00A9, 0x00A9),
@@ -41,7 +43,7 @@ FONT_RANGES: tuple[tuple[int, int], ...] = (
(0x250C, 0x250C), (0x2510, 0x2510), (0x2514, 0x2514), (0x2518, 0x2518),
(0x251C, 0x251C), (0x2524, 0x2524), (0x252C, 0x252C), (0x2534, 0x2534),
(0x253C, 0x253C), (0x2550, 0x2551), (0x2554, 0x2554), (0x2557, 0x2557),
(0x2559, 0x255B),
(0x255A, 0x255A),
(0x255D, 0x2561), (0x2563, 0x2563), (0x2566, 0x256A), (0x256C, 0x2570),
(0x2580, 0x2580), (0x2584, 0x2584), (0x2588, 0x2588), (0x258C, 0x258C),
(0x2590, 0x2593), (0x25A0, 0x25A0), (0x25AC, 0x25AC), (0x25B2, 0x25B2),
@@ -49,12 +51,13 @@ FONT_RANGES: tuple[tuple[int, int], ...] = (
(0x25C0, 0x25C0), (0x25C4, 0x25C4), (0x25C9, 0x25C9), (0x25CB, 0x25CB),
(0x25CF, 0x25CF), (0x25F7, 0x25F7), (0x2605, 0x2605), (0x263C, 0x263C),
(0x2699, 0x2699), (0x26A0, 0x26A0), (0x26BF, 0x26BF), (0x2713, 0x2713),
(0x2717, 0x2717), (0x276F, 0x276F), (0x2800, 0x28FF), (0xFFFD, 0xFFFD),
(0x2717, 0x2717), (0x276F, 0x276F), (0x2800, 0x28FF), (0xE0B0, 0xE0B1),
(0xFFFD, 0xFFFD),
)
# fmt: on
#: Every codepoint the console font can draw, as one frozen set (built once at import;
#: 520 members, so the set is small and the per-character membership test is a dict hit).
#: 519 members, so the set is small and the per-character membership test is a dict hit).
FONT_CODEPOINTS: frozenset[int] = frozenset(
cp for first, last in FONT_RANGES for cp in range(first, last + 1)
)
+177 -54
View File
@@ -14,7 +14,9 @@ drawn*, so every surface the survey found can eventually route through it:
chip ahead as its background — so a route reads as a ribbon whose segments meet on a
chevron: the oh-my-posh look, at one cell a seam. The chip fill is the node's
hash-derived hue (:func:`~meshterm.ui.theme.node_style`), our own node the map's yellow
``★`` on a neutral dark grey, a faded hop dark slate, a keyless hop grey. Two chips that
``★`` on a neutral dark grey, a faded hop dark slate, a keyless hop grey. (On the
16-colour console of the PicoCalc, a node's chip is the dim twin of its hue, and the
label is in the hue itself. Refer to :func:`_slot_colours`.) Two chips that
land on the *same* fill — a mirrored return leg, a stretch of keyless greys — or on two
fills too close to tell apart (under :data:`SEAM_BLUR` apart in OKLab, the perceptual
distance :mod:`~meshterm.ui.oklab` measures) take the one exception the interlock can't
@@ -82,6 +84,7 @@ from __future__ import annotations
import re
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import NamedTuple
from rich.cells import cell_len
from rich.console import Console, ConsoleOptions, RenderResult
@@ -89,10 +92,11 @@ from rich.measure import Measurement
from rich.text import Text
from ..core.models import LOCAL_DEVICE_LABEL
from ..platforms import Platform, on_platform
from . import oklab
from .marks import SELF_MARK
from .termfont import powerline_enabled, powerline_full
from .theme import active_theme, node_style
from .theme import DIM_TWIN, active_theme, node_style, slot_hex
#: The powerline solid right-pointing triangle (U+E0B0) — the glyph the widget draws
#: at every seam and closing edge, deliberately from the *core* set so every recommended
@@ -152,6 +156,35 @@ _YOU_BG = "#3f3f46"
_DIM_BG = "#334155"
_DIM_FG = "#94a3b8"
#: On the 16-colour console, the page: slot 0, black. It is the fill of a chip that the
#: console cannot fill, because the VT has no dark grey background. That chip is our
#: ``★``, a faded hop, or the composer's slot. Its text then sits on the page, and the
#: chips on each side close on a point and open on a notch.
_SLOT_PAGE = slot_hex(0)
#: On the console, the text of a faded hop: slot 8, the dark grey.
_SLOT_FAINT = slot_hex(8)
#: On the console, white: slot 15. It is the text of a keyless chip and of the
#: composer's slot.
_SLOT_WHITE = slot_hex(15)
#: Each dim console slot and its bright twin: the reverse of
#: :data:`~meshterm.ui.theme.DIM_TWIN`.
_BRIGHT_TWIN = {dim: bright for bright, dim in DIM_TWIN.items()}
class _ChipColours(NamedTuple):
"""The colours of one chip.
Attributes:
fill: The background of the chip.
ink: The colour of the label.
soft: The colour of an annotation, and of a hash label after its lit bytes.
"""
fill: str
ink: str
soft: str
#: The mark standing in for elided hops (see :meth:`PathLine.ellipsized`) — rendered as
#: a dim pseudo-hop so it recedes in both modes.
_ELISION = "⋯"
@@ -365,7 +398,7 @@ def cut_mark(line: Text, at: int, side: str) -> Text:
fill = next((fills[i] for i in steps if fills[i]), None)
if fill is None:
return Text(_ELLIPSIS, style="muted")
return Text(CRACK_HEAD if side == ELIDE_HEAD else CRACK_TAIL, style=fill)
return Text(CRACK_HEAD if side == ELIDE_HEAD else CRACK_TAIL, style=f"{_FLAT}{fill}")
def with_action_mark(line: Text, width: int) -> Text:
@@ -690,7 +723,7 @@ class PathLine:
foot = 0 if self._to_destination else cell_len(self._separator.rstrip())
return plain_cells, cell_len(self._separator), 0, 0, head, foot
widths = [
cell_len(hop.label) if hop.gap else self._chip(hop, self._chip_fill(hop)).cell_len
cell_len(hop.label) if hop.gap else self._chip(hop, _colours_of(hop)).cell_len
for hop in hops
]
sep = cell_len(POWERLINE_SEP)
@@ -971,7 +1004,8 @@ class PathLine:
carry_in: This line continues a wrapped path (it does not open one).
carry_on: The path continues past this line (it does not end here).
"""
fills = [self._chip_fill(hop) for hop in hops]
colours = [_colours_of(hop) for hop in hops]
fills = [colour.fill for colour in colours]
rounded = powerline_full()
# A fold and a half-drawn route are two reasons for the same mark; either is
# enough. An elision already breaks the ribbon on that side, and a mark cut out
@@ -981,29 +1015,29 @@ class PathLine:
if opens_mid:
text.append_text(self._notch(fills[0])) # the break's other half
elif rounded and not hops[0].gap:
text.append(POWERLINE_ROUND_OPEN, style=fills[0])
text.append(POWERLINE_ROUND_OPEN, style=f"{_FLAT}{fills[0]}")
for i, hop in enumerate(hops):
if i:
if hops[i - 1].gap:
text.append_text(self._notch(fills[i])) # the ribbon picks up again
elif hop.gap:
text.append(POWERLINE_SEP, style=fills[i - 1]) # …and stops here
text.append(POWERLINE_SEP, style=f"{_FLAT}{fills[i - 1]}") # …and stops
else:
text.append_text(self._seam(fills[i - 1], fills[i]))
text.append_text(self._seam(colours[i - 1], colours[i]))
if hop.gap:
text.append(hop.label, style="faint" if hop.dim else "muted")
else:
text.append_text(self._chip(hop, fills[i]))
text.append_text(self._chip(hop, colours[i]))
if hops[-1].gap:
return text # the line ended on the page; there is no chip left to close
if carry_on or not self._to_destination:
text.append(POWERLINE_SEP, style=fills[-1]) # the point: the path goes on
text.append(POWERLINE_SEP, style=f"{_FLAT}{fills[-1]}") # the point: it goes on
elif rounded:
text.append(POWERLINE_ROUND_CLOSE, style=fills[-1]) # the lozenge's far end
text.append(POWERLINE_ROUND_CLOSE, style=f"{_FLAT}{fills[-1]}") # the far end
return text
@staticmethod
def _seam(before: str, after: str) -> Text:
def _seam(before: _ChipColours, after: _ChipColours) -> Text:
"""The one cell between two chips: the previous fill's point, laid on the next.
The classic interlock — foreground the chip behind, background the chip ahead —
@@ -1031,16 +1065,20 @@ class PathLine:
and a chevron in the dark chip ink read as a mark laid on the route rather than a
seam in it. One cell either way.
On the 16-colour console, the thin chevron is the bright twin of the fill, which
is the colour of the label on that chip (:func:`_slot_thin_ink`).
Args:
before: The fill the point is drawn in (the previous chip's).
after: The fill it is laid on (the next chip's).
before: The colours of the previous chip. The point is drawn in its fill.
after: The colours of the next chip. The point is laid on its fill.
Returns:
The seam's single styled cell.
"""
if _fills_blur(before, after):
return Text(POWERLINE_THIN, style=f"{_seam_ink(before, after)} on {after}")
return Text(POWERLINE_SEP, style=f"{before} on {after}")
if _fills_blur(before.fill, after.fill):
ink = _thin_ink(before, after)
return Text(POWERLINE_THIN, style=f"{_FLAT}{ink} on {after.fill}")
return Text(POWERLINE_SEP, style=f"{_FLAT}{before.fill} on {after.fill}")
@staticmethod
def _notch(fill: str) -> Text:
@@ -1058,37 +1096,12 @@ class PathLine:
The one styled cell.
"""
text = Text()
text.append(POWERLINE_SEP, style=f"{fill} reverse")
text.append(POWERLINE_SEP, style=f"{_FLAT}{fill} reverse")
return text
def _chip_fill(self, hop: PathHop) -> str:
"""A chip's fill: cursor accent, override, dim slate, our grey, hue, keyless grey.
The insertion slot outranks everything — it is the one chip that isn't a node,
and it wears the ``cursor`` white so it reads as chrome among identities rather
than as a hop with an unlucky hue (white sits outside the node spectrum, and the
chip ink is already picked to stay readable on it). The fade comes next, outranking
identity including our own: a dimmed hop is one nobody composed (an automatic landing
back home, a mirrored return leg), and our end must recede with the rest of that automatic
half rather than keep its yellow among the greys. Plain mode says the same thing
by fading the name.
"""
if hop.cursor:
return _style_hex("cursor") or _KEYLESS_BG
if hop.style:
resolved = _style_hex(hop.style)
if resolved:
return resolved
if hop.dim:
return _DIM_BG
if hop.you:
return _YOU_BG
if hop.key:
return _style_hex(node_style(hop.key)) or _KEYLESS_BG
return _KEYLESS_BG
def _chip(self, hop: PathHop, fill: str) -> Text:
"""One chip: same words as arrow mode, dark ink on the identity fill.
@staticmethod
def _chip(hop: PathHop, colours: _ChipColours) -> Text:
"""One chip: the same words as arrow mode, in the chip's ink on its fill.
Every chip is padded on both sides, the ``★`` included (JP, 2026-08-09): the pads
are not there to make room for a long *word*, they are the chip's own shape, and a
@@ -1096,24 +1109,134 @@ class PathLine:
than as a segment of the route. Our own chip's ink is the map's yellow on the
neutral dark grey :data:`_YOU_BG`, so the star in the line and the star on the map
read as one mark.
The colours come from the palette of the platform (:func:`_colours_of`).
"""
ink = _DIM_FG if hop.dim else (_SELF_INK if hop.you else _CHIP_FG)
soft = _DIM_FG if hop.dim else _CHIP_FG_SOFT
fill, ink, soft = colours
text = Text()
text.append(" ", style=f"on {fill}")
text.append(" ", style=f"{_FLAT}on {fill}")
if hop.lit_bytes > 0 and not hop.dim:
split = hop.lit_bytes * 2
text.append(hop.label[:split], style=f"bold {ink} on {fill}")
text.append(hop.label[split:], style=f"{soft} on {fill}")
text.append(hop.label[:split], style=f"{_WEIGHT}{ink} on {fill}")
text.append(hop.label[split:], style=f"{_FLAT}{soft} on {fill}")
else:
weight = "" if hop.dim else "bold "
weight = _FLAT if hop.dim else _WEIGHT
text.append(hop.label, style=f"{weight}{ink} on {fill}")
if hop.annotation:
text.append(f" ({hop.annotation})", style=f"{soft} on {fill}")
text.append(" ", style=f"on {fill}")
text.append(f" ({hop.annotation})", style=f"{_FLAT}{soft} on {fill}")
text.append(" ", style=f"{_FLAT}on {fill}")
return text
def _spectrum_fill(hop: PathHop) -> str:
"""A chip's fill: cursor accent, override, dim slate, our grey, hue, keyless grey.
The insertion slot outranks everything — it is the one chip that isn't a node,
and it wears the ``cursor`` white so it reads as chrome among identities rather
than as a hop with an unlucky hue (white sits outside the node spectrum, and the
chip ink is already picked to stay readable on it). The fade comes next, outranking
identity including our own: a dimmed hop is one nobody composed (an automatic landing
back home, a mirrored return leg), and our end must recede with the rest of that automatic
half rather than keep its yellow among the greys. Plain mode says the same thing
by fading the name.
"""
if hop.cursor:
return _style_hex("cursor") or _KEYLESS_BG
if hop.style:
resolved = _style_hex(hop.style)
if resolved:
return resolved
if hop.dim:
return _DIM_BG
if hop.you:
return _YOU_BG
if hop.key:
return _style_hex(node_style(hop.key)) or _KEYLESS_BG
return _KEYLESS_BG
def _spectrum_colours(hop: PathHop) -> _ChipColours:
"""The colours of a chip on a truecolor terminal: dark ink on the fill."""
ink = _DIM_FG if hop.dim else (_SELF_INK if hop.you else _CHIP_FG)
soft = _DIM_FG if hop.dim else _CHIP_FG_SOFT
return _ChipColours(_spectrum_fill(hop), ink, soft)
def _slot_colours(hop: PathHop) -> _ChipColours:
"""The colours of a chip on the 16-colour console: a bright slot on its dim twin.
The VT draws a background only from slots 0 to 7, which are the dim twins of the
bright slots that colour the node names. Thus a node's chip is the dim twin of its
hue, and its label is in the hue itself: the colour that its name has in arrow mode
and in each list. A pink name gets a purple chip, a yellow name a brown chip. The
whole label is in this colour, a hash label too: a grey for the rest of a hash was
not readable on the green and the cyan fills (JP, 2026-10-06).
A keyless chip is white on light grey, the same pair one bank down. Our ``★``, a
faded hop, and the composer's slot have no fill of their own, because the console
has no dark grey background. They stand on the page (:data:`_SLOT_PAGE`): the star
in its yellow, a faded hop in the dark grey, the slot as a white ``+`` in a break of
the ribbon. The order of the rules is the same as on a truecolor terminal
(:func:`_spectrum_fill`).
"""
if hop.cursor:
return _ChipColours(_SLOT_PAGE, _SLOT_WHITE, _SLOT_WHITE)
if hop.style:
resolved = _style_hex(hop.style)
if resolved:
fill = DIM_TWIN.get(resolved, resolved)
ink = resolved if resolved in DIM_TWIN else _SLOT_WHITE
return _ChipColours(fill, ink, ink)
if hop.dim:
return _ChipColours(_SLOT_PAGE, _SLOT_FAINT, _SLOT_FAINT)
if hop.you:
return _ChipColours(_SLOT_PAGE, _SELF_INK, _SELF_INK)
hue = _style_hex(node_style(hop.key)) if hop.key else None
ink = hue if hue in DIM_TWIN else _SLOT_WHITE
return _ChipColours(DIM_TWIN[ink], ink, ink)
def _spectrum_thin_ink(before: _ChipColours, after: _ChipColours) -> str:
"""The thin chevron on a truecolor terminal: the previous fill, shaded."""
return _seam_ink(before.fill, after.fill)
def _slot_thin_ink(before: _ChipColours, after: _ChipColours) -> str:
"""The thin chevron on the console: the bright twin of the previous fill.
On a node's chip, this colour is the label's hue. On the page, it is the dark grey.
"""
return _BRIGHT_TWIN.get(before.fill, before.ink)
#: The colours of a chip, the colour of a thin seam, and the two prefixes of the chip
#: styles. They are bound at platform-switch time (:func:`_bind_chips`).
_colours_of: Callable[[PathHop], _ChipColours] = _spectrum_colours
_thin_ink: Callable[[_ChipColours, _ChipColours], str] = _spectrum_thin_ink
#: The weight of a chip label: ``bold`` on a truecolor terminal.
_WEIGHT = "bold "
#: The prefix of each other chip style: empty on a truecolor terminal.
_FLAT = ""
@on_platform
def _bind_chips(platform: Platform) -> None:
"""Bind the chip palette to the platform (runs now and on every switch).
On the 16-colour console, bold is brightness. Rich merges the base style of a row
into each span on it, so the ``bold`` of a selected row would move each dim fill on
the ribbon to its bright twin. Thus each chip style there says ``not bold``, and a
label gets no weight: its bright slot is its emphasis.
"""
global _colours_of, _thin_ink, _WEIGHT, _FLAT
if platform.truecolor:
_colours_of, _thin_ink = _spectrum_colours, _spectrum_thin_ink
_WEIGHT, _FLAT = "bold ", ""
else:
_colours_of, _thin_ink = _slot_colours, _slot_thin_ink
_WEIGHT = _FLAT = "not bold "
def _shorten(value: str, hash_bytes: int | None) -> str:
"""Bare lowercase hex, truncated to ``hash_bytes`` bytes (whole when falsy)."""
raw = value.lower().removeprefix("0x")
+4 -3
View File
@@ -42,9 +42,10 @@ Four sources of truth, in confidence order:
(:attr:`~meshterm.platforms.Platform.font`), every frame is folded down to that font
at the render boundary, whatever terminal started the app — so the inventory is the
answer, and asking the terminal would be asking the wrong glass. The Cardputer's panel
is Terminus, which carries the core chevrons, so its paths are chips; the PicoCalc's
512-glyph console font has no room for them, so its paths stay arrows. Nothing is
probed: :data:`~meshterm.ui.fontset.FONTS` already says what each font can draw.
is Terminus, which carries the core chevrons. The PicoCalc's 512-glyph console font
draws the two chevrons in two donor slots. Thus the paths are chips on both handhelds.
Nothing is probed: :data:`~meshterm.ui.fontset.FONTS` already says what each font can
draw.
* **The configured font**, resolved per terminal: Windows Terminal (``WT_SESSION`` +
``WT_PROFILE_ID`` → the profile's ``font.face`` in ``settings.json``), VS Code's
integrated terminal (``TERM_PROGRAM=vscode`` → ``terminal.integrated.fontFamily``,
+13
View File
@@ -253,6 +253,19 @@ _VT_SLOTS: tuple[tuple[int, str, str], ...] = (
(15, "white (you, heat.hot, the room mark)", "#ffffff"),
)
def slot_hex(slot: int) -> str:
"""The ``#rrggbb`` of one console palette slot (:data:`_VT_SLOTS`)."""
return _VT_SLOTS[slot][2]
#: Each bright console slot and its twin in the dim bank: slot ``n + 8`` and slot ``n``.
#: The VT draws a background only from slots 0 to 7. Thus, where a bright colour must
#: become a fill, its dim twin is the only background of the same hue. The path chips
#: use this pair on the console (:mod:`~meshterm.ui.pathline`): the dim twin is the fill,
#: and the bright slot is the text on it.
DIM_TWIN: dict[str, str] = {slot_hex(n + 8): slot_hex(n) for n in range(8)}
#: The custom 16-slot remap P3 originally shipped (tailwind-family RGBs programmed via
#: ``setvtrgb``) — **archived, not installed**: JP chose the standard palette but asked
#: to keep this in case he changes his mind. Reinstall with
@@ -170,6 +170,14 @@ MARKS = {
0x26BF: art([ # SQUARED KEY (drawn as a padlock) -- private channel
"......", "......", ".####.", ".#..#.", "######", "######",
"##..##", "##..##", "######", "######", "......", "......"]),
# The two powerline chevrons of the path chips (meshterm/ui/pathline.py). They fill
# the full cell, so that the point of one chip touches the fill of the next.
0xE0B0: art([ # POWERLINE SOLID RIGHT TRIANGLE: the seam between two chips
"#.....", "##....", "###...", "####..", "#####.", "######",
"######", "#####.", "####..", "###...", "##....", "#....."]),
0xE0B1: art([ # POWERLINE THIN RIGHT CHEVRON: the seam between two chips of one colour
"#.....", ".#....", "..#...", "...#..", "....#.", ".....#",
".....#", "....#.", "...#..", "..#...", ".#....", "#....."]),
}
# Donor codepoints whose glyph slots we may repurpose (glyphs MeshTerm never draws).
@@ -21,7 +21,7 @@
# one; point SHARED at it if it does not. The shared generator is additionally offered
# under GPL-2.0-only by its source file, permitting its extraction and use here.
#
# WHAT IS ONLY HERE. The GPL-2.0 base bitmap, its CP437 unicode mapping, and the same 18
# WHAT IS ONLY HERE. The GPL-2.0 base bitmap, its CP437 unicode mapping, and the same 20
# marks redrawn for the shorter cell (MARKS8).
#
# The 6x8 base has CP437 coverage, so unlike the Terminus base it has NO CYRILLIC: a node
@@ -72,7 +72,7 @@ exec(compile(_block.group(1), SHARED, "exec"), globals())
OUT8 = os.environ["OUT8"]
# The same 18 marks redrawn for the 6x8 cell (first-draft art; a later tweak round
# The same 20 marks redrawn for the 6x8 cell (first-draft art; a later tweak round
# refines whichever font wins the A/B).
MARKS8 = {
0x25CF: art([ # BLACK CIRCLE
@@ -111,6 +111,10 @@ MARKS8 = {
"..##..", "......", "#.##.#", "#.##.#", "......", "..##..", "......", "......"]),
0x26BF: art([ # SQUARED KEY -- padlock
".####.", ".#..#.", "######", "##..##", "##..##", "######", "######", "......"]),
0xE0B0: art([ # POWERLINE SOLID RIGHT TRIANGLE: the full cell
"##....", "###...", "#####.", "######", "######", "#####.", "###...", "##...."]),
0xE0B1: art([ # POWERLINE THIN RIGHT CHEVRON: on the edge of the solid triangle
"##....", ".##...", "...##.", "....##", "....##", "...##.", ".##...", "##...."]),
}
# --- the base bitmap: the Linux kernel's font_6x8 (GPL-2.0; see the header) ------------
+123 -2
View File
@@ -12,10 +12,14 @@ from __future__ import annotations
import random
import pytest
from rich.text import Text
import meshterm.ui.pathline as pathline
from meshterm.platforms import CARDPUTER_ZERO, set_platform
from meshterm.emulator import vt
from meshterm.emulator.vt import Style, Terminal
from meshterm.platforms import CARDPUTER_ZERO, PICOCALC_LYRA, set_platform
from meshterm.ui import oklab
from meshterm.ui.fontset import FONT_CODEPOINTS
from meshterm.ui.pathline import (
_DIM_BG,
_SELF_INK,
@@ -45,7 +49,8 @@ from meshterm.ui.pathline import (
path_line,
with_action_mark,
)
from meshterm.ui.theme import fold_text, node_style
from meshterm.ui.theme import DIM_TWIN, fold_text, node_style, slot_hex
from meshterm.ui.tui.render import render_to_ansi
from meshterm.ui.widgets import path_text
@@ -176,6 +181,122 @@ def test_the_cardputer_draws_chips_in_glyphs_its_font_has(monkeypatch: pytest.Mo
assert fold_text(ink) == ink
def _rgb(colour: str) -> tuple[int, int, int]:
return (int(colour[1:3], 16), int(colour[3:5], 16), int(colour[5:7], 16))
def _console_cells(row: Text) -> list[tuple[str, Style]]:
"""The cells that the PicoCalc console gets for one row, read back through a VT."""
palette = tuple(_rgb(slot_hex(n)) for n in range(16))
term = Terminal(53, 1, palette=palette)
term.feed(render_to_ansi(row, 53, no_wrap=True))
return term.screen[0]
def _selected(line: Text) -> Text:
"""``line`` on a selected row, whose base style is the bold ``cursor``."""
row = Text(style="cursor")
row.append("❯ ")
row.append_text(line)
return row
@pytest.fixture
def picocalc(monkeypatch: pytest.MonkeyPatch) -> None:
"""The PicoCalc, with the chip verdict that its own font gives."""
monkeypatch.delenv("MESHTERM_POWERLINE")
set_platform(PICOCALC_LYRA)
def test_the_picocalc_draws_chips_from_its_eight_backgrounds(picocalc: None) -> None:
"""A node's chip is the dim twin of its hue, and the label is the hue itself.
The VT draws a background only from slots 0 to 7. Thus each fill is a dim slot, and
each label is the bright slot that the name has in arrow mode. A keyless chip is
white on light grey. Our star and a faded hop stand on the page, because the console
has no dark grey background. The row is a selected row, whose ``bold`` base style
must not move a dim fill to its bright twin: each chip cell arrives without bold.
"""
hops = [
PathHop(SELF_GLYPH, you=True),
PathHop("Hilltop", key="1c"),
PathHop("Alice", key="0a", annotation="0a"),
PathHop("3d"),
PathHop("Ridge", key="1c", dim=True),
PathHop(SELF_GLYPH, you=True),
]
line = PathLine(hops).text() # auto: the console font has the chevrons
assert POWERLINE_SEP in line.plain
cells = _console_cells(_selected(line))
text = "".join(char for char, _ in cells)
dim_slots = {_rgb(slot_hex(n)) for n in range(8)}
for char, style in cells[2 : 2 + len(line.plain)]:
assert style.bg is None or style.bg in dim_slots, f"{char!r} on {style.bg}"
assert not style.flags & vt.BOLD, f"{char!r} is bold, so a dim slot turns bright"
def at(word: str) -> Style:
return cells[text.index(word)][1]
yellow, red = _style_hex(node_style("1c")), _style_hex(node_style("0a"))
assert (at("Hilltop").fg, at("Hilltop").bg) == (_rgb(yellow), _rgb(DIM_TWIN[yellow]))
assert (at("Alice").fg, at("Alice").bg) == (_rgb(red), _rgb(DIM_TWIN[red]))
assert at("(0a)").fg == _rgb(red) # the annotation is in the label's colour too
assert (at("3d").fg, at("3d").bg) == (_rgb(slot_hex(15)), _rgb(slot_hex(7)))
assert at("Ridge").bg in (None, _rgb(slot_hex(0))) # faded: on the page
assert at("Ridge").fg == _rgb(slot_hex(8))
assert at(SELF_GLYPH).bg in (None, _rgb(slot_hex(0)))
def test_a_picocalc_hash_label_is_one_colour(picocalc: None) -> None:
"""The rest of a hash is in the hue of its lit bytes, not in a grey.
A grey after the lit byte was not readable on the green and the cyan fills
(JP, 2026-10-06).
"""
for key in ("0a", "1c", "44", "70", "9a", "c4"):
line = PathLine([PathHop(f"{key}63c6", key=key, lit_bytes=1)]).text()
cells = _console_cells(line)
hue = _rgb(_style_hex(node_style(key)))
label = [style.fg for char, style in cells if char in f"{key}63c6" and char != " "]
assert label == [hue] * 6, key
def test_picocalc_seams_on_one_colour_and_the_composer_slot(picocalc: None) -> None:
"""Two chips of one colour join on a thin chevron in the hue. The slot is a break.
The thin chevron is the bright twin of the fill: the colour of the label on the
chip. The composer's slot has no white background on the console, so it is a white
``+`` on the page, and the chips on each side close on a point and open on a notch.
"""
same = PathLine([PathHop("Mill", key="1a"), PathHop("Tower", key="20")]).text()
seam = same.plain.index(POWERLINE_THIN)
cell = _console_cells(same)[seam][1]
yellow = _style_hex(node_style("1a"))
assert (cell.fg, cell.bg) == (_rgb(yellow), _rgb(DIM_TWIN[yellow]))
slot = [
PathHop("Hilltop", key="1c"),
PathHop(CURSOR_GLYPH, cursor=True),
PathHop("Alice", key="0a"),
]
line = PathLine(slot).text()
cells = _console_cells(line)
plus = cells[line.plain.index(CURSOR_GLYPH)][1]
assert plus.fg == _rgb(slot_hex(15)) and plus.bg in (None, _rgb(slot_hex(0)))
assert line.plain.count(POWERLINE_SEP) == 2 # a point into the break, a notch out of it
def test_every_picocalc_chip_glyph_is_in_its_font(picocalc: None) -> None:
"""Each mark of the chip language is in the console font, so nothing folds to ``?``."""
hops = [PathHop(SELF_GLYPH, you=True), PathHop("Hilltop", key="1c"), elision_hop()]
line = PathLine([*hops, PathHop("a"), PathHop("b")]).text()
drawn = [line, cut_to(line, 12), cut_mark(line, 1, ELIDE_HEAD)]
ink = "".join(text.plain for text in drawn)
assert {POWERLINE_SEP, POWERLINE_THIN, CRACK_TAIL, CRACK_HEAD, SELF_GLYPH} <= set(ink)
assert all(ord(char) in FONT_CODEPOINTS for char in ink)
assert fold_text(ink) == ink
def test_the_cursor_is_a_hop_of_its_own_in_either_mode() -> None:
"""The composer's insertion point is a slot *in* the route, not a gap between hops.
+6 -4
View File
@@ -256,15 +256,17 @@ def test_powerline_support_kitty_ssh_and_unknown() -> None:
def test_powerline_support_a_handheld_answers_from_its_own_font() -> None:
"""A handheld's font draws the screen, so its inventory answers and no terminal does.
The Cardputer's Terminus carries the core chevrons and no rounded caps; the PicoCalc's
512-glyph console font has neither, so a kitty that started it changes nothing. The
override still outranks the font, as it outranks every probe.
The Cardputer's Terminus and the PicoCalc's console font both have the core chevrons
and no rounded caps. A font without the chevrons gets ``none``, also when a kitty
started the app. The override still outranks the font, as it outranks each probe.
"""
kitty = {"TERM": "xterm-kitty"} # a terminal that would otherwise earn core
cardputer = _powerline_support(kitty, handheld=("cardputer-zero", CARDPUTER_ZERO_CODEPOINTS))
assert (cardputer.level, cardputer.source) == (CORE, "platform:cardputer-zero")
picocalc = _powerline_support(kitty, handheld=("picocalc-lyra", FONT_CODEPOINTS))
assert (picocalc.level, picocalc.source) == (NONE, "platform:picocalc-lyra")
assert (picocalc.level, picocalc.source) == (CORE, "platform:picocalc-lyra")
bare = _powerline_support(kitty, handheld=("bare", frozenset(range(0x20, 0x7F))))
assert (bare.level, bare.source) == (NONE, "platform:bare")
capped = CARDPUTER_ZERO_CODEPOINTS | {0xE0B4, 0xE0B6}
assert _powerline_support({}, handheld=("capped", capped)).level == FULL
pinned = {"MESHTERM_POWERLINE": "0"}