From 7264a71104a89e4531841b3659e1d9a156687788 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Mon, 19 Oct 2026 23:48:59 -0400 Subject: [PATCH] The mention chips of a selected chat message go white with its text In a channel, a DM, or a room, the selected message's text is white, and now its mention chips follow: the label turns white and the fill stays, so the chips still tell one node from the next. Arrow mode whitens the label the same way. A mention that no key can place keeps its grey, as on every selected row. Co-Authored-By: Claude Haiku 5.5 --- CLAUDE.md | 5 ++++- meshterm/ui/chat.py | 9 ++++++++- meshterm/ui/widgets.py | 23 ++++++++++++++++++++++- tests/test_chat.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 02a5b12..04d6bcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -592,7 +592,10 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow what a route graph's one-byte labels are matched by — so `pathline` stamps its own extent (`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. Reverse-video chips — a dialog's committing button, + 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, 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 diff --git a/meshterm/ui/chat.py b/meshterm/ui/chat.py index c767196..b7b07eb 100644 --- a/meshterm/ui/chat.py +++ b/meshterm/ui/chat.py @@ -51,6 +51,7 @@ from .widgets import ( hop_chip, mention_hop, mention_mark, + picked_hop, self_chip, ) @@ -910,6 +911,9 @@ class ChatScreen(Screen): whose chip closes on the point). The chip has no ``@``, because the chip already says that the name is a mention (JP, 2026-10-13). Arrow mode has no chip, so there a mention keeps its ``@`` (refer to :func:`~meshterm.ui.widgets.mention_hop`). + + On the selected message, the label of each chip is white, as the text around it is + (refer to :func:`~meshterm.ui.widgets.picked_hop`, JP, 2026-10-19). """ base = "cursor" if selected else None text = Text() @@ -917,7 +921,10 @@ class ChatScreen(Screen): for match in MENTION.finditer(body): if match.start() > pos: text.append(body[pos : match.start()], style=base) - text.append_text(hop_chip(self._mention_hop(match.group(1)), role=DESTINATION)) + hop = self._mention_hop(match.group(1)) + if selected: + hop = picked_hop(hop) # the chips turn white with the text around them + text.append_text(hop_chip(hop, role=DESTINATION)) pos = match.end() if pos < len(body): text.append(body[pos:], style=base) diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index a371024..f8b1f6a 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -5,7 +5,7 @@ from __future__ import annotations import math from collections.abc import Callable, Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from datetime import datetime from itertools import pairwise from typing import TYPE_CHECKING @@ -789,6 +789,27 @@ def mention_hop(name: str, key: str | None, *, you: bool = False) -> PathHop: return PathHop(label, key=key or None, white_ink=you and bool(key), quiet=True) +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 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. + """ + if not hop.key or hop.white_ink: + return hop + if powerline_enabled(): + return replace(hop, white_ink=True) + return replace(hop, style="cursor") + + def mention_chip(name: str, key: str | None, *, you: bool = False) -> Text: """A mention in a message: the chip of the node that it names, which opens on the notch. diff --git a/tests/test_chat.py b/tests/test_chat.py index 6e89f70..ae4ebf9 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -3254,6 +3254,40 @@ def test_a_locked_author_has_the_padlock_on_its_sender_chip( assert "\U0001f512" not in shown and "Alice" in shown +@pytest.mark.parametrize("chips", [True, False]) +def test_the_chips_of_the_selected_message_turn_white_with_its_text( + powerline, # noqa: ANN001 + chips: bool, +) -> None: + """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. + """ + powerline(chips) + keys = {"alice": "a1" * 32, "bob": "b2" * 32} + body = "hi @[Bob] and @[Zed]" + screen = ChatScreen( + Conversation(label="#public", is_channel=True, channel_idx=0), + [ChatMessage(text=f"Alice: {body}", is_channel=True, channel_idx=0)], + send=None, + names={}, + session=_StubSession(), + key_of=lambda name: keys.get(name.casefold()), + ) + + def styles(label: str, *, selected: bool) -> str: + text = screen._render_mentions(body, selected=selected) + at = text.plain.index(label) + return " ".join(str(s.style) for s in text.spans if s.start <= at < s.end) + + 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) + + def test_the_lock_atom_never_pushes_another_key_out_of_the_hint(tmp_path: Path) -> None: """With a link and a location, the hint has no room for ^K, so the atom stays out.