A path chip is the node's name colour on a darker ground of itself

On the desktop and the Cardputer, a chip was the node's hue with dark
ink on it, so a node's colour moved from its name to the ground behind
it the moment a route drew it. Now the label is the hue, the colour the
name has in every list and in arrow mode, and the fill is the same hue
at 0.60 of its OKLab lightness (CHIP_LIGHTNESS): the rule the PicoCalc
draws with its dim twins, at full resolution. A hash label and an
annotation are the label colour throughout, with the lit bytes bold.
A keyless chip is the same pair in grey; our star, a faded hop and the
composer's white slot keep what they had.

The value came from two ladders over twelve hues. A fixed step down in
lightness made the blue fills almost black (blue is the wheel's darkest
hue), one lightness for every fill made the blue label hard to read on
its own ground, and 0.65 / 0.55 were too weak in blue / too dull in
yellow. Darkening at constant chroma leaves sRGB, and the clip merged
neighbouring greens into one #008000, so oklab.fitted lowers the chroma
instead and keeps the hue.

A thin seam between two chips of one colour is now drawn in the
previous chip's label colour, the light half of its own pair, on both
palettes; the shaded-fill rule (SEAM_SHADE) is gone with it.

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:25:13 -04:00
1 parent 69d9836546
commit 353dcb7792
7 files changed
+209 -137

No files matched your search

+12 -2
View File
@@ -409,13 +409,23 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
name: the one node the reader never has to be told, and the cells belong to the hops
that differ from row to row. As a chip it is the map's yellow star on neutral dark
grey, padded like every other chip.
- **A path chip is the node's name colour on a darker ground of itself.** The label is the
hue `node_style` gives the name on every other surface, so a node is one colour in a
list, in arrows and in a chip; the fill is that hue at `pathline.CHIP_LIGHTNESS` of its
OKLab lightness, chroma lowered only as far as sRGB needs (`oklab.fitted`; a clip there
merged neighbouring greens). The PicoCalc is the same rule at its resolution: the fill
is the bright slot's dim twin (`theme.DIM_TWIN`). A hash label and an annotation are
the label colour throughout, never a grey; on a truecolor terminal the lit bytes are
bold. A keyless chip is the same pair in grey; our `★` and a faded hop keep their dark
greys; the composer's slot keeps the white chip (on the PicoCalc, a white `+` in a
break of the ribbon).
- Chip seams are **one** interlocked chevron (previous fill on next). Two chips of the
same fill are the exception the interlock can't draw — and so are two fills the eye
can't tell apart: under `pathline.SEAM_BLUR` apart in OKLab (`ui/oklab.py`, THE
perceptual colour distance — never a hue gap, never sRGB, both of which mis-size the
greens against the cyans). There the seam is the **thin** chevron (`POWERLINE_THIN`)
in the previous chip's own fill shaded `SEAM_SHADE` darker (lighter for a dark fill),
drawn on the next, so the ribbon runs on unbroken and the join is a line the chip draws
in the previous chip's label colour, drawn on the next, so the ribbon runs on unbroken
and the join is a line the chip draws
on itself, never a wedge of page cut out of the route and never a third colour. An
elision breaks the ribbon rather than joining it — bare `⋯`
on the page between a closing point and the next chip's notch, no fill, no padding.
+34 -2
View File
@@ -65,17 +65,49 @@ def to_hex(lab: Lab) -> str:
clipped colour is the nearest the terminal can show, and it stays the right hue to
the eye at the shifts the app asks for.
"""
linear = _linear_rgb(lab)
return "#" + "".join(f"{round(_encoded(min(1.0, max(0.0, c))) * 255):02x}" for c in linear)
def _linear_rgb(lab: Lab) -> tuple[float, float, float]:
"""OKLab → linear sRGB. A channel outside 0 to 1 is outside the sRGB gamut."""
lightness, a, b = lab
l_ = lightness + 0.3963377774 * a + 0.2158037573 * b
m_ = lightness - 0.1055613458 * a - 0.0638541728 * b
s_ = lightness - 0.0894841775 * a - 1.2914855480 * b
lms = (l_**3, m_**3, s_**3)
linear = (
return (
4.0767416621 * lms[0] - 3.3077115913 * lms[1] + 0.2309699292 * lms[2],
-1.2684380046 * lms[0] + 2.6097574011 * lms[1] - 0.3413193965 * lms[2],
-0.0041960863 * lms[0] - 0.7034186147 * lms[1] + 1.7076147010 * lms[2],
)
return "#" + "".join(f"{round(_encoded(min(1.0, max(0.0, c))) * 255):02x}" for c in linear)
def _in_gamut(lab: Lab) -> bool:
"""Whether the sRGB gamut holds ``lab`` (a very small tolerance for rounding)."""
return all(-1e-6 <= channel <= 1 + 1e-6 for channel in _linear_rgb(lab))
def fitted(lab: Lab) -> Lab:
"""``lab`` with its chroma lowered until the sRGB gamut holds it.
The lightness and the hue do not change. A colour that is already in the gamut comes
back unchanged. Use this instead of the clip in :func:`to_hex` when a set of colours
must stay apart: a dark, saturated colour leaves the gamut, and the clip moves many
such colours to one corner of the gamut. For example, two greens a few steps apart
on the hue wheel both clip to ``#008000``.
"""
if _in_gamut(lab):
return lab
lightness, a, b = lab
low, high = 0.0, 1.0
for _ in range(24): # the chroma scale, to approximately 1e-7
middle = (low + high) / 2
if _in_gamut((lightness, a * middle, b * middle)):
low = middle
else:
high = middle
return (lightness, a * low, b * low)
def distance(first: str, second: str) -> float:
+83 -90
View File
@@ -12,16 +12,17 @@ drawn*, so every surface the survey found can eventually route through it:
look, unchanged. ``powerline`` renders each hop as a colour-filled chip, joined by one
solid triangle U+E0B0 interlocked between them — the chip behind as its foreground, the
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. (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
chevron: the oh-my-posh look, at one cell a seam. The label of a node's chip is its
hash-derived hue (:func:`~meshterm.ui.theme.node_style`), the colour that its name has
on each surface, and the fill is that hue, darker (:data:`CHIP_LIGHTNESS`). Our own
node is the map's yellow ``★`` on a neutral dark grey, a faded hop is dark slate, and a
keyless hop is light grey on grey. (On the 16-colour console of the PicoCalc, the fill
is the dim twin of the hue. 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
draw: the seam is the *thin* chevron instead, the previous fill shaded a step darker
(lighter, for a dark fill) and drawn on the next, so the ribbon runs on unbroken and the
draw: the seam is the *thin* chevron instead, in the colour of the previous chip's
label and drawn on the next, so the ribbon runs on unbroken and the
join is a line the chip draws on itself (:meth:`PathLine._seam`). Hops elided out of
the middle break the
ribbon instead of joining it: the mark sits bare on the page between a closing point and
@@ -140,13 +141,43 @@ _ARROW = " → "
#: (:func:`~meshterm.ui.tui.render._whiten_identities`) instead of whitening a whole route.
PATH_INK = "pathline"
#: Chip ink: near-black slate for readable text on every spectrum hue and on white.
#: The lightness of a node's chip, as a part of the lightness of its hue (OKLab ``L``).
#: On a truecolor terminal, the fill of a node's chip is its hue with ``L`` multiplied by
#: this value, and the label on it is the hue itself: the colour that the name has in each
#: list and in arrow mode. Thus a node is one colour on each surface, and a chip only puts
#: a darker ground of that colour under it. JP chose this look on the PicoCalc
#: (2026-10-06), where the dim twin of a slot is the same rule at the resolution of the
#: console (:data:`~meshterm.ui.theme.DIM_TWIN`). Its dim twins have 0.55 to 0.73 of the
#: lightness of their bright slots.
#:
#: The value came from two ladders of rules over twelve hues. A fixed step down in ``L``
#: made the blue fills almost black, because the blue hue is the darkest on the wheel.
#: One ``L`` for all fills made the blue label hard to read on its fill. ``0.65`` made the
#: blue label weak, and ``0.55`` made the yellow fills dull. The fill also keeps its hue
#: when the darker colour leaves the sRGB gamut: the chroma comes down, refer to
#: :func:`~meshterm.ui.oklab.fitted`. A clip there made the greens near ``0x50`` one colour.
CHIP_LIGHTNESS = 0.60
def _chip_fill(colour: str) -> str:
"""The fill of a chip whose label is ``colour``: the same hue, darker."""
lightness, a, b = oklab.from_hex(colour)
return oklab.to_hex(oklab.fitted((lightness * CHIP_LIGHTNESS, a, b)))
def _chip_label(fill: str) -> str:
"""The label for a chip whose fill is ``fill``: the reverse of :func:`_chip_fill`."""
lightness, a, b = oklab.from_hex(fill)
return oklab.to_hex(oklab.fitted((min(1.0, lightness / CHIP_LIGHTNESS), a, b)))
#: The text of the composer's slot: near-black slate on the white chip.
_CHIP_FG = "#0f172a"
#: Chip ink, softened — annotations and a hash label's unlit tail (the two-tone).
_CHIP_FG_SOFT = "#334155"
#: Chip fill for a keyless hop — the ``faint`` grey; colour stays reserved for
#: keyed identities in chips just as it is in arrow mode.
_KEYLESS_BG = "#64748b"
#: The label of a keyless chip: the same pair as a node's chip, in grey.
_KEYLESS_FG = _chip_label(_KEYLESS_BG)
#: Our own node's chip fill: a neutral dark grey, so the yellow ``★`` riding it reads as
#: the map's marker rather than as a node's hue (JP, 2026-08-09). Deliberately *not* a
#: spectrum colour and not the white it used to be — our end of a route is a fixture the
@@ -166,9 +197,6 @@ _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):
@@ -313,28 +341,12 @@ def _style_hex(style: str) -> str | None:
#: doesn't; the two greys, ``0.18`` apart, keep their interlock on the metric alone.
SEAM_BLUR = 0.06
#: How far the thin chevron's colour steps from the fill it is drawn in, in OKLab ``L``
#: — a lightness shift alone, so it reads as *that chip's colour, shaded* rather than as
#: a third colour on the line. The step is taken from the previous fill but measured from
#: whichever of the two fills is further along (:func:`~meshterm.ui.oklab.shaded`'s
#: ``floor``), so the mark clears the fill it actually sits on by this much even when the
#: two differ by everything :data:`SEAM_BLUR` allows. A lightness step is what a thin line
#: is seen by (the eye resolves detail in luminance, not in chroma), so it needs nothing
#: like the blur distance to read: ``0.10`` is five noticeable differences, a clear line
#: that is still plainly the chip's own colour, chosen on the same ladder (JP, 2026-09-16).
SEAM_SHADE = 0.10
def _fills_blur(before: str, after: str) -> bool:
"""Whether two fills are the same to the eye: identical, or under :data:`SEAM_BLUR`."""
return before == after or oklab.distance(before, after) < SEAM_BLUR
def _seam_ink(before: str, after: str) -> str:
"""The thin chevron's colour: ``before`` shaded :data:`SEAM_SHADE` past both fills."""
return oklab.shaded(before, SEAM_SHADE, floor=oklab.from_hex(after)[0])
#: A chip's fill, as it appears in a rendered span's style — the ``on #rrggbb`` half.
#: Only :meth:`PathLine._chip` sets a background, which is what makes this the test for
#: "is there a chip here": a seam's point, a notch, and every arrow-mode span carry a
@@ -1055,18 +1067,16 @@ class PathLine:
one shade of green on the next, so the test is a perceptual distance under
:data:`SEAM_BLUR` (:func:`_fills_blur`), not equality and not a hue gap.
There the seam is the **thin** chevron, :data:`POWERLINE_THIN`, in the previous
chip's own fill shaded a step darker — lighter, for a dark fill — and drawn on
the fill ahead (:func:`_seam_ink`, JP, 2026-09-16): powerline's own mark for a
join inside one colour, and the chip it belongs to drawing a line on itself, so
the ribbon runs on unbroken and nothing on it is a colour that isn't a chip's.
The solid point drawn bare used to cut a wedge of page out of the route here (a
run of near hues read as dashes, and on a light terminal the wedge was a hole),
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`).
There the seam is the **thin** chevron, :data:`POWERLINE_THIN`, drawn on the fill
ahead in the colour of the previous chip's label: powerline's own mark for a join
inside one colour, and the chip it belongs to drawing a line on itself, so the
ribbon runs on unbroken and nothing on it is a colour that isn't a chip's. The
solid point drawn bare used to cut a wedge of page out of the route here (a run of
near hues read as dashes, and on a light terminal the wedge was a hole). The thin
chevron was then the previous fill, a lightness step darker (JP, 2026-09-16). When
the label became the node's hue on a darker fill of itself (2026-10-06), the label
colour became the line: it is the light half of the same pair, so it is visible
on each fill and is still the chip's own colour. One cell either way.
Args:
before: The colours of the previous chip. The point is drawn in its fill.
@@ -1076,8 +1086,7 @@ class PathLine:
The seam's single styled cell.
"""
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_THIN, style=f"{_FLAT}{before.ink} on {after.fill}")
return Text(POWERLINE_SEP, style=f"{_FLAT}{before.fill} on {after.fill}")
@staticmethod
@@ -1128,38 +1137,38 @@ class PathLine:
return text
def _spectrum_fill(hop: PathHop) -> str:
"""A chip's fill: cursor accent, override, dim slate, our grey, hue, keyless grey.
def _spectrum_colours(hop: PathHop) -> _ChipColours:
"""The colours of a chip on a truecolor terminal: the hue on a darker fill of itself.
A node's label is its hue: the colour that its name has in each list and in arrow
mode. The fill is that hue, darker (:data:`CHIP_LIGHTNESS`). The whole
label is in the hue, a hash label and an annotation too, and the lit bytes of a hash
are bold. A keyless chip is the same pair in grey. Our ``★`` and a faded hop keep their
dark greys, which already carry a light label. The PicoCalc draws the same rules at
the resolution of its console (:func:`_slot_colours`).
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.
than as a hop with an unlucky hue (white sits outside the node spectrum, and its
dark text stays readable on it). It is the only light chip on the ribbon. 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
return _ChipColours(_style_hex("cursor") or _KEYLESS_BG, _CHIP_FG, _CHIP_FG)
resolved = _style_hex(hop.style) if hop.style else None
if resolved:
return _ChipColours(_chip_fill(resolved), resolved, resolved)
if hop.dim:
return _DIM_BG
return _ChipColours(_DIM_BG, _DIM_FG, _DIM_FG)
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)
return _ChipColours(_YOU_BG, _SELF_INK, _SELF_INK)
hue = _style_hex(node_style(hop.key)) if hop.key else None
if hue:
return _ChipColours(_chip_fill(hue), hue, hue)
return _ChipColours(_KEYLESS_BG, _KEYLESS_FG, _KEYLESS_FG)
def _slot_colours(hop: PathHop) -> _ChipColours:
@@ -1177,7 +1186,7 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
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`).
(:func:`_spectrum_colours`).
"""
if hop.cursor:
return _ChipColours(_SLOT_PAGE, _SLOT_WHITE, _SLOT_WHITE)
@@ -1196,23 +1205,9 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
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`).
#: The colours of a chip, 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.
@@ -1228,13 +1223,11 @@ def _bind_chips(platform: Platform) -> None:
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
global _colours_of, _WEIGHT, _FLAT
if platform.truecolor:
_colours_of, _thin_ink = _spectrum_colours, _spectrum_thin_ink
_WEIGHT, _FLAT = "bold ", ""
_colours_of, _WEIGHT, _FLAT = _spectrum_colours, "bold ", ""
else:
_colours_of, _thin_ink = _slot_colours, _slot_thin_ink
_WEIGHT = _FLAT = "not bold "
_colours_of, _WEIGHT, _FLAT = _slot_colours, "not bold ", "not bold "
def _shorten(value: str, hash_bytes: int | None) -> str:
+4 -5
View File
@@ -557,14 +557,13 @@ def name_chip(label: str, key: str | None = None, *, you: bool = False) -> Text:
:class:`~meshterm.ui.pathline.PathLine` and asks it for its line, so the fill, the
ink, the padding, the rounded cap and the closing point are all whatever a route's
segments are wearing today, and can never drift from them (JP, 2026-08-12). The colour
reads exactly as it does in a route — the *fill* carries the node's hue with dark ink
on it, a node no key can place takes the keyless grey, and our own end is the map's
``★`` on its neutral dark grey rather than a name.
reads exactly as it does in a route. The name is in the node's hue, on a darker fill
of that hue. A node that no key can place takes the keyless grey, and our own end is
the map's ``★`` on its neutral dark grey instead of a name.
Degrading is inherited too: where the terminal can't draw powerline separators, a path
line is arrow-mode text and a lone hop is simply the name in its own hue — which is
what a sender label was before it was framed, and what the PicoCalc console (whose font
carries no separator glyph) goes on drawing.
what a sender label was before it was framed.
Args:
label: The name as it should read (already resolved; this never renames it).
+19
View File
@@ -61,6 +61,25 @@ def test_shaded_floor_clears_a_second_colour_too() -> None:
assert oklab.from_hex(floored)[0] >= oklab.from_hex(oklab.shaded(slate, 0.15))[0]
def test_fitted_lowers_chroma_alone_and_keeps_near_hues_apart() -> None:
"""A colour outside the gamut comes back inside it, with its lightness and hue.
A colour in the gamut comes back unchanged. The clip in ``to_hex`` gave two dark
greens near ``0x50`` almost one colour. After the fit, they are clearly two colours.
"""
inside = oklab.from_hex(_fill("80"))
assert oklab.fitted(inside) == inside
darker = [oklab.from_hex(_fill(key)) for key in ("4c", "5c")]
outside = [(lab[0] * 0.6, lab[1], lab[2]) for lab in darker]
fits = [oklab.fitted(lab) for lab in outside]
clipped = oklab.distance(*(oklab.to_hex(lab) for lab in outside))
assert oklab.distance(*(oklab.to_hex(lab) for lab in fits)) > 2 * clipped
for before, after in zip(outside, fits, strict=True):
assert after[0] == before[0]
assert after[1] / before[1] == pytest.approx(after[2] / before[2]) # the same hue
assert abs(after[1]) < abs(before[1]) # less chroma
def test_from_hex_rejects_anything_but_six_digits() -> None:
"""Six hex digits, ``#`` and case optional; anything else is a ``ValueError``."""
with pytest.raises(ValueError):
+51 -34
View File
@@ -22,8 +22,10 @@ from meshterm.ui import oklab
from meshterm.ui.fontset import FONT_CODEPOINTS
from meshterm.ui.pathline import (
_DIM_BG,
_DIM_FG,
_SELF_INK,
_YOU_BG,
CHIP_LIGHTNESS,
CRACK_HEAD,
CRACK_TAIL,
CURSOR_GLYPH,
@@ -35,12 +37,10 @@ from meshterm.ui.pathline import (
POWERLINE_SEP,
POWERLINE_THIN,
SEAM_BLUR,
SEAM_SHADE,
SELF_GLYPH,
WRAP_OFFSET,
PathHop,
PathLine,
_seam_ink,
_style_hex,
cut_mark,
cut_to,
@@ -54,6 +54,11 @@ from meshterm.ui.tui.render import render_to_ansi
from meshterm.ui.widgets import path_text
def _fill(key: str) -> str:
"""The fill of a node's chip on a truecolor terminal: its hue, darker."""
return pathline._chip_fill(_style_hex(node_style(key)))
def _styles(text) -> dict[str, str]: # noqa: ANN001
"""Map each styled slice of a Text to its style string, for spot checks."""
return {text.plain[s.start : s.end]: str(s.style) for s in text.spans}
@@ -132,7 +137,7 @@ def test_chips_are_joined_by_one_interlocked_chevron() -> None:
the route continues, so a finished path never wears one. It ends square on the last
chip's own pad, and the only chevrons in the line are the joins.
"""
alice_fill = node_style("aa").split()[-1]
alice_fill = _fill("aa")
line = PathLine([PathHop("Alice", key="aa"), PathHop("you", you=True)], mode="powerline")
text = line.text()
assert text.plain == f" Alice {POWERLINE_SEP} you "
@@ -143,16 +148,37 @@ def test_chips_are_joined_by_one_interlocked_chevron() -> None:
def test_chips_keep_the_same_words_and_honour_style_overrides() -> None:
"""Chips change colours and separators, never the words.
An explicit style override (hex or theme name) becomes the chip fill.
An explicit style override (hex or theme name) becomes the chip's colour: the label
is in it, on a darker fill of it, as a node's hue is.
"""
hops = [PathHop("Hub", key="3d", annotation="3d"), PathHop("you", you=True)]
plain = PathLine(hops, mode="plain").text().plain
chips = PathLine(hops, mode="powerline").text().plain
assert plain.replace(" → ", " ") == chips.replace(POWERLINE_SEP, "").replace(" ", " ").strip()
themed = PathLine([PathHop("X", style="brand")], mode="powerline").text()
assert pathline._fills(themed)[-1] == "#5eead4" # the chip runs to the very last cell
brand = pathline._chip_fill("#5eead4")
assert pathline._fills(themed)[-1] == brand # the chip runs to the very last cell
assert any(str(s.style) == f"bold #5eead4 on {brand}" for s in themed.spans)
hexed = PathLine([PathHop("X", style="bold #123456")], mode="powerline").text()
assert pathline._fills(hexed)[-1] == "#123456"
assert pathline._fills(hexed)[-1] == pathline._chip_fill("#123456")
def test_a_chip_is_the_name_colour_on_a_darker_fill_of_it() -> None:
"""A node's label is its hue, the colour of its name on each surface.
The fill is the same hue, darker (``CHIP_LIGHTNESS`` of its OKLab lightness). A hash
label and an annotation are in the hue too, with only the lit bytes in bold. Where
the darker colour leaves the sRGB gamut, the chroma comes down and the hue stays, so
two greens a few steps apart on the wheel keep two fills.
"""
hue = _style_hex(node_style("3d"))
line = PathLine([PathHop("3d63c6", key="3d", lit_bytes=1, annotation="3d")], mode="powerline")
styles = {line.text().plain[s.start : s.end]: str(s.style) for s in line.text().spans}
assert styles["3d"] == f"bold {hue} on {_fill('3d')}"
assert styles["63c6"] == styles[" (3d)"] == f"{hue} on {_fill('3d')}"
fill = oklab.from_hex(_fill("3d"))
assert fill[0] == pytest.approx(oklab.from_hex(hue)[0] * CHIP_LIGHTNESS, abs=0.01)
assert _fill("4c") != _fill("5c") # a clip made both #008000
def test_auto_mode_follows_the_terminal_verdict(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -374,7 +400,7 @@ def test_cut_to_cracks_a_chip_in_its_own_fill_instead_of_ellipsizing() -> None:
assert fitted.plain.endswith(CRACK_TAIL)
assert "…" not in fitted.plain
# The cut lands inside ``BBBB``, so the crack wears BBBB's hue, not its neighbours'.
assert str(fitted.spans[-1].style) == _style_hex(node_style("22bb"))
assert str(fitted.spans[-1].style) == _fill("22bb")
assert cut_to(full, 200) is full # fits → untouched, no copy, no mark
@@ -399,13 +425,13 @@ def test_cut_mark_mirrors_itself_and_reads_the_visible_side() -> None:
body = full.plain.index("AAAA") # inside the first chip, either way you scan
head, tail = cut_mark(full, body, ELIDE_HEAD), cut_mark(full, body, ELIDE_TAIL)
assert (head.plain, tail.plain) == (CRACK_HEAD, CRACK_TAIL)
assert str(head.style) == str(tail.style) == _style_hex(node_style("11aa"))
assert str(head.style) == str(tail.style) == _fill("11aa")
# A cut landing on the interlocked seam itself cracks in the field that cell carries —
# the chip *ahead*, whose fill is literally the seam's background — so the mark reads
# as the next segment beginning and being sheared, not as a colour off the line.
seam = full.plain.index(POWERLINE_SEP)
assert str(cut_mark(full, seam, ELIDE_TAIL).style) == _style_hex(node_style("22bb"))
assert str(cut_mark(full, seam, ELIDE_TAIL).style) == _fill("22bb")
def test_cut_mark_falls_back_to_the_ellipsis_off_a_chip() -> None:
@@ -665,7 +691,7 @@ def test_wrapped_chip_lines_open_on_the_break_they_continue() -> None:
# …past the line's own PATH_INK stamp, which covers the whole body from the same cell
# (it marks the run as a path line for the cursor fold and draws nothing).
carried = next(s for s in lines[1].spans if s.start == step and str(s.style) != PATH_INK)
assert str(carried.style) == f"{_style_hex(node_style('3d'))} reverse"
assert str(carried.style) == f"{_fill('3d')} reverse"
assert lines[0].plain.endswith(POWERLINE_SEP) # …the path runs on past this line
assert lines[-1].plain.endswith(" ") # …and stops square on the last chip's pad
assert all(line.cell_len <= 20 for line in lines)
@@ -722,7 +748,7 @@ def test_a_line_drawing_only_a_route_middle_wears_the_chevron_at_that_end(monkey
assert text.plain.startswith(POWERLINE_SEP + " AAAA") # the notch, not the lozenge
assert text.plain.endswith("BBBB " + POWERLINE_SEP) # the point: the route goes on
opening = next(s for s in text.spans if s.start == 0 and str(s.style) != PATH_INK)
assert str(opening.style) == f"{_style_hex(node_style('aa'))} reverse" # cut out of AAAA
assert str(opening.style) == f"{_fill('aa')} reverse" # cut out of AAAA
# One end at a time: the half that *is* an endpoint keeps its cap either way.
head = PathLine(hops, mode="powerline", to_destination=False).text().plain
@@ -794,14 +820,14 @@ def _seams(*hops: PathHop) -> list[tuple[str, str]]:
]
def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_shaded() -> None:
"""Where two chips land on the same fill, the seam is the thin chevron in that fill, shaded.
def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_in_its_label() -> None:
"""Where two chips land on the same fill, the seam is the thin chevron in the label colour.
The interlock can't draw a solid point in its own background, so the join is drawn
*on* the shared fill instead — powerline's own mark for a join inside one colour —
and the ribbon runs on unbroken. Its colour is the previous chip's own, a lightness
step darker, so nothing on the line is a colour that isn't a chip's; a dark fill (the
faded slate) steps lighter instead.
and the ribbon runs on unbroken. Its colour is the label of the previous chip: the
light half of the chip's own pair, so it is visible on the fill, and nothing on the
line is a colour that is not a chip's. The faded slate draws it in its muted text.
Not a rare accident, either: a mirrored return leg is a run of identically faded hops
by construction, and a stretch of keyless hops shares one grey. Either way it stays a
@@ -809,21 +835,15 @@ def test_a_seam_between_two_of_one_colour_is_the_thin_chevron_shaded() -> None:
"""
hue = _style_hex(node_style("aa"))
glyph, style = _seams(PathHop("A", key="aa"), PathHop("B", key="aa"))[0]
assert glyph == POWERLINE_THIN and style == f"{_seam_ink(hue, hue)} on {hue}"
shade = _seam_ink(hue, hue)
assert oklab.from_hex(shade)[0] < oklab.from_hex(hue)[0] # darker…
assert oklab.distance(shade, hue) == pytest.approx(SEAM_SHADE, abs=0.01) # …by the step
assert oklab.distance(shade, hue) < oklab.distance(hue, _style_hex(node_style("77")))
assert (glyph, style) == (POWERLINE_THIN, f"{hue} on {_fill('aa')}")
assert _seams(PathHop("A", key="aa"), PathHop("B", key="77"))[0] == (
POWERLINE_SEP,
f"{hue} on {_style_hex(node_style('77'))}",
f"{_fill('aa')} on {_fill('77')}",
) # blended, one cell
glyph, style = _seams(PathHop("A", dim=True), PathHop("B", dim=True))[0]
assert glyph == POWERLINE_THIN and style.endswith(f" on {_DIM_BG}")
lighter = style.split()[0]
assert oklab.from_hex(lighter)[0] > oklab.from_hex(_DIM_BG)[0] # a dark fill steps up
assert (glyph, style) == (POWERLINE_THIN, f"{_DIM_FG} on {_DIM_BG}")
def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
@@ -831,19 +851,16 @@ def test_a_seam_blurs_by_perceived_distance_not_by_hue_gap() -> None:
The eye, not the wheel: sixteen first-byte steps across the greens are one colour to
a reader and sixteen across the reds are two, so the same hue gap blurs on one side
of the wheel and interlocks on the other. The greys follow the same metric with no
exemption: the keyless grey and the faded slate sit well apart and keep their seam.
of the wheel and interlocks on the other. The test measures the fills, because the
solid point is a fill on a fill. The greys follow the same metric with no exemption:
the keyless grey and the faded slate sit well apart and keep their seam.
"""
green = _seams(PathHop("A", key="4c"), PathHop("B", key="5c"))[0]
red = _seams(PathHop("A", key="00"), PathHop("B", key="10"))[0]
assert green[0] == POWERLINE_THIN and red[0] == POWERLINE_SEP
assert oklab.distance(_style_hex(node_style("4c")), _style_hex(node_style("5c"))) < SEAM_BLUR
assert oklab.distance(_style_hex(node_style("00")), _style_hex(node_style("10"))) > SEAM_BLUR
# The shade is taken from the chip behind but clears the chip ahead as well.
before, after = _style_hex(node_style("4c")), _style_hex(node_style("5c"))
assert green[1] == f"{_seam_ink(before, after)} on {after}"
assert oklab.distance(_seam_ink(before, after), after) >= SEAM_SHADE - 1e-6
assert oklab.distance(_fill("4c"), _fill("5c")) < SEAM_BLUR
assert oklab.distance(_fill("00"), _fill("10")) > SEAM_BLUR
assert green[1] == f"{_style_hex(node_style('4c'))} on {_fill('5c')}"
wrapped = _seams(PathHop("A", key="fc"), PathHop("B", key="02"))[0]
assert wrapped[0] == POWERLINE_THIN # neighbours across the wheel's seam, too
+6 -4
View File
@@ -17,6 +17,7 @@ from meshterm.ui.pathline import (
POWERLINE_ROUND_OPEN,
PathHop,
PathLine,
_chip_fill,
)
from meshterm.ui.theme import node_style
from meshterm.ui.widgets import (
@@ -242,15 +243,16 @@ def test_name_chip_is_a_path_line_of_one_hop() -> None:
]
def test_name_chip_wears_the_hue_as_its_fill_padded_like_every_chip(powerline) -> None:
"""A route's colour scheme: the node's hue fills the chip, the ink on it is dark."""
def test_name_chip_wears_the_hue_as_its_label_padded_like_every_chip(powerline) -> None:
"""A route's colour scheme: the name is in its hue, on a darker fill of that hue."""
powerline(True)
chip = name_chip("Alice", "a1b2")
assert chip.plain.strip(POWERLINE_ROUND_OPEN + POWERLINE_ROUND_CLOSE) == " Alice "
ink = _span_style(chip, "Alice")
assert ink.bgcolor.name == node_style("a1b2").removeprefix("bold ") # the hue
assert ink.color.name == "#0f172a" # the chip ink every hop is lettered in
hue = node_style("a1b2").removeprefix("bold ")
assert ink.color.name == hue # the colour of the name on each surface
assert ink.bgcolor.name == _chip_fill(hue) # the same hue, darker
def test_name_chip_draws_us_as_the_star_never_as_our_name(powerline) -> None: