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) <noreply@anthropic.com>
This commit is contained in:
1 parent
feea5a89db
commit
796ff94dad
21 files changed
+84
-263
No files matched your search
+6
-7
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <meshterm.context.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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
+7
-8
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
+14
-26
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+10
-10
@@ -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])
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+9
-10
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-26
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user