The selected chat message whitens its author chip and its unknown mentions

The author chip of the selected message's group now turns its label white,
as the message's text does, and goes back when the selection leaves the
group. A mention of an unknown node turns white too: its grey fill already
says the node is unknown, and white reads better on it than black.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Haiku 5.5 committed 2026-10-19 23:59:45 -04:00
1 parent 7264a71104
commit 8dff5bbb29
5 files changed
+128 -27

No files matched your search

+5 -3
View File
@@ -593,9 +593,11 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
(`PATH_INK`, a style that draws nothing) and the fold skips what lies inside it. That is
what makes the arrow form say what the chip form always said, its fills having never been
in the fold's vocabulary. The one exception is a chat's **selected message**: its mention
chips go white with its text (JP, 2026-10-19) — the label only, the fill kept, keyed hues
only (`widgets.picked_hop`: `white_ink` on a chip, `cursor` in arrow mode). A mention is
a name in prose, not a hop of a route. Reverse-video chips — a dialog's committing button,
chips and its group's author chip go white with its text (JP, 2026-10-19) — the label
only, the fill kept (`widgets.picked_hop`: `white_ink` on a chip, `cursor` in arrow mode).
An unknown node goes white too: its grey fill already says it is unknown, and white reads
better on it than its black label. A mention or an author is a name in a conversation,
not a hop of a route. Reverse-video chips — a dialog's committing button,
a running action's Abort, the arrow-mode composer slot, the editor cursor — are the
`selected` fill, a **grey** block (`muted`'s grey; light grey slot 7 on the VT, the one
grey a reverse can put behind text there, and the same fill the F-key lane uses). A chip
+37 -5
View File
@@ -677,6 +677,9 @@ class ChatScreen(Screen):
boundaries: list[int] = []
prev_group: tuple[bool, str] | None = None
prev_day = None
# The first message of the group of the selected message. Its slice has the author
# chip of the group, which is white while the selection is in the group.
picked_head = self._picked_head()
for idx in range(total):
message = messages[idx]
@@ -688,9 +691,12 @@ class ChatScreen(Screen):
group = (message.outbound, sender)
new_day = day != prev_day
selected = idx == self._selected
# A slice that the selection changes renders again, now and on the next paint
# after the selection leaves it: the selected message, and the head of its group.
marked = selected or idx == picked_head
pending = message.acked is None and message.outbound and not message.is_channel
reusable = (
not selected
not marked
and not pending
and idx < cache_count
and not cached_selected[idx]
@@ -725,7 +731,7 @@ class ChatScreen(Screen):
self._sticky_headers.append((len(lines), [divider[0]]))
lines += divider
if new_day or group != prev_group:
header = self._group_header(sender, message)
header = self._group_header(sender, message, picked=idx == picked_head)
lines += render_lines(header, width)
if selected:
# The first message has the head of the transcript: nothing comes before
@@ -741,7 +747,7 @@ class ChatScreen(Screen):
prev_group, prev_day = group, day
ids.append(id(message))
ackeds.append(message.acked)
selecteds.append(selected)
selecteds.append(marked)
boundaries.append(len(lines))
self._render_cache = {
@@ -770,7 +776,28 @@ class ChatScreen(Screen):
return (name or "·"), body
return (self._name(message.peer) or message.peer or "?"), message.text
def _group_header(self, sender: str, message: ChatMessage) -> Text:
def _picked_head(self) -> int | None:
"""The index of the first message of the group of the selected message, or ``None``.
A group is a run of messages from one sender on one day, under one author chip
(refer to :meth:`_render_grouped`). The chip is drawn with the first message of the
run, so that message is the one to draw again when the selection is in the run.
"""
selected, messages = self._selected, self._messages
if selected is None or not 0 <= selected < len(messages):
return None
def group_of(message: ChatMessage) -> tuple:
day = message.created_at.astimezone().date()
return (day, message.outbound, self._sender_and_body(message)[0])
group = group_of(messages[selected])
head = selected
while head > 0 and group_of(messages[head - 1]) == group:
head -= 1
return head
def _group_header(self, sender: str, message: ChatMessage, *, picked: bool = False) -> Text:
"""Build the sender header that starts a group: the name, drawn as a chip.
The chip makes a *label* different from the prose under it. A name alone above its
@@ -792,6 +819,11 @@ class ChatScreen(Screen):
A sender that is a locked contact has the padlock before its name, in the place of
the ``★`` of our chip (JP, 2026-10-19). Thus the chip shows the state that ^K
toggles.
With ``picked``, the selected message is in this group, and the label of the chip
is white, as the text of that message is (refer to
:func:`~meshterm.ui.widgets.picked_hop`, JP, 2026-10-19). Our chip already has a
white label.
"""
if message.outbound:
return self_chip(self._self_name, self._self_key, role=ORIGIN)
@@ -799,7 +831,7 @@ class ChatScreen(Screen):
key = None if sender == "·" else self._header_key(sender, message)
pages = self._pages
locked = key is not None and pages is not None and pages.lock_state(pages.find(key)) is True
return author_chip(sender, key, locked=locked)
return author_chip(sender, key, locked=locked, picked=picked)
def _header_key(self, sender: str, message: ChatMessage) -> str | None:
"""The key that gives the colour of the sender chip of a group: the key of the name.
+10 -4
View File
@@ -325,6 +325,8 @@ class PathHop:
the fill is the same as the fill of each known sender, and the white label
tells it from them (JP, 2026-10-14). Arrow mode draws the same pair, with the
padding of a chip. The PicoCalc draws it too, white on the dim twin of the hue.
On a quiet chip, it is a white label on the grey fill: a node with no key on the
selected message of a chat (refer to :func:`~meshterm.ui.widgets.picked_hop`).
quiet: A node with no key in a chat: a sender or a mention that nobody can
identify. Its chip is black on a grey fill (:data:`_QUIET_BG`), so it does not
stand out among the senders that have a colour (JP, 2026-10-14). A hop with a
@@ -1230,7 +1232,8 @@ def _spectrum_colours(hop: PathHop) -> _ChipColours:
thing when it fades the name.
Our chip in a chat (``white_ink``) has the fill of the chip of its key, and a white
label. A quiet chip (a node with no key in a chat) is black on a grey fill.
label. A quiet chip (a node with no key in a chat) is black on a grey fill, or white on
it with ``white_ink``.
"""
if hop.cursor:
return _ChipColours(_style_hex("cursor") or _KEYLESS_BG, _CHIP_FG, _CHIP_FG)
@@ -1247,7 +1250,8 @@ def _spectrum_colours(hop: PathHop) -> _ChipColours:
if hue:
return _ChipColours(_chip_fill(hue), hue, hue)
if hop.quiet:
return _ChipColours(_QUIET_BG, _QUIET_FG, _QUIET_FG)
ink = _WHITE if hop.white_ink else _QUIET_FG
return _ChipColours(_QUIET_BG, ink, ink)
return _ChipColours(_KEYLESS_BG, _KEYLESS_FG, _KEYLESS_FG)
@@ -1269,7 +1273,8 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
(:func:`_spectrum_colours`).
Our chip in a chat (``white_ink``) is white on the dim twin of its hue, the same rule as
on a truecolor terminal. A quiet chip is black on the light grey of the keyless chip.
on a truecolor terminal. A quiet chip is black on the light grey of the keyless chip, or
white on it with ``white_ink``.
"""
if hop.cursor:
return _ChipColours(_SLOT_PAGE, _SLOT_WHITE, _SLOT_WHITE)
@@ -1288,7 +1293,8 @@ def _slot_colours(hop: PathHop) -> _ChipColours:
if hop.white_ink and hue:
return _ChipColours(DIM_TWIN[ink], _SLOT_WHITE, _SLOT_WHITE)
if hop.quiet and not hue:
return _ChipColours(DIM_TWIN[ink], _SLOT_PAGE, _SLOT_PAGE)
label = _SLOT_WHITE if hop.white_ink else _SLOT_PAGE
return _ChipColours(DIM_TWIN[ink], label, label)
return _ChipColours(DIM_TWIN[ink], ink, ink)
+17 -12
View File
@@ -684,7 +684,7 @@ def name_chip(
return hop_chip(hop, role=role)
def author_chip(name: str, key: str | None, *, locked: bool = False) -> Text:
def author_chip(name: str, key: str | None, *, locked: bool = False, picked: bool = False) -> Text:
"""The author of a message in a chat: the chip of the node, which closes on the point.
It is :func:`name_chip` with the role of an origin, and one difference: a node that no
@@ -701,12 +701,15 @@ def author_chip(name: str, key: str | None, *, locked: bool = False) -> Text:
name: The name as it must read.
key: Any known prefix of the key of the node, or ``None``.
locked: Whether the author is a contact that is locked against the archive sweep.
picked: Whether the selected message of the chat is in the group of this chip.
Then the label is white (refer to :func:`picked_hop`).
Returns:
The chip as a one-line :class:`Text`.
"""
label = f"{glyph(LOCK_ICON)} {name}" if locked else name
return hop_chip(PathHop(label, key=key or None, quiet=True), role=ORIGIN)
hop = PathHop(label, key=key or None, quiet=True)
return hop_chip(picked_hop(hop) if picked else hop, role=ORIGIN)
def self_chip(name: str | None, key: str | None, *, role: str | None = None) -> Text:
@@ -792,18 +795,20 @@ def mention_hop(name: str, key: str | None, *, you: bool = False) -> PathHop:
def picked_hop(hop: PathHop) -> PathHop:
"""``hop`` on the selected message of a chat: its label in the white of the selection.
The text of the selected message is white (``cursor``), and its chips follow it (JP,
2026-10-19). A chip keeps its fill, so it still tells one node from the next, and only
its label turns white: the white label of our own chip
(:attr:`~meshterm.ui.pathline.PathHop.white_ink`). Arrow mode has no fill, so there the
label takes the ``cursor`` white, as each other name on a selected row does.
The text of the selected message is white (``cursor``), and its chips follow it: the
mentions in its body, and the author chip of its group (JP, 2026-10-19). A chip keeps
its fill, so it still tells one node from the next, and only its label turns white: the
white label of our own chip (:attr:`~meshterm.ui.pathline.PathHop.white_ink`). Arrow
mode has no fill, so there the label takes the ``cursor`` white, as each other name on a
selected row does.
The rule is the rule of the white fold of a selected row (refer to
:func:`~meshterm.ui.tui.render._whiten_identities`): only the hue of a key turns white.
A node that no key can place keeps its grey, because the selection must not claim to
know that node. A hop that already has a white label stays as it is.
A node that no key can place turns white too. Its grey fill already says that the node
is unknown (JP, 2026-10-19), and white on that fill reads better than its black label.
This is different from the white fold of a list row (refer to
:func:`~meshterm.ui.tui.render._whiten_identities`), where a grey name has no fill to
keep the grey. A hop that already has a white label stays as it is.
"""
if not hop.key or hop.white_ink:
if hop.white_ink:
return hop
if powerline_enabled():
return replace(hop, white_ink=True)
+59 -3
View File
@@ -3262,8 +3262,8 @@ def test_the_chips_of_the_selected_message_turn_white_with_its_text(
"""On the selected message, a mention chip has a white label, as the text around it does.
JP, 2026-10-19. A chip keeps its fill, so it still tells one node from the next. In
arrow mode, the label takes the ``cursor`` white. A node that no key can place keeps its
grey, as on each selected row: the selection must not claim to know that node.
arrow mode, the label takes the ``cursor`` white. A node that no key can place turns
white too: its grey fill already says that the node is unknown.
"""
powerline(chips)
keys = {"alice": "a1" * 32, "bob": "b2" * 32}
@@ -3285,7 +3285,63 @@ def test_the_chips_of_the_selected_message_turn_white_with_its_text(
white = "#ffffff" if chips else "cursor"
assert white in styles("Bob", selected=True)
assert white not in styles("Bob", selected=False)
assert white not in styles("Zed", selected=True)
assert white in styles("Zed", selected=True)
assert white not in styles("Zed", selected=False)
@pytest.mark.parametrize("chips", [True, False])
def test_the_author_chip_of_the_selected_message_turns_white(
powerline, # noqa: ANN001
chips: bool,
) -> None:
"""The author chip of the group of the selected message has a white label (JP, 2026-10-19).
The chip is drawn with the first message of the group. Thus that message draws again
when the selection is lower in the group, and again when the selection leaves the group,
although the transcript keeps each message in a cache.
"""
powerline(chips)
keys = {"alice": "a1" * 32}
screen = ChatScreen(
Conversation(label="#public", is_channel=True, channel_idx=0),
[
ChatMessage(text="Alice: one", is_channel=True, channel_idx=0),
ChatMessage(text="Alice: two", is_channel=True, channel_idx=0),
ChatMessage(text="Zed: three", is_channel=True, channel_idx=0),
],
send=None,
names={},
session=_StubSession(),
key_of=lambda name: keys.get(name.casefold()),
)
white = "#ffffff" if chips else "cursor"
for sender, message in (("Alice", screen._messages[0]), ("Zed", screen._messages[2])):
picked = screen._group_header(sender, message, picked=True)
plain = screen._group_header(sender, message)
assert white in " ".join(str(s.style) for s in picked.spans)
assert white not in " ".join(str(s.style) for s in plain.spans)
calls: list[tuple[str, bool]] = []
header = screen._group_header
def spy(sender, message, *, picked=False): # noqa: ANN001, ANN202
calls.append((sender, picked))
return header(sender, message, picked=picked)
screen._group_header = spy # type: ignore[method-assign]
screen.render_body(60)
assert calls == [("Alice", False), ("Zed", False)]
calls.clear()
screen.handle("up")
screen.handle("up") # the second message of Alice: its group is the first one
screen.render_body(60)
assert calls == [("Alice", True)]
calls.clear()
screen.handle("down") # Zed: the chip of Alice goes back, and the chip of Zed is white
screen.render_body(60)
assert calls == [("Alice", False), ("Zed", True)]
def test_the_lock_atom_never_pushes_another_key_out_of_the_hint(tmp_path: Path) -> None: