From 796ff94dad692bf7c25a22d3bb0ea66bd17b1936 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Wed, 14 Oct 2026 00:18:59 -0400 Subject: [PATCH] Our star is white The star of our node had the hue of our key, set from the self-info at run time. It is now the white of "you" on each surface: the map, the routes, the lists, the legends, and the chat. The run-time colour, the device-state hook, and the style generation of the render cache are removed. A faded star keeps its grey. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 13 +++--- CLAUDE.md | 30 ++++++------ meshterm/context.py | 13 ------ meshterm/services/device_state.py | 9 ---- meshterm/tools/chat.py | 4 +- meshterm/ui/chat.py | 15 +++--- meshterm/ui/contactlist.py | 4 +- meshterm/ui/map_render.py | 2 +- meshterm/ui/marks.py | 11 +++-- meshterm/ui/pathline.py | 40 ++++++---------- meshterm/ui/records_screen.py | 4 +- meshterm/ui/theme.py | 76 ------------------------------- meshterm/ui/tui/render.py | 5 +- meshterm/ui/walk_screen.py | 4 +- meshterm/ui/widgets.py | 20 ++++---- tests/conftest.py | 15 ------ tests/test_chat.py | 2 +- tests/test_device_state.py | 26 ----------- tests/test_pathline.py | 19 ++++---- tests/test_records_screen.py | 4 +- tests/test_widgets.py | 31 ++----------- 21 files changed, 84 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33092db..10d3ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,12 @@ can change how things behave. It does not only add new things. your messages under `★` and your node's name, and the Messages list shows them in the same way. Your name is white, on the same colour of box that your key gives each chip, where each other sender has the colour of its key as the text. A mention of your name - has the same white text, and the message that has it gets a `★` in your colour at the - left of its time. Thus the messages that name you are easy to find down the left side - of the chat. Before, your messages showed only the `★`. -- **Your node's star has your colour.** The `★` that marks your node on the map, in - routes, in lists, and in a chat has the colour of your key, the same colour as your name - in a chat. Before, it was yellow. It is yellow only until the device gives MeshTerm your - key. A star in a faded part of a route stays grey. + has the same white text, and the message that has it gets a white `★` at the left of + its time. Thus the messages that name you are easy to find down the left side of the + chat. Before, your messages showed only the `★`. +- **Your node's star is white.** The `★` that marks your node on the map, in routes, in + lists, and in a chat is white, the same white as your name in the lists. Before, it was + yellow. A star in a faded part of a route stays grey. - **In a chat, a name that MeshTerm cannot match to a node is black on grey.** The box around a sender or a mention with no known node has black text on a grey box. Thus it does not stand out among the coloured names. Before, it was light blue on slate. diff --git a/CLAUDE.md b/CLAUDE.md index e6ec418..e04c792 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -417,16 +417,13 @@ 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 (our key's hue) · `●` companion · `▲` - repeater · `■` room server · `◉` sensor · `○` unknown. -- **Our `★` is our key's hue, everywhere** (JP, 2026-10-13): map, routes, lists, - legends, chat. `marks.SELF_MARK` carries the theme name `you.mark`, never a hex, and - `theme.set_self_key` writes `node_style(our key)` into it when the device reports the - key (`DeviceState.on_self_info` → `context._publish_self_key`; the services never - import the UI). One call recolours every surface; the render cache keys on - `theme.style_generation()` so no stale bytes survive. Until the key is known (and with - no device, as in `specimen`) it is the map's old yellow. A star something *mutes* keeps - its grey — a faded hop, a dimmed return leg — and our name stays the `you` white. +- Node types (shared with the map): `★` you (white) · `●` companion · `▲` repeater · + `■` room server · `◉` sensor · `○` unknown. +- **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 + yellow, then briefly our key's hue (set from the self-info at run time), and both were + dropped. A star something *mutes* keeps its grey — a faded hop, a dimmed return leg. - Path cuts: a path line drawn as **chips** that gets cut — a lane that ran out, a row scrolled past its edge — breaks the chip off on a half block in that chip's own fill (`▐` ends a line, `▌` opens one; `pathline.cut_mark`/`cut_to`), so half the cell is @@ -437,8 +434,8 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow origin and destination, never the first and last *relay*. Our own end is always the app-wide `★` (`pathline.SELF_GLYPH`, taken straight from `marks.SELF_MARK`), never our 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 star, in our hue, on neutral - dark grey, padded like every other chip. + that differ from row to row. As a chip it is the map's white 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 @@ -496,11 +493,10 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow pair, white on the hue's dim twin. Our **sender** chip is `★ Name` (`widgets.self_chip`), the star in the white label; the star on the page right of the chip was tried and put back (JP, 2026-10-13). A **mention** of us is the same chip - with no star - (`mention_chip(you=True)`); the message carrying it gets **a `★` in our key's hue** - left of its time instead (`widgets.mention_mark`, the same on every platform and in - arrow mode). The map's yellow star, the chip's notch-and-point pair and its point alone - were all tried there and dropped (JP, 2026-10-13). Every time sits two cells right to + with no star (`mention_chip(you=True)`); the message carrying it gets **our white + `★`** left of its time instead (`widgets.mention_mark`, the same on every platform and + in arrow mode). The map's yellow star, the chip's notch-and-point pair, its point alone + and a star in our hue were all tried there and dropped (JP, 2026-10-13/14). Every time sits two cells right to keep the column (`❯`, a space, the star, a space, then `12:34 body`), and the Messages list puts the star left of the row's age. Arrow mode draws the chip as a padded block in the same pair. With no name or key from the device, the bare `★` chip stays. Path diff --git a/meshterm/context.py b/meshterm/context.py index c5e6b6d..c775206 100644 --- a/meshterm/context.py +++ b/meshterm/context.py @@ -51,18 +51,6 @@ if TYPE_CHECKING: from .ui.surface import Ui -def _publish_self_key(info: dict) -> None: - """Give the theme our key from a new self-info, so that our star takes our hue. - - The device-state cache calls this after each read (refer to - :attr:`~meshterm.services.device_state.DeviceState.on_self_info`). The context makes - the connection, because the services do not import the UI. - """ - from .ui.theme import set_self_key - - set_self_key(str(info.get("public_key") or "") or None) - - @dataclass(slots=True) class AppContext: """The shared services and the state of one run, which MeshTerm gives to tools. @@ -517,7 +505,6 @@ class AppContext: from .services.device_state import DeviceState self._devstate = DeviceState(self) - self._devstate.on_self_info = _publish_self_key return self._devstate @property diff --git a/meshterm/services/device_state.py b/meshterm/services/device_state.py index 40545f9..03b96bf 100644 --- a/meshterm/services/device_state.py +++ b/meshterm/services/device_state.py @@ -55,8 +55,6 @@ from typing import TYPE_CHECKING from ..core.channel_probe import ChannelSlot, probe_channel_slots if TYPE_CHECKING: - from collections.abc import Callable - from ..context import AppContext from ..core.models import Contact @@ -132,11 +130,6 @@ class DeviceState: self._channels_lock = asyncio.Lock() self._capacity_lock = asyncio.Lock() self._tasks: set[asyncio.Task] = set() - #: Called with each self-info that the cache reads from the device. The context - #: connects it to the theme, so that our star takes the hue of our key (refer to - #: :meth:`AppContext.devstate `). ``None`` - #: calls nothing. - self.on_self_info: Callable[[dict], None] | None = None # -- contacts (stale-while-revalidate) -------------------------------------- @@ -324,8 +317,6 @@ class DeviceState: device = await self._ctx.device() self._self_info = dict(await device.get_self_info()) self._self_info_at = time.monotonic() - if self.on_self_info is not None: - self.on_self_info(self._self_info) return self._self_info async def _refresh_self_info_quietly(self) -> None: diff --git a/meshterm/tools/chat.py b/meshterm/tools/chat.py index 98d705a..b74aec8 100644 --- a/meshterm/tools/chat.py +++ b/meshterm/tools/chat.py @@ -985,7 +985,7 @@ def _title( text.append(" " * _BADGE_WIDTH) # The relative age lane (right-aligned) is between the badge and the message text. Thus # the ages are in one neat column, and all the previews start at the same position. A - # last message that names us has a ``★`` in our hue at the left of its age, the same as + # last message that names us has our white ``★`` at the left of its age, the same as # the transcript has it at the left of the time (refer to ChatScreen._body_lines). age = _ago(last.created_at) if last is not None else "" lane = f" {age:>{_AGE_WIDTH}}" @@ -1056,7 +1056,7 @@ def _preview_text( - Our own messages start with our chip, ``★ Name`` in white (refer to :func:`~meshterm.ui.widgets.self_chip`). A mention of our name has the chip that is - white label, with no star. A message that names us has a ``★`` in our hue at the + white label, with no star. A message that names us has our white ``★`` at the left of the age of the row (refer to :func:`_title`). - In a received channel message, the inline ``Name:`` sender is the author, in the hue that comes from its key (the node grey when no known node has the name). diff --git a/meshterm/ui/chat.py b/meshterm/ui/chat.py index 264e33b..fb337b8 100644 --- a/meshterm/ui/chat.py +++ b/meshterm/ui/chat.py @@ -775,14 +775,13 @@ class ChatScreen(Screen): is marked as the reply target (the same as the ``❯`` pointer and the highlight of the select screen). - A message that mentions our name has a mark at the left of its time: the ``★``, - in the hue of our key (refer to :func:`~meshterm.ui.widgets.mention_mark`, JP, + A message that mentions our name has a mark at the left of its time: the white + ``★`` of our node (refer to :func:`~meshterm.ui.widgets.mention_mark`, JP, 2026-10-13). The chip of the mention in the body has our white label, but no star - (refer to - :func:`~meshterm.ui.widgets.mention_hop`). Thus the mark of a message that names us - is in one column, where the eye finds it when it goes down the transcript. Each time - has the two cells of that column in front of it, so that all the times stay in one - column too. + (refer to :func:`~meshterm.ui.widgets.mention_hop`). Thus the mark of a message that + names us is in one column, where the eye finds it when it goes down the transcript. + Each time has the two cells of that column in front of it, so that all the times stay + in one column too. A URL is never cut where the screen can hold it. A URL that is too long for the body lane moves out to the column after the ``❯``, on its own line (refer to @@ -856,7 +855,7 @@ class ChatScreen(Screen): Thus the user sees that a mention belongs to the node that it names. The text around the mentions keeps the base style of the line (``cursor`` when the message is the selected reply target, or else no style). A mention of our own name has our - chip, in white, and the line has a ``★`` in our hue at the left of its time + chip, in white, and the line has our white ``★`` at the left of its time (refer to :meth:`_body_lines`). Thus a message that names us is easy to see. The chip opens on the notch, because the node that a mention names is the diff --git a/meshterm/ui/contactlist.py b/meshterm/ui/contactlist.py index b7801b8..95e41b4 100644 --- a/meshterm/ui/contactlist.py +++ b/meshterm/ui/contactlist.py @@ -244,7 +244,7 @@ class ContactRow: a muted ``?``. node_type: The node type of the contact, for the leading glyph (``None`` = the plain node ``●``). The row of our node ignores it, and always starts with the - ``★`` in our hue. + white ``★``. last_seen: When MeshTerm last heard the contact (aware UTC). It fills the heard lane, in the colour of the recency heat. ``None`` shows ``never`` in the cold style. @@ -361,7 +361,7 @@ def _you_lane( """The lane of our node, with the same layout as the usual rows of :func:`_lane`. It is drawn as the map and the static table draw our node: the ``★`` self marker - (in our hue), the name in the pure-white ``you`` style with a muted ``(you)`` tag, and our + (white), the name in the pure-white ``you`` style with a muted ``(you)`` tag, and our key with its hash lit at the routing width, in the flexible key lane. Our 64-hex key is longer than any lane, so it shows as many digits as fit, and then an ellipsis. **Each middle lane shows a faint ``—``**, whatever lanes the list declares. MeshTerm never diff --git a/meshterm/ui/map_render.py b/meshterm/ui/map_render.py index e5e26d8..041fd19 100644 --- a/meshterm/ui/map_render.py +++ b/meshterm/ui/map_render.py @@ -839,7 +839,7 @@ def _draw_nodes( # A node that the filter removes is context: a bare dim glyph with no label. Labels get # the name hue of the node, which comes from its key (the colour rule for all the app). - # The label of our node is the pure ``you`` white (the ★ glyph keeps our hue). An + # The label of our node is the pure ``you`` white, the same as the ★ glyph. An # active find filter still makes the label of each match full white, so that the node # that the user looks for is easy to see. from .widgets import name_rgb # widgets imports this module, so bind late to avoid the cycle diff --git a/meshterm/ui/marks.py b/meshterm/ui/marks.py index 74ec0e6..2c823ba 100644 --- a/meshterm/ui/marks.py +++ b/meshterm/ui/marks.py @@ -31,15 +31,16 @@ def parse_hex(color: str) -> RGB: # (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 hue of our key (yellow until the device reports the key). A node that was heard -# but never identified is a light grey ring. Each glyph is in the PicoCalc console font. +# 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 a name (``you.mark``), because its colour comes -# from our key at run time (refer to :func:`~meshterm.ui.theme.set_self_key`). -SELF_MARK = ("★", "you.mark") +# 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. +SELF_MARK = ("★", "you") REPEATER_MARK = ("▲", "#a78bfa") NODE_MARK = ("●", "#f472b6") UNKNOWN_MARK = ("○", "node.unknown") diff --git a/meshterm/ui/pathline.py b/meshterm/ui/pathline.py index 0c28e68..62da824 100644 --- a/meshterm/ui/pathline.py +++ b/meshterm/ui/pathline.py @@ -17,8 +17,7 @@ surface that the survey found can in the end use it. look, and each seam uses one cell. The label of the chip of a node has the hue that comes from its hash (:func:`~meshterm.ui.theme.node_style`). This is the colour that its name has on each surface. The fill is that hue, but darker (:data:`CHIP_LIGHTNESS`). Our - node is the ``★`` of the map, in the hue of our key, on a neutral dark grey. A faded hop - is dark slate. A + node is the white ``★`` of the map on a neutral dark grey. A faded hop is dark slate. 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`.) @@ -88,7 +87,7 @@ surface that the survey found can in the end use it. ``★`` of the app. The user already knows who both ends are, and the cells are for the hops that *are* news. These stars fade where the route is being *composed*, because our two ends are fixtures and not choices. A surface that only *shows* a walk passes - ``dim_self=False`` and keeps our stars in our hue. + ``dim_self=False`` and keeps our stars in the ``you`` white. The existing call sites still render through ``path_text``. A separate pass for each surface will move them here. The insertion cursor of the composer is a hop in its own @@ -141,15 +140,8 @@ POWERLINE_ROUND_CLOSE = "" #: Our node, where a surface asks for it without its name and hash (``bare_self``): the #: ``★`` of the app. It is the map's own marker, taken from #: :data:`~meshterm.ui.marks.SELF_MARK`. Thus the star in a route and the star on the map -#: can never be different, in glyph or in hue. The hue is a theme name, the hue of our key -#: (refer to :func:`~meshterm.ui.theme.set_self_key`). A chip resolves it at each paint -#: (:func:`_self_ink`). -SELF_GLYPH, _SELF_STYLE = SELF_MARK - - -def _self_ink() -> str: - """The colour of our ``★`` as ``#rrggbb``, for the ink of a chip (the hue of our key).""" - return _style_hex(_SELF_STYLE) or "#facc15" +#: can never be different, in glyph or in colour: the white of ``you``. +SELF_GLYPH = SELF_MARK[0] #: The insertion cursor of the composer. It stands in the route as a hop of its own: the @@ -305,9 +297,9 @@ class PathHop: comes from the hash (its first byte, so each prefix gives the same hue). ``None`` renders muted and grey, because colour is only for identities that have a key. - you: Our node. Arrows draw our name in the pure-white ``you`` style, and our ``★`` - in the hue of our key. Chips draw the star in that hue on the neutral dark grey - :data:`_YOU_BG`, with padding like each other chip. + you: Our node. Arrows draw it in the pure-white ``you`` style. Chips draw the white + ``★`` on the neutral dark grey :data:`_YOU_BG`, with padding like each other + chip. annotation: The hash note for traces. It is rendered ``" (3d)"`` after the label in both modes (pass the bare ``3d``, without parentheses). lit_bytes: For a hash label: the leading *bytes* that are drawn in the hue (the two @@ -1039,9 +1031,7 @@ class PathLine: elif hop.style: text.append(hop.label, style=hop.style) elif hop.you: - # Our star has the hue of our key, as on the map. Our name stays the white of - # ``you``. - text.append(hop.label, style=_SELF_STYLE if hop.label == SELF_GLYPH else "you") + text.append(hop.label, style="you") elif hop.key and hop.lit_bytes > 0: split = hop.lit_bytes * 2 # the two tones of highlighted_hash, in hex digits text.append(hop.label[:split], style=node_style(hop.key)) @@ -1198,9 +1188,9 @@ class PathLine: Each chip has padding on both sides, also the ``★`` (JP, 2026-08-09). The pads are not there to make room for a long *word*. They are the chip's own shape. A star that is squeezed between two chevrons reads as a glyph that is wedged into the seam, - and not as a segment of the route. The ink of our chip is the colour of the star of - the map, the hue of our key, on the neutral dark grey :data:`_YOU_BG`. Thus the star - in the line and the star on the map read as one mark. + and not as a segment of the route. The ink of our chip is the white of the star of + the map, on the neutral dark grey :data:`_YOU_BG`. Thus 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`). """ @@ -1253,8 +1243,7 @@ def _spectrum_colours(hop: PathHop) -> _ChipColours: if hop.white_ink and hue: return _ChipColours(_chip_fill(hue), _WHITE, _WHITE) if hop.you: - ink = _self_ink() - return _ChipColours(_YOU_BG, ink, ink) + return _ChipColours(_YOU_BG, _WHITE, _WHITE) if hue: return _ChipColours(_chip_fill(hue), hue, hue) if hop.quiet: @@ -1293,8 +1282,7 @@ def _slot_colours(hop: PathHop) -> _ChipColours: if hop.dim: return _ChipColours(_SLOT_PAGE, _SLOT_FAINT, _SLOT_FAINT) if hop.you: - ink = _self_ink() - return _ChipColours(_SLOT_PAGE, ink, ink) + return _ChipColours(_SLOT_PAGE, _SLOT_WHITE, _SLOT_WHITE) hue = _style_hex(node_style(hop.key)) if hop.key else None ink = hue if hue in DIM_TWIN else _SLOT_WHITE if hop.white_ink and hue: @@ -1361,7 +1349,7 @@ def path_line( rules). Thus a call site changes builders and does not change what it says. A named hop reads ``Name`` (with ``show_hash`` it is ``Name (3d)``). An unnamed hop is its hash in the grey for an unknown node (colour marks an identified node, as in the path graph). - ``None`` is our device in white, or the lone ``★`` in our hue with ``bare_self``. ``dim_from`` + ``None`` is our device in white, or the lone ``★`` with ``bare_self``. ``dim_from`` fades a resolved tail. The plain rendering is identical to ``path_text`` in characters and in style. What the change gives is the shapes of :class:`PathLine` (ellipsized and wrapped) and the powerline mode. diff --git a/meshterm/ui/records_screen.py b/meshterm/ui/records_screen.py index cad8963..678e9f4 100644 --- a/meshterm/ui/records_screen.py +++ b/meshterm/ui/records_screen.py @@ -773,7 +773,7 @@ class RecordScreen(Screen): # a chip), instead of a hanging indent under a lane. Our two ends are the app-wide ★, # not our name and key again. The graph above already marks our node with the same # star at the two ends, and the legend under it says that the star is "you". The - # stars keep our hue, because nothing about a walk that is already done is + # stars keep the ``you`` white, because nothing about a walk that is already done is # ours to compose. route = path_line( [None, *record.route, None], @@ -1043,7 +1043,7 @@ async def open_records(ctx: AppContext) -> dict: (:data:`~meshterm.ui.pathline.SELF_GLYPH`). A record is a boomerang by construction. To name our node two times in each row uses more cells than the full score lane, and tells the user what each other row already told. These stars keep - our hue (``dim_self=False``). The dim colour means "not yours to + the ``you`` white (``dim_self=False``). The dim colour means "not yours to compose", and nothing is composed on a board of walks that are already done. The line is composed at its **natural** length and is given whole. Thus each row is diff --git a/meshterm/ui/theme.py b/meshterm/ui/theme.py index 5c9659e..d3a25d7 100644 --- a/meshterm/ui/theme.py +++ b/meshterm/ui/theme.py @@ -20,7 +20,6 @@ from functools import lru_cache from rich.cells import cell_len from rich.console import Console -from rich.style import Style from rich.theme import Theme from ..platforms import Platform, on_platform @@ -80,11 +79,6 @@ MESH_THEME = Theme( # of the node hues on purpose (refer to node_style). Thus the user can always find # "you" easily. "you": "bold #ffffff", - # The ``★`` of our node (``marks.SELF_MARK``): on the map, in a route, in a list. It - # has the hue of our key, the colour of our node everywhere else (JP, 2026-10-13). - # :func:`set_self_key` puts the hue here when the device reports our key. This - # yellow is the star of the map until then, and with no device. - "you.mark": "#facc15", # The text that the user types in the compose line of a chat: pure white, not bold. # It is more visible than the transcript above it (JP, 2026-10-13). The ``›`` in # front of it has the ``cursor`` white, the same as the ``❯`` of a selected @@ -371,7 +365,6 @@ MESH_THEME_16 = Theme( "pathline": "none", "err.reverse": "reverse bold color(9)", "you": "bold color(15)", - "you.mark": "color(11)", # Slot 15 is already bright, so it needs no bold to be white. "compose": "color(15)", "device.known": "bold color(15)", @@ -1120,73 +1113,6 @@ _fold_impl: Callable[[str], str] = _no_fold _STYLE_RGB: dict[str, tuple[int, int, int]] = {} -#: The theme name of the colour of our ``★`` (refer to :func:`set_self_key`). -SELF_MARK_STYLE = "you.mark" - -#: The colour of our ``★`` in each theme before our key is known: the yellow of the map. -_SELF_MARK_DEFAULTS: dict[int, Style] = { - id(theme): theme.styles[SELF_MARK_STYLE] for theme in (MESH_THEME, MESH_THEME_16) -} - -#: Our public key, which gives the hue of our ``★``. ``None`` before the device reports it. -_SELF_KEY: str | None = None - -#: A count that goes up each time that the colour of a theme name changes after the -#: platform was bound (refer to :func:`style_generation`). -_STYLE_GEN = 0 - - -def set_self_key(key: str | None) -> None: - """Give our ``★`` the hue of our key, on each surface that draws it. - - The star of our node is one theme name (:data:`SELF_MARK_STYLE`), which the map, the - routes, the lists, and the chat all draw. Thus this one call changes it everywhere, - and no surface must know our key. The hue is the one that :func:`node_style` gives our - key, on this platform: the colour of our node in the chips and in the names of the - chat (JP, 2026-10-13). A star that a surface fades (a dimmed hop, the grey of a muted - line) keeps its own grey, because it does not use this name. - - The app calls this when the device reports our key (refer to - :class:`~meshterm.services.device_state.DeviceState`). A call with the same key does - nothing. ``None`` puts the yellow of the map back. - - Args: - key: Our public key, or ``None``. - """ - global _SELF_KEY - key = key or None - if key == _SELF_KEY: - return - _SELF_KEY = key - _apply_self_mark() - - -def _apply_self_mark() -> None: - """Write the colour of our ``★`` into the active theme, and make the caches forget it. - - The consoles of the render share the dict of styles of the theme. Thus the next render - reads the new colour. The ANSI cache of the render holds the old bytes, so the - generation changes (refer to :func:`style_generation`). - """ - global _STYLE_GEN - theme = _ACTIVE_THEME - if _SELF_KEY: - theme.styles[SELF_MARK_STYLE] = Style.parse(node_style(_SELF_KEY)) - else: - theme.styles[SELF_MARK_STYLE] = _SELF_MARK_DEFAULTS[id(theme)] - _STYLE_RGB.pop(SELF_MARK_STYLE, None) - _STYLE_GEN += 1 - - -def style_generation() -> int: - """The count of the changes to a theme colour at run time (the colour of our ``★``). - - A cache of rendered bytes keeps it in its key, so that bytes from before a change are - never used again (refer to :func:`~meshterm.ui.tui.render.render_to_ansi`). - """ - return _STYLE_GEN - - @on_platform def _bind(platform: Platform) -> None: """Bind the choices of the theme that depend on the platform (runs now and at each switch).""" @@ -1208,5 +1134,3 @@ def _bind(platform: Platform) -> None: # cache, and does not trust old pads. _FOLD_TABLE = None _fold_to_font.cache_clear() - # The hue of our star is the hue of the platform: a quantized slot on the console. - _apply_self_mark() diff --git a/meshterm/ui/tui/render.py b/meshterm/ui/tui/render.py index 47c0af2..f67baea 100644 --- a/meshterm/ui/tui/render.py +++ b/meshterm/ui/tui/render.py @@ -23,7 +23,7 @@ from rich.text import Span, Text from ...platforms import Platform, on_platform from ..pathline import PATH_INK -from ..theme import active_theme, fold_text, is_identity_style, style_generation +from ..theme import active_theme, fold_text, is_identity_style #: A cache of one headless render console for each width. Consoles are cheap, but a paint #: occurs at each key press and at each tick of the live-monitor timer. Thus the cache @@ -84,7 +84,7 @@ def _cache_key(renderable: RenderableType, width: int, no_wrap: bool) -> tuple | necessary. Thus it reports ``None`` and renders without the cache. """ if isinstance(renderable, str): - return ("s", renderable, width, no_wrap, _CACHE_GEN, style_generation()) + return ("s", renderable, width, no_wrap, _CACHE_GEN) if isinstance(renderable, Text): return ( "t", @@ -98,7 +98,6 @@ def _cache_key(renderable: RenderableType, width: int, no_wrap: bool) -> tuple | width, no_wrap, _CACHE_GEN, - style_generation(), ) return None diff --git a/meshterm/ui/walk_screen.py b/meshterm/ui/walk_screen.py index f82bfb7..b7f5e05 100644 --- a/meshterm/ui/walk_screen.py +++ b/meshterm/ui/walk_screen.py @@ -1416,7 +1416,7 @@ class WalkScreen(Screen): """The Rich style of the type glyph of a node in body text: the colour of its type. This is the text form of :meth:`_marker_rgb`: the same ``type.*`` entry (or the - hue of our star). Thus the first glyph of the focus line, the glyphs of the list + white of our star). Thus the first glyph of the focus line, the glyphs of the list rows, and the canvas markers are one mark, drawn in three ways. """ return self._glyph(node)[1] @@ -1443,7 +1443,7 @@ class WalkScreen(Screen): 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 ``★`` in our hue for our node, and the grey ``○`` for a node that we + 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. diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index 2b7fb17..bc7c55d 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -716,8 +716,8 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) -> PicoCalc draws the same pair, white on the dim twin of our hue. A mention of our name is a different mark (refer to :func:`mention_chip`). The chip is - the same white on our fill, but it has no star. The message that names us has a ``★`` - in our hue at the left of its time (refer to :func:`mention_mark`). + the same white on our fill, but it has no star. The message that names us has our + white ``★`` at the left of its time (refer to :func:`mention_mark`). Our node with no name or no key (the device did not report them) keeps the bare ``★`` chip of a route. @@ -736,15 +736,15 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) -> def mention_mark(key: str | None) -> Text: - """The mark of a message that names us: the ``★`` of the app, in the hue of our key. + """The mark of a message that names us: the white ``★`` of our node. The chat puts it at the left of the time of each message that mentions our name, and the Messages list puts it at the left of the age of a row whose last message does (JP, - 2026-10-13). The hue is the colour of our name, the same colour as the chip that the - mention has in the body, so the eye joins the two. It is the same on each platform, and - with chips or without them. JP tried the yellow star of the map, the notch and the point - of our chip, and the point alone, and kept this one. It is the star of our node in the - rest of the app too (:data:`~meshterm.ui.marks.SELF_MARK`). + 2026-10-13). It is the star of our node in the rest of the app + (:data:`~meshterm.ui.marks.SELF_MARK`), white, the same as the label of the chip that + the mention has in the body. It is the same on each platform, and with chips or + without them. JP tried the yellow star of the map, the notch and the point of our chip, + the point alone, and the star in our hue, and kept this one (2026-10-14). Args: key: Our public key. With ``None``, the star has the keyless grey, the same as the @@ -1112,7 +1112,7 @@ 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 ``★`` in our hue, plain nodes +# 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 @@ -1145,7 +1145,7 @@ def node_marker(node_type: int | None) -> tuple[str, RGB]: def self_marker() -> tuple[str, RGB]: - """The map marker of our node, the ``★`` in our hue, as a ``(glyph, rgb)`` pair.""" + """The map marker of our node, the white ``★``, as a ``(glyph, rgb)`` pair.""" return SELF_MARK[0], mark_rgb(SELF_MARK[1]) diff --git a/tests/conftest.py b/tests/conftest.py index aab2049..8e7b600 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,21 +119,6 @@ def not_ignored(paths: Iterable[Path]) -> list[Path]: return [path for path, name in zip(paths, relative, strict=True) if name not in ignored] -@pytest.fixture(autouse=True) -def _no_self_key() -> Iterator[None]: - """Start and end each test with no key for our star (the yellow of the map). - - The colour of our ``★`` is global: the device-state cache sets it from the self-info - that it reads. A test that opens a mock device sets it. Thus each test starts clean, - and no test sees the hue of an earlier test. - """ - from meshterm.ui.theme import set_self_key - - set_self_key(None) - yield - set_self_key(None) - - @pytest.fixture(autouse=True) def _plain_path_rendering(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: monkeypatch.setenv("MESHTERM_POWERLINE", "0") diff --git a/tests/test_chat.py b/tests/test_chat.py index 7d353d4..d914da5 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1530,7 +1530,7 @@ def test_channel_self_style_keyed_on_concept_not_label() -> None: white = _ink(mine._group_header("you", ours), " ★ Me ") # our name, in white assert white.startswith("bold #ffffff on ") screen = _channel_screen([]) - assert _ink(screen._group_header("you", ours), "★") == "you.mark" # no name: our star + assert _ink(screen._group_header("you", ours), "★") == "you" # no name: the white star assert _ink(screen._group_header("you", theirs), "you") == "node.unknown" # no key: grey keyed = _channel_screen([], key_of=_keys_of({"you": "d4" + "0" * 62})) remote = _ink(keyed._group_header("you", theirs), "you") diff --git a/tests/test_device_state.py b/tests/test_device_state.py index 0a214db..e133beb 100644 --- a/tests/test_device_state.py +++ b/tests/test_device_state.py @@ -107,32 +107,6 @@ def _devstate( return DeviceState(ctx) # type: ignore[arg-type] -def test_each_self_info_read_goes_to_the_hook() -> None: - """The context gives our key to the theme through this hook (our star takes our hue).""" - dev = FakeDevice() - ds = _devstate(dev) - seen: list[dict] = [] - ds.on_self_info = seen.append - - async def run() -> None: - await ds.self_info() - await ds.self_info() # from the cache: no new read, so no new call - - asyncio.run(run()) - assert seen == [{"name": "node", "tx_power": 20}] - - -def test_the_context_publishes_our_key_to_the_theme() -> None: - """A self-info with a key gives our star the hue of that key.""" - from meshterm.context import _publish_self_key - from meshterm.ui.theme import active_theme, node_style - - _publish_self_key({"public_key": "5e7f"}) - assert active_theme().styles["you.mark"].color.name in node_style("5e7f") - _publish_self_key({}) # no key: the yellow of the map again - assert active_theme().styles["you.mark"].color.name == "#facc15" - - def test_stable_facts_are_fetched_once_and_served_from_cache() -> None: """MeshTerm reads self-info, the path hash mode, and channels from the radio one time. diff --git a/tests/test_pathline.py b/tests/test_pathline.py index e85cc6f..2c6bb76 100644 --- a/tests/test_pathline.py +++ b/tests/test_pathline.py @@ -40,7 +40,6 @@ from meshterm.ui.pathline import ( WRAP_OFFSET, PathHop, PathLine, - _self_ink, _style_hex, cut_mark, cut_to, @@ -927,12 +926,12 @@ def test_bare_self_stands_us_on_a_star_and_fades_both_ends() -> None: assert all(line.cell_len <= 28 for line in lines) -def test_bare_self_keeps_us_in_our_hue_where_nothing_is_composed() -> None: +def test_bare_self_keeps_us_in_the_you_white_where_nothing_is_composed() -> None: """``dim_self=False`` makes the stars not muted: the fade means "not for you to compose". A surface that only *shows* a walk (the boards and the record card of the Trophy case) - composes nothing. Thus there is no "not yours" to say, and our star has the colour of - our star in other places (``you.mark``, the hue of our key). A + composes nothing. Thus there is no "not yours" to say, and our node reads in the ``you`` + white of the app. This is the same colour that our star has in other places. A ``dim_from`` that reaches a star still fades it, so a mirrored return leg keeps its grey in both cases. """ @@ -941,15 +940,15 @@ def test_bare_self_keeps_us_in_our_hue_where_nothing_is_composed() -> None: hops, prefix_bytes=2, self_name="Me", bare_self=True, dim_self=False, mode="plain" ).text() stars = [s for s in text.spans if text.plain[s.start : s.end] == SELF_GLYPH] - assert [str(s.style) for s in stars] == ["you.mark", "you.mark"] - # In chips the same identity is the star of the map on the neutral dark grey. The fill - # is not a hue, because our end is a fixture and not a node that the user must tell - # from other nodes. + assert [str(s.style) for s in stars] == ["you", "you"] + # In chips the same identity is the white star of the map on the neutral dark grey. The + # fill is not a hue, because our end is a fixture and not a node that the user must + # tell from other nodes. chips = path_line( hops, prefix_bytes=2, self_name="Me", bare_self=True, dim_self=False, mode="powerline" ).text() fills = [str(s.style) for s in chips.spans if chips.plain[s.start : s.end] == SELF_GLYPH] - assert fills == [f"bold {_self_ink()} on {_YOU_BG}"] * 2 + assert fills == [f"bold #ffffff on {_YOU_BG}"] * 2 # The explicit fade still wins: a star inside a dimmed return leg stays grey. faded = path_line( @@ -962,7 +961,7 @@ def test_bare_self_keeps_us_in_our_hue_where_nothing_is_composed() -> None: mode="plain", ).text() tail = [s for s in faded.spans if faded.plain[s.start : s.end] == SELF_GLYPH] - assert [str(s.style) for s in tail] == ["you.mark", "faint"] + assert [str(s.style) for s in tail] == ["you", "faint"] def test_action_mark_rides_outside_the_width_budget() -> None: diff --git a/tests/test_records_screen.py b/tests/test_records_screen.py index 7684844..fc8298c 100644 --- a/tests/test_records_screen.py +++ b/tests/test_records_screen.py @@ -780,10 +780,10 @@ async def test_a_board_row_spends_its_cells_on_the_walk(tui_ctx) -> None: assert "2 nodes" in row # the score, in the unit of the discipline assert row.count("★") == 2 # our two ends, bare: the row does not name us twice assert "Homestead" not in row # …so the device name is not on the line - # Also, those ends are us, in the colour of our star. We compose nothing here, so + # Also, those ends are us, in the white of "you". We compose nothing here, so # there is no "not yours" fade to use. stars = [s for s in label.spans if row[s.start : s.end] == "★"] - assert [str(s.style) for s in stars] == ["you.mark", "you.mark"] + assert [str(s.style) for s in stars] == ["you", "you"] assert "3d" in row and "f2" in row # the hops, at the hash width of the record # The lanes before the walk keep to what they must show: a score lane with the width # of the widest score of this board (no dead padding), and the day without the minute diff --git a/tests/test_widgets.py b/tests/test_widgets.py index a397734..2a593f3 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -405,39 +405,18 @@ def test_a_mention_of_us_is_white_with_no_star(powerline) -> None: @pytest.mark.parametrize("chips", [True, False]) def test_the_mark_of_a_message_that_names_us_is_our_star(powerline, chips) -> None: - """The ``★`` of our node, with chips or without them: the theme name of our hue.""" + """The white ``★`` of our node, with chips or without them.""" powerline(chips) mark = mention_mark("5e7f") - assert mark.plain == "★" and str(mark.style) == "you.mark" + assert mark.plain == "★" and str(mark.style) == "you" assert str(mention_mark(None).style) == "node.unknown" # no key: the keyless grey -def test_our_star_takes_the_hue_of_our_key_everywhere() -> None: - """One call gives the theme name of our star our hue, and the render cache forgets it. - - Before the device reports our key, the star has the yellow of the map. ``None`` puts the - yellow back. - """ - from rich.style import Style - - from meshterm.ui.marks import SELF_MARK, parse_hex - from meshterm.ui.theme import active_theme, mark_rgb, set_self_key - from meshterm.ui.tui.render import render_to_ansi +def test_our_star_is_white_on_the_map() -> None: + """The map marker of our node is the white of ``you`` (JP, 2026-10-14).""" from meshterm.ui.widgets import self_marker - star = Text(SELF_MARK[0], style=SELF_MARK[1]) - yellow = render_to_ansi(star, 1) - assert self_marker() == ("★", (0xFA, 0xCC, 0x15)) - - set_self_key("5e7f") - hue = node_style("5e7f") - assert active_theme().styles["you.mark"] == Style.parse(hue) - assert render_to_ansi(star, 1) != yellow # the cache did not keep the old bytes - assert mark_rgb("you.mark") == parse_hex(hue.split()[-1]) # the map draws the same hue - assert self_marker()[1] == parse_hex(hue.split()[-1]) - - set_self_key(None) - assert render_to_ansi(star, 1) == yellow + assert self_marker() == ("★", (0xFF, 0xFF, 0xFF)) def test_self_chip_keeps_the_bare_star_without_our_name_or_key(powerline) -> None: