diff --git a/CLAUDE.md b/CLAUDE.md index 0f35940..76a91ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -420,7 +420,11 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow - Status marks (single-width, themed): `✓` ok · `✗` err · `⚠`/`?` warn · `●` unread/ unacked (err) · `○` acked/empty (muted). Never `✔`, `✖`, `✅`, `❌` as status. - Node types (shared with the map): `★` you (white) · `●` companion · `▲` repeater · - `■` room server · `◉` sensor · `○` unknown. + `■` room server · `◉` sensor · `○` unknown. **Every type mark is white** (JP, + 2026-10-14; they were pink, violet, white, orange): the shape is the type. Each keeps its + own `type.*` style so one can regain a colour in one place. `○` keeps `node.unknown` + grey, which says "unidentified". On the map a crowd in one cell no longer glows — a white + marker has nowhere brighter to go, and JP chose to drop the signal. - **Our `★` is white, everywhere** (JP, 2026-10-14): map, routes, lists, legends, chat. `marks.SELF_MARK` carries the theme name `you` — the white of our name — so the console picks its own white, and a raster resolves it through `mark_rgb`. It was the map's @@ -553,7 +557,8 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow step darker, while an unidentified node is content you can still act on. Our own node is the pure-white `you` style; a context colouring (chart quality) may still win. - **White is what "you picked this" looks like**, and nothing else in the app is allowed - to say it. The cursor row — the row `❯` points at, in a select list or in a screen + to say it — beside the `you` white and the white node-type marks, whose shape carries + their meaning. The cursor row — the row `❯` points at, in a select list or in a screen drawing its own rows — wears `cursor` (white), never `brand`: the wordmark's teal is the app's identity, not a selection, and teal/cyan is itself a node hue, so a cyan-keyed node used to vanish into its own highlight. White is outside the node spectrum, so it @@ -764,8 +769,9 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT - The borderless title bar (`frame._title_bar`) is this platform's whole panel border: `↑↓ ──── Title ─────── Esc back`. The **clip arrows lead the row and are always both - drawn** — colour reads the scroll (the `border` grey where that direction has more, - `faint` where it doesn't), the same live/dim language the lane draws one row below. A + drawn** — colour reads the scroll (lit like the title, `title.border`, where that + direction has more, `faint` where it doesn't; the `border` grey sits too close to + `faint` to say it), the same live/dim language the lane draws one row below. A pair that appeared and vanished, half of it a blank cell, asked the reader to compare the row against a memory of itself; fixed furniture means the bar never changes width as the body scrolls either. @@ -804,7 +810,7 @@ branch on the platform per frame, and never `from meshterm.platforms import PLAT steps. Nothing loses its colour for being on the console. Marks whose hue is *fixed* rather than derived go through a theme name so the slot is chosen deliberately (the node types' `type.*`; a raster resolves the same entry via `theme.mark_rgb`) — a naive - downsample greys the repeater's violet. Footer hints are replaced by the **F-key lane** + downsample greys a dim violet. Footer hints are replaced by the **F-key lane** (`ui/tui/fkeys.py`): five `FPair` slots per screen, F1–F5 primary and F6–F10 each slot's *opposite number* (physical Shift+F1..F5), labels ≤6 cells. `DEFAULT_LANE` claims only **F4/F5 — the pager**, which has no physical key at all; **the jump to either end diff --git a/meshterm/tools/chat.py b/meshterm/tools/chat.py index b74aec8..6ca668a 100644 --- a/meshterm/tools/chat.py +++ b/meshterm/tools/chat.py @@ -945,7 +945,7 @@ def _title( - The name of a direct contact gets the palette hue that comes from its key (a channel label keeps the base style). - - The first dot is the standard companion pink, and its shape shows the history. + - The first dot has the colour of the companion mark, and its shape shows the history. - The unread ``●`` badge is red, and the age is muted. - The preview mutes its body, and lights the sender names and the ``@mentions`` in the hues that come from their keys. These are the same colours as in the live transcript. @@ -1002,9 +1002,9 @@ def _title( return text -#: The standard companion pink (the shared colour of the plain node ``●``) is the hue of the -#: contact dot. The shape (filled or hollow) shows the history, the colour shows "a -#: companion", and the name next to it has the hue that comes from the key of the person. +#: The colour of the companion mark (the shared colour of the plain node ``●``) is the +#: colour of the contact dot. The shape (filled or hollow) shows the history, and the name +#: next to it has the hue that comes from the key of the person. _COMPANION_DOT_STYLE = NODE_GLYPHS[NODE_TYPE_CHAT][1] #: The marker of a room: the ``■`` of the room server, in the colour of its type. The map @@ -1020,7 +1020,7 @@ def _append_marker( """Add the first marker of the row (a channel glyph or a contact dot) in its own lane. A channel keeps its marker for open or private (# / 🌐 / 🔒). A contact gets a small - circle in the standard companion pink. It is filled (``●``) after we exchanged + circle in the colour of the companion mark. It is filled (``●``) after we exchanged messages, and a hollow ring (``○``) before that. Thus the shape (hollow or filled) shows whether there is history, and the name itself has the hue that comes from the key of the person. diff --git a/meshterm/ui/map_render.py b/meshterm/ui/map_render.py index 389c2b8..586d97d 100644 --- a/meshterm/ui/map_render.py +++ b/meshterm/ui/map_render.py @@ -25,7 +25,6 @@ platform. from __future__ import annotations import math -from collections import Counter from dataclasses import dataclass, field from operator import itemgetter @@ -759,30 +758,6 @@ def _marker_style(marker: MapMarker) -> tuple[str, str]: return _NODE -#: The number of piled nodes at which the glyph of a marker gets full brightness. -_PILE_FULL = 8 - - -def _pile_color(color: RGB, count: int, *, cap: int = _PILE_FULL) -> RGB: - """Make the colour of a marker brighter, by the number of nodes that share its cell. - - One cell can show only one glyph. Thus, where many nodes are at the same position, the - map would hide the crowd behind one marker. Instead, we keep the glyph of the top node, - and move its colour toward white as the pile grows. Thus a bright marker shows a busy - cluster. The ramp is logarithmic (2 nodes → a clear increase, then saturation near - ``cap``). Thus it stays informative, and one extra node does not look like a crowd. - """ - if count <= 1: - return color - t = min(1.0, math.log2(count) / math.log2(cap)) - r, g, b = color - return ( - round(r + (255 - r) * t), - round(g + (255 - g) * t), - round(b + (255 - b) * t), - ) - - #: How much a node outside an active find filter dims (a multiplier for the glyph colour). _FIND_DIM = 0.35 @@ -801,9 +776,10 @@ def _draw_nodes( """Draw the mesh nodes on top: each glyph, then labels by importance until they collide. The glyphs are drawn lowest priority first, so that our node and the repeaters are on - top of the leaf nodes. Where several nodes share a cell, the glyph that stays is made - brighter by the pile size (:func:`_pile_color`). Thus crowded positions glow, and do - not silently hide the crowd. Then the labels are placed highest priority first (our + top of the leaf nodes. One cell shows one glyph, so where several nodes share a cell, + the glyph of the top node stays. (A crowd once made that glyph brighter. Each marker + is white now, and white cannot get brighter, so JP removed that signal on + 2026-10-14.) Then the labels are placed highest priority first (our node, then repeaters, then leaf nodes), each only if it fits with no overlap. Thus, on a crowded map, the important labels get the available space, and the other nodes show as a bare marker (with no overlap). @@ -823,9 +799,6 @@ def _draw_nodes( matched = not needle or needle in marker.label.casefold() placed.append((marker, ix, iy, matched)) - # A cell is (dot_x >> 1, dot_y >> 2). Count how many nodes are in each cell. - pile = Counter((ix >> 1, iy >> 2) for _, ix, iy, _m in placed) - # Matches draw after (over) the dimmed nodes that do not match, at any rank. Thus the # node that the user looks for is never hidden below a brighter neighbour. for marker, ix, iy, matched in sorted(placed, key=lambda p: (p[3], p[0]._rank())): @@ -833,8 +806,6 @@ def _draw_nodes( rgb = mark_rgb(color) if not matched: rgb = _dimmed(rgb, _FIND_DIM) - else: - rgb = _pile_color(rgb, pile[(ix >> 1, iy >> 2)]) canvas.marker(ix, iy, glyph, rgb) # A node that the filter removes is context: a bare dim glyph with no label. Labels get diff --git a/meshterm/ui/mapcanvas.py b/meshterm/ui/mapcanvas.py index 2fbc4ca..3f0b602 100644 --- a/meshterm/ui/mapcanvas.py +++ b/meshterm/ui/mapcanvas.py @@ -522,7 +522,7 @@ class MapCanvas: :data:`_BOLD`). The kernel VT draws ``bold`` as brightness. If a bold run has a colour that was just quantized into the dim bank, the VT changes it to the bright partner of that slot. A light-grey unknown label then shows as the white "you", and a - purple repeater shows as pink. The colour that a marker has is the important part. + brown highway shows as yellow. The colour that a marker has is the important part. The bold is not. """ from .theme import fold_text diff --git a/meshterm/ui/marks.py b/meshterm/ui/marks.py index 2c823ba..b284949 100644 --- a/meshterm/ui/marks.py +++ b/meshterm/ui/marks.py @@ -27,22 +27,22 @@ def parse_hex(color: str) -> RGB: return (int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)) -# The palette of markers, shared in the whole app. The basemap is blue (water), green -# (parks), and warm amber (roads). Thus the markers use the hues that a map never has, -# pink and violet, so that they are easy to see against it. Ordinary nodes are the -# loudest (bright pink). Repeaters are less loud (a calmer violet). Our node is the star, -# in the white of ``you`` (JP, 2026-10-14). It was yellow, and then the hue of our key. A -# node that was heard but never identified is a light grey ring. Each glyph is in the -# PicoCalc console font. +# The palette of markers, shared in the whole app. Each marker is white, and its shape +# tells what it is (JP, 2026-10-14): a ``●`` for an ordinary node and a ``▲`` for a +# repeater, in the ``type.*`` white of the theme. They were pink and violet, the hues that +# a map never has. Our node is the star, in the white of ``you`` (JP, 2026-10-14). It was +# yellow, and then the hue of our key. A node that was heard but never identified is a +# light grey ring. Each glyph is in the PicoCalc console font. # # The colour of a mark is any value that :func:`~meshterm.ui.theme.mark_rgb` takes: a # literal hex, or the name of a theme style when the platform must choose the shade. The # unknown ring has a name (``node.unknown``), so that the ring and the grey label beside # it can never be different. Our star has the name ``you``, the white of our node in each -# list, so that the console chooses its own white. +# list, and the two type marks have their ``type.*`` names, so that the console chooses +# its own white. SELF_MARK = ("★", "you") -REPEATER_MARK = ("▲", "#a78bfa") -NODE_MARK = ("●", "#f472b6") +REPEATER_MARK = ("▲", "type.repeater") +NODE_MARK = ("●", "type.node") UNKNOWN_MARK = ("○", "node.unknown") #: The only concealed character. One bullet replaces one character of a value that the diff --git a/meshterm/ui/theme.py b/meshterm/ui/theme.py index 126cc09..778cb3f 100644 --- a/meshterm/ui/theme.py +++ b/meshterm/ui/theme.py @@ -34,8 +34,9 @@ MESH_THEME = Theme( # ``muted``, and not a hue. A border is chrome, and a blue border was one more colour # on each screen that had no meaning (JP, 2026-10-14). The two caution tiers keep # their colours, because there the colour is the meaning: ``warn`` (amber) and - # ``err`` (red). - "border": "#a2a2a2", + # ``err`` (red). It is darker than ``muted`` (JP chose it on a ladder of greys), so the + # frame stays behind the text that it holds. + "border": "#7c7c7c", # The only reverse-video chip: the committing button of a dialog, the Abort of a # running action, and the insertion slot of the composer where no chip fill carries # it. It is a *grey* block, and the ink of the page shows through it. It was cyan @@ -170,16 +171,15 @@ MESH_THEME = Theme( "batt.low": "bold #f87171 on #991b1b", "batt.flash": "not bold #000000 on #dc2626", "batt.flash.off": "bold #f87171", - # The node-type *marks*: the only colours for ● ▲ ■ ◉ where a node with a type is - # drawn (refer to ui.widgets.NODE_GLYPHS). They are also the language of the markers - # of the map: clients are the loud pink, repeaters are the calmer violet, rooms are - # a white square, and sensors are an orange dot with a ring. They are named styles - # and not raw hex, so the 16-slot console chooses its slot *on purpose*. A naive - # downsample of the violet gives grey, and a repeater looks like an unknown node. - "type.node": "#f472b6", - "type.repeater": "#a78bfa", + # The node-type *marks*: the colours for ● ▲ ■ ◉ where a node with a type is drawn + # (refer to ui.widgets.NODE_GLYPHS), and the markers of the map. All four are white. + # The shape tells the type, so a hue for each type was a colour with no more + # information (JP, 2026-10-14). They were pink, violet, white, and orange. Each type + # keeps its own style name, so that one type can get a colour again in one place. + "type.node": "#ffffff", + "type.repeater": "#ffffff", "type.room": "#ffffff", - "type.sensor": "#fb923c", + "type.sensor": "#ffffff", # A region name (the scope of a flood). It is not a node, so it has no hue from the # node wheel. It is a light grey. Its slant makes it different from prose, as a # tag is different from a sentence. @@ -258,13 +258,13 @@ MESH_THEME = Theme( #: moves it to slot N+8. Thus a style for a dim slot must say what it means about #: bold. It cannot leave the question open, because Rich merges a *base* style into #: each span that it wraps. Then the ``bold`` of a selected row changes the colour of -#: the span completely (a light-grey unknown hash becomes the white "you", and a purple -#: repeater becomes pink). Thus each style for a dim slot is explicitly ``not bold`` +#: the span completely (a light-grey unknown hash becomes the white "you", and a brown +#: highway becomes yellow). Thus each style for a dim slot is explicitly ``not bold`` #: (the colour is necessary, so keep it) or explicitly ``bold`` (the promotion is the #: intent, and only ``title.muted`` and ``title.border`` have it). A style is never #: silent. Never use bold on 5 and 6, because *different* meanings use their partners -#: here (5 purple = the repeater mark, 13 pink = the node mark. 6 cyan = hint.brand, 14 -#: bright cyan = brand). +#: here (5 purple = the chip fill under a pink name, 13 pink = a node hue. 6 cyan = +#: hint.brand, 14 bright cyan = brand). #: * Backgrounds can use only slots 0–7 (SGR 40–47). #: * The six *chromatic bright* slots (9-14) are the place where the spectrum of node #: names lands. They are the console rendering of the hue wheel that comes from the key @@ -275,7 +275,7 @@ _VT_SLOTS: tuple[tuple[int, str, str], ...] = ( (2, "green (hint.ok)", "#00aa00"), (3, "brown/orange (hint.warn, batt.mid, heat.cool, the sensor mark)", "#aa5500"), (4, "blue (bluetooth bg, map.water)", "#0000aa"), - (5, "purple (the repeater mark)", "#aa00aa"), + (5, "purple (the chip fill of node hue 300°)", "#aa00aa"), (6, "cyan (hint.brand)", "#00aaaa"), (7, "light grey (default text, heat.cold)", "#aaaaaa"), (8, "dark grey (muted, faint, track)", "#555555"), @@ -283,9 +283,9 @@ _VT_SLOTS: tuple[tuple[int, str, str], ...] = ( (10, "bright green (ok; node hue 120°)", "#55ff55"), (11, "bright yellow (warn, heat.warm; node hue 60°)", "#ffff55"), (12, "bright blue (accent; node hue 240°)", "#5555ff"), - (13, "bright pink (the node mark — the map's pink, for free; node hue 300°)", "#ff55ff"), + (13, "bright pink (node hue 300°)", "#ff55ff"), (14, "bright cyan (brand; node hue 180°)", "#55ffff"), - (15, "white (you, heat.hot, the room mark)", "#ffffff"), + (15, "white (you, heat.hot, the node-type marks)", "#ffffff"), ) @@ -349,19 +349,15 @@ def vtrgb_lines() -> str: #: palette. The theme is literal and not derived, so a reviewer can see each slot choice #: next to its meaning. The rules for bold-brightness and for backgrounds, which the #: theme must obey, are in the documentation of :data:`_VT_SLOTS`, and tests pin them. -#: The stock palette gives two important results. Bright pink (13) is the colour of the -#: clients on the map, at no cost. Dim purple (5) replaces the violet of the repeaters -#: on the map. The cost is that the three greys (muted, faint, track) all become slot 8, +#: The stock palette has a cost: the three greys (muted, faint, track) all become slot 8, #: because the stock palette has only one dark grey. MESH_THEME_16 = Theme( { "brand": "bold color(14)", "accent": "bold color(12)", - # The border: the light grey, which is the nearest slot to the grey of the regular - # theme. It is not the dark grey of ``muted``, because the title bar draws a clip - # arrow with nothing in its direction in ``faint``, and that is slot 8. It is - # ``not bold`` because 7 is a dim slot (refer to the bold rule). - "border": "not bold color(7)", + # The border: the dark grey, which is the nearest slot to the grey of the regular + # theme. Slot 8 is already in the bright bank, so bold changes nothing. + "border": "color(8)", # Slot 7 is the only grey that the VT can put *behind* a reverse. Backgrounds stop at # the dim bank, and the dark grey of slot 8 becomes black there. It is the same # light grey that the F-key lane already uses to fill its chips. This is the @@ -421,10 +417,10 @@ MESH_THEME_16 = Theme( "batt.low": "bold color(9) on color(1)", "batt.flash": "not bold color(0) on color(1)", "batt.flash.off": "bold color(9)", - "type.node": "color(13)", - "type.repeater": "not bold color(5)", + "type.node": "color(15)", + "type.repeater": "color(15)", "type.room": "color(15)", - "type.sensor": "not bold color(3)", + "type.sensor": "color(15)", # A region name: the VT has no italic, and each chromatic slot is a node hue. Thus # the style is the plain light grey. The word "scope" in front of it makes it # different from other text. @@ -496,9 +492,10 @@ def mark_rgb(colour: str) -> tuple[int, int, int]: A canvas paints in RGB and not in styles (refer to :mod:`~meshterm.ui.mapcanvas`). A marker that must agree with its twin that Rich draws gets the *same colour* through this function. The function accepts both encodings that the marker language uses. One - is a literal ``#rrggbb`` (the fixed map marks in :mod:`~meshterm.ui.marks`). The other - is a theme style name (the node-type marks, which have names so that each platform - chooses its own colour, refer to ``type.*`` of the theme). + is a literal ``#rrggbb`` (for example a grey of the basemap in + :mod:`~meshterm.ui.map_render`). The other is a theme style name (the marks in + :mod:`~meshterm.ui.marks` and the node-type marks, which have names so that each + platform chooses its own colour, refer to ``type.*`` of the theme). On the 16-slot theme, a ``color(N)`` entry gives the :data:`_VT_SLOTS` RGB of that slot, and not the stock triple of Rich. The palette of Rich is a shade different from diff --git a/meshterm/ui/tui/frame.py b/meshterm/ui/tui/frame.py index 4657b69..5e37ef1 100644 --- a/meshterm/ui/tui/frame.py +++ b/meshterm/ui/tui/frame.py @@ -309,8 +309,10 @@ def _title_bar( memory of it, to learn something that the row can state directly. (When only one arrow showed, a blank cell stood in for the other.) Thus the pair is fixed at the left edge, where the eye starts, and **colour** gives the information. An arrow whose direction - has more content takes the grey of the border, so it looks like part of the frame. An - arrow with nothing in its direction becomes ``faint``, a step darker. This is the same + has more content is lit as the title is (``title.border``). An arrow with nothing in + its direction becomes ``faint``. The live arrow once had the grey of the border. That + grey is now almost the grey of ``faint`` (JP chose a darker border), and the two arrows + looked the same. This is the same live/dim language that the F-key lane draws one row below (a dim chip keeps its label and loses its fill). The two are the only indicators of the platform. @@ -371,8 +373,9 @@ def _title_bar( label_w = heading.cell_len + 2 bar = Text() - bar.append("↑", style=border if more_above else "faint") - bar.append("↓", style=border if more_below else "faint") + lit = title_style(border) + bar.append("↑", style=lit if more_above else "faint") + bar.append("↓", style=lit if more_below else "faint") bar.append(" ") if title: left = max(1, (rule_span - label_w) // 2) diff --git a/meshterm/ui/walk_screen.py b/meshterm/ui/walk_screen.py index 31635c8..7ebf79c 100644 --- a/meshterm/ui/walk_screen.py +++ b/meshterm/ui/walk_screen.py @@ -1387,9 +1387,8 @@ class WalkScreen(Screen): The walk once gave each marker the colour of its identity (the hue of the node, from its key), and only the glyph shape showed the type. Thus this canvas was different from each other place that shows a node with its type (the map, the - route graph, a contact row). There, the colour of the mark *is* the type, and the - ``type.*`` styles of the theme select it on purpose for each platform (a simple - downsample makes the violet of the repeater grey). The identity has its own lane + route graph, a contact row). There, the mark *is* the type, in the ``type.*`` styles + of the theme, which each platform resolves for itself. The identity has its own lane here, and always had it: the **name** next to the marker, in the hue of the key (:meth:`_label_rgb`). The shape and the colour now agree, and the legend under the canvas can explain both. @@ -1441,11 +1440,11 @@ class WalkScreen(Screen): These are the shared marks (:data:`~meshterm.ui.widgets.NODE_GLYPHS`) and the shared ``type.*`` colours for them. Thus a node shows here with exactly the glyph - and colour that the map, the route graph, and the contact list use for it: ``▲`` - violet for a repeater, ``■`` for a room, ``◉`` for a sensor, ``●`` pink for a - plain node, the white ``★`` for our node, and the grey ``○`` for a node that we - heard of but never identified. The colour and the shape say the same thing. Thus - the legend below the canvas is a real explanation, not a coincidence. + and colour that the map, the route graph, and the contact list use for it: a white + ``▲`` for a repeater, ``■`` for a room, ``◉`` for a sensor, ``●`` for a plain node, + ``★`` for our node, and the grey ``○`` for a node that we heard of but never + identified. Thus the legend below the canvas is a real explanation, not a + coincidence. Returns: ``(glyph, colour)``: the colour in the encoding that diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index 669e103..990346b 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -1112,14 +1112,12 @@ def stats_panel( # The glyphs and colours of the node types. They are the same as the marker palette of the -# map in the whole app (refer to ui.map_render). Our node is the white ``★``, plain nodes -# are the loud pink ``●``, and repeaters are the calmer violet ``▲``. The other types have -# hues that are safe on the map and different from those: a white square for rooms (the -# house glyph was difficult to read) and an orange dot with a ring for sensors. All are -# single-width BMP glyphs, so the columns stay aligned. The colours are the ``type.*`` -# entries of the theme and not raw hex. Thus the 16-slot console chooses its slot on -# purpose (otherwise the violet downsamples to grey, and a repeater looks like an unknown -# node). On the regular platform, they *are* the hues of the map. +# map in the whole app (refer to ui.map_render). Our node is the ``★``, plain nodes are a +# ``●``, repeaters are a ``▲``, rooms are a square (the house glyph was difficult to read), +# and sensors are a dot with a ring. All are white, and the shape tells the type (JP, +# 2026-10-14). All are single-width BMP glyphs, so the columns stay aligned. The colours +# are the ``type.*`` entries of the theme and not raw hex, so that the 16-slot console +# chooses its own white, and so that one type can get a colour again in one place. NODE_GLYPHS: dict[int, tuple[str, str]] = { NODE_TYPE_REPEATER: (REPEATER_MARK[0], "type.repeater"), NODE_TYPE_ROOM: ("■", "type.room"), diff --git a/tests/test_chat.py b/tests/test_chat.py index d914da5..91c0554 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -570,7 +570,8 @@ def test_title_contact_dot_reflects_conversation_history(repo: Repository) -> No """The contact dot of the title shows if there is history with the contact. The dot is filled (●) after the user and the contact talked, and hollow (○) before. It - is companion pink in both cases. The name has the hue of the key of the contact. + has the colour of the companion mark in both cases. The name has the hue of the key of + the contact. """ from meshterm.tools.chat import _COMPANION_DOT_STYLE from meshterm.ui.theme import node_style @@ -580,23 +581,23 @@ def test_title_contact_dot_reflects_conversation_history(repo: Repository) -> No label="Alice", is_channel=False, contact=Contact(name="Alice", public_key="d4" + "0" * 62) ) - def dot_is_pink(row) -> bool: + def dot_has_mark_colour(row) -> bool: return any( span.style == _COMPANION_DOT_STYLE and span.start == 0 and span.end == 1 for span in row.spans ) - # No history yet: a hollow ring in the standard companion pink. + # No history yet: a hollow ring in the colour of the companion mark. fresh = _title(ctx, contact, {}, _NO_KEYS, _lanes([contact])) - assert fresh.plain.startswith("○") and dot_is_pink(fresh) + assert fresh.plain.startswith("○") and dot_has_mark_colour(fresh) # The name lane has the hue of the key of Alice. name_at = fresh.plain.index("Alice") assert any(s.style == node_style("d4") and s.start <= name_at < s.end for s in fresh.spans) - # After the user and the contact exchange messages, the same pink dot is filled. + # After the user and the contact exchange messages, the same dot is filled. last = ChatMessage(text="hi", peer=contact.peer) talked = _title(ctx, contact, {contact.key: last}, _NO_KEYS, _lanes([contact])) - assert talked.plain.startswith("●") and dot_is_pink(talked) + assert talked.plain.startswith("●") and dot_has_mark_colour(talked) # -- chat service ------------------------------------------------------------- diff --git a/tests/test_map.py b/tests/test_map.py index 7c80d24..e3ccd14 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -715,22 +715,23 @@ def _glyph_color(lines: list[str], glyph: str) -> tuple[int, int, int]: return int(m.group(1)), int(m.group(2)), int(m.group(3)) -def test_render_map_brightens_piled_markers() -> None: - """A cell that many nodes share renders its glyph brighter than the glyph of one node.""" +def test_render_map_draws_a_crowd_as_one_white_marker() -> None: + """A marker is white, and a crowd in one cell draws the same as one node. + + A crowd once made its glyph brighter. Each marker is white now, so JP removed that + signal (2026-10-14). + """ from meshterm.ui.map_render import MapMarker, render_map from meshterm.ui.marks import NODE_MARK + from meshterm.ui.theme import mark_rgb - base = parse_hex(NODE_MARK[1]) vp = Viewport(45.50, -73.57, 14, 60, 40) lone = render_map(vp, {}, [MapMarker("solo", 45.50, -73.57)]) - assert _glyph_color(lone, "●") == base # one node keeps its base colour + assert _glyph_color(lone, "●") == mark_rgb(NODE_MARK[1]) == (255, 255, 255) crowd = [MapMarker(f"n{i}", 45.50, -73.57) for i in range(8)] - piled = _glyph_color(render_map(vp, {}, crowd), "●") - # The colour goes toward white: each channel is brighter than the base cyan. - assert all(p > b for p, b in zip(piled, base, strict=True)) - assert piled != base + assert _glyph_color(render_map(vp, {}, crowd), "●") == (255, 255, 255) def test_render_map_works_without_basemap() -> None: diff --git a/tests/test_node_detail.py b/tests/test_node_detail.py index e2d68da..ebf92d6 100644 --- a/tests/test_node_detail.py +++ b/tests/test_node_detail.py @@ -621,7 +621,7 @@ def test_located_accepts_real_fixes_and_rejects_junk() -> None: def _header() -> Text: - header = Text("▲ ", style="#a78bfa") + header = Text("▲ ", style="type.repeater") header.append("Hub", style="accent") header.append(" repeater", style="muted") return header diff --git a/tests/test_picocalc_lyra_chrome.py b/tests/test_picocalc_lyra_chrome.py index 781c77c..fb6d432 100644 --- a/tests/test_picocalc_lyra_chrome.py +++ b/tests/test_picocalc_lyra_chrome.py @@ -103,21 +103,21 @@ def test_the_clip_arrows_are_always_drawn_and_say_it_in_colour() -> None: The arrows once appeared and disappeared. A pair that was half shown left a blank cell for the missing arrow, and the user had to compare the row with a memory of the row (JP, 2026-08-31). Now the pair never moves and never changes width. An arrow for a - direction with more content has the grey of the border. An arrow for a direction with - no more content is ``faint``, a step darker. This is the live and dim language of the - F-key lane, one row up. + direction with more content is lit as the title is. An arrow for a direction with no + more content is ``faint``. This is the live and dim language of the F-key lane, one row + up. """ hint = "↑↓ move · Esc back" assert _arrow_styles(_bar_text("Contacts", hint, above=True, below=True)) == ( - "border", - "border", + "title.border", + "title.border", ) assert _arrow_styles(_bar_text("Contacts", hint, above=False, below=True)) == ( "faint", - "border", + "title.border", ) assert _arrow_styles(_bar_text("Contacts", hint, above=True, below=False)) == ( - "border", + "title.border", "faint", ) # A body that fits keeps the pair, and both are dim. Nothing appears or disappears. diff --git a/tests/test_theme16.py b/tests/test_theme16.py index 4198e1f..ba23428 100644 --- a/tests/test_theme16.py +++ b/tests/test_theme16.py @@ -59,8 +59,8 @@ def test_theme16_bold_never_jumps_a_dim_slot_to_an_unrelated_bright_one() -> Non """The VT draws bold as brightness: bold on slots 0-7 becomes slot N+8. Slots 5 and 6 have meanings, and their bright partners have different meanings here - (5 purple is the repeater mark, and 13 pink is the node mark. 6 cyan is hint.brand, - and 14 is brand). Thus no style can combine bold with them. + (5 purple is the chip fill under a pink name, and 13 pink is a node hue. 6 cyan is + hint.brand, and 14 is brand). Thus no style can combine bold with them. """ for name, style in _our_styles(MESH_THEME_16).items(): if style.bold and style.color is not None and style.color.number in (5, 6): @@ -348,13 +348,16 @@ def test_route_graph_resolves_a_named_marker_colour_to_rgb() -> None: assert label_rgb_of("3d63") == theme.mark_rgb("type.repeater"), platform.name -def test_node_type_marks_stay_distinct_on_the_console() -> None: - """Each type mark has its own slot. A naive downsample makes the repeater violet grey.""" - set_platform(PICOCALC_LYRA) +def test_node_type_marks_are_white_on_both_platforms() -> None: + """Each type mark is white, and the shape tells the type (JP, 2026-10-14). + + The white is not the grey of a node that MeshTerm cannot identify. + """ marks = ("type.node", "type.repeater", "type.room", "type.sensor") - rgbs = [theme.mark_rgb(name) for name in marks] - assert len(set(rgbs)) == len(marks) - assert theme.mark_rgb("type.repeater") != theme.mark_rgb("muted") + for platform in (REGULAR, PICOCALC_LYRA): + set_platform(platform) + assert {theme.mark_rgb(name) for name in marks} == {(255, 255, 255)}, platform.name + assert theme.mark_rgb("type.repeater") != theme.mark_rgb("node.unknown") def test_heat_ladder_walks_jps_seven_slots_in_order() -> None: diff --git a/tests/test_tui.py b/tests/test_tui.py index feea32f..044cf3c 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -1309,7 +1309,7 @@ def _cell_color(line: str, idx: int) -> tuple[int, int, int]: return (triplet.red, triplet.green, triplet.blue) -_BORDER = (162, 162, 162) # The border grey of the theme, #a2a2a2. +_BORDER = (124, 124, 124) # The border grey of the theme, #7c7c7c. def _find(plain: str, glyphs: frozenset[str]) -> int: