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:
1 parent
c07f15f4ec
commit
69d9836546
9 files changed
+350
-71
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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``,
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in new issue
Block a user