diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bf641..d71aa17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,10 @@ 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 has the colour of your key, but in reverse: dark text on a light fill, where each other sender has light text on a dark fill. A mention of your name has - the same reversed colours, and the message that has it gets a `★` at the left of its - time. Thus the messages that name you are easy to find down the left side of the chat. - On the PicoCalc, the console cannot draw a light fill, so there the `★` alone marks you. - Before, your messages showed only the `★`. + the same reversed colours, and the message that has it gets a small arrow mark 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. On the PicoCalc, the console cannot draw a light fill, so + there the `★` marks your messages. Before, your messages showed only the `★`. - **A mention has no `@` in front of it.** The coloured box around the name already says that it is a mention. On a terminal that cannot draw the boxes, a mention keeps its `@`. - **The line where you write a message shows each mention as a box.** When you type the diff --git a/CLAUDE.md b/CLAUDE.md index aa7134c..47182cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -488,9 +488,12 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow the star in the chip's dark ink: a yellow star measured under 3:1 on the light fill for 238 of 256 hues, and the star on the page right of the chip was tried and put back (JP, 2026-10-13). A **mention** of us is the turned-over chip with no star - (`mention_chip(you=True)`); the message carrying it gets the `★` in the two cells left - of its time instead (`❯ ★ 12:34 body` — every time sits two cells right to keep the - column; the Messages list puts it left of the row's age). Arrow mode draws the chip as + (`mention_chip(you=True)`); the message carrying it gets **the two ends of our chip** + in the two cells left of its time instead — notch then point, no label, in our chip's + fill (`widgets.mention_mark` → `pathline.chip_ends`; `>>` in our hue in arrow mode; + it replaced a `★`, JP 2026-10-13). Every time sits two cells right to keep the column + (`❯`, a space, the mark, then `12:34 body`), and the Messages list puts the mark left + of the row's age. Arrow mode draws the chip as a padded reverse-video block. The PicoCalc can't put a bright slot behind text, so there the chip keeps the normal colours and the `★` alone marks us. With no name or key from the device, the bare `★` chip stays. Path lines (delivery paths, the packet viewer, routes) keep `★`. diff --git a/meshterm/tools/chat.py b/meshterm/tools/chat.py index 819c6b7..2771058 100644 --- a/meshterm/tools/chat.py +++ b/meshterm/tools/chat.py @@ -57,7 +57,6 @@ from ..services.trace_runner import ( ) from ..ui import renderers from ..ui.fields import ChannelRef, NodeRef -from ..ui.marks import SELF_MARK from ..ui.menus import Lane, column_header, fit_cells, section_heading from ..ui.pathline import PathHop from ..ui.termfont import powerline_enabled @@ -71,6 +70,7 @@ from ..ui.widgets import ( format_age, hop_chip, mention_chip, + mention_mark, self_chip, ) from .base import Tool, ToolResult, register @@ -986,15 +986,15 @@ 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 the ``★`` at the left of its age, the same as the - # transcript has it at the left of the time (refer to ChatScreen._body_lines). + # last message that names us has the ends of our chip at the left of its age, the same + # as the transcript has them 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}}" if age and last is not None and _names_us(last, us): - star = len(lane) - len(age) - 2 # the star, a space, then the age - text.append(lane[:star]) - text.append(SELF_MARK[0], style="type.you") - text.append(lane[star + 1 :], style="muted") + mark = len(lane) - len(age) - 2 # the two cells of the mark, then the age + text.append(lane[:mark]) + text.append_text(mention_mark(us[1])) + text.append(age, style="muted") else: text.append(lane, style="muted") text.append(" ") @@ -1057,8 +1057,8 @@ def _preview_text( - Our own messages start with our chip, ``★ Name`` turned over (refer to :func:`~meshterm.ui.widgets.self_chip`). A mention of our name has the chip that is - turned over, with no star. The star of a message that names us is at the left of the - age of the row (refer to :func:`_title`). + turned over, with no star. A message that names us has the ends of our chip 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). - A room post starts with its author, named by key in the same way as the room screen diff --git a/meshterm/ui/chat.py b/meshterm/ui/chat.py index 395fb33..0595ca4 100644 --- a/meshterm/ui/chat.py +++ b/meshterm/ui/chat.py @@ -33,7 +33,6 @@ from ..core.models import ChatMessage, Contact, Conversation, Message, utcnow from ..core.regions import WILDCARD from ..platforms import Platform, on_platform from .compose import MentionEditor -from .marks import SELF_MARK from .pathline import PATH_INK, PathHop from .qr import QrScreen, qr_strip from .theme import snr_style @@ -45,7 +44,15 @@ from .tui.prompt import ( from .tui.render import render_hanging, render_lines, right_aligned_tail from .tui.screen import CANCEL, Screen from .tui.spinner import Spinner, spinner_interval -from .widgets import DESTINATION, ORIGIN, hop_chip, mention_hop, name_chip, self_chip +from .widgets import ( + DESTINATION, + ORIGIN, + hop_chip, + mention_hop, + mention_mark, + name_chip, + self_chip, +) if TYPE_CHECKING: from ..context import AppContext @@ -69,7 +76,7 @@ _WORD = re.compile(r"\S+") #: The column where a URL starts when it is too long to hang under its body: the column #: after the ``❯`` that is in front of the first line of the selected message. A message -#: that names us has its ``★`` in this column, and the timestamp follows it. +#: that names us has its mark in this column, and the timestamp follows it. _URL_GUTTER = 2 #: Whether the URLs of a message are drawn as QR codes under it (``Platform.url_codes``, @@ -768,12 +775,14 @@ 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 the ``★`` of the app at the left of its time - (JP, 2026-10-13). The chip of the mention in the body is turned over, but it has no - star (refer to :func:`~meshterm.ui.widgets.mention_hop`). Thus the star 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 message that mentions our name has a mark at the left of its time: the two ends + of our chip, in its colour, with no label (refer to + :func:`~meshterm.ui.widgets.mention_mark`, JP, 2026-10-13). The chip of the mention + in the body is turned over, but it has 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. 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 @@ -789,8 +798,7 @@ class ChatScreen(Screen): prefix = Text() prefix.append("❯ " if selected else " ", style="cursor" if selected else None) if self._names_us(body): - prefix.append(SELF_MARK[0], style="type.you") - prefix.append(" ") + prefix.append_text(mention_mark(self._self_key)) else: prefix.append(" ") prefix.append(f"{stamp:%H:%M} ", style="cursor" if selected else "muted") @@ -847,8 +855,8 @@ 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, turned over, and the line has a ``★`` at the left of its time (refer to - :meth:`_body_lines`). Thus a message that names us is easy to see. + chip, turned over, and the line has the ends of our chip 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 destination of a route that came from the sender (refer to :meth:`_group_header`, @@ -881,7 +889,7 @@ class ChatScreen(Screen): return mention_hop(name, self._sender_key(name, mention=True)) def _names_us(self, body: str) -> bool: - """Whether ``body`` has a mention of our name (it gets a ``★`` before its time).""" + """Whether ``body`` has a mention of our name (it gets a mark before its time).""" if not self._self_name: return False return any(match.group(1) == self._self_name for match in MENTION.finditer(body)) diff --git a/meshterm/ui/pathline.py b/meshterm/ui/pathline.py index ef593b0..4bdd8b7 100644 --- a/meshterm/ui/pathline.py +++ b/meshterm/ui/pathline.py @@ -1294,6 +1294,27 @@ def _bind_chips(platform: Platform) -> None: _colours_of, _WEIGHT, _FLAT = _slot_colours, "not bold ", "not bold " +def chip_ends(hop: PathHop) -> Text: + """The two ends of the chip of ``hop`` with no label between them: the notch, then the point. + + It is two cells in the fill of the chip, the same two cells that open and close a chip + of a destination (refer to :meth:`PathLine._notch`). A chat puts it at the left of the + time of a message that names us, in the fill of our chip (refer to + :func:`~meshterm.ui.widgets.mention_mark`). The colours come from the palette of the + platform, so the PicoCalc draws it in the fill that the chip has there. + + Args: + hop: The hop whose chip gives the fill. + + Returns: + The two styled cells. + """ + fill = _colours_of(hop).fill + text = PathLine._notch(fill) + text.append(POWERLINE_SEP, style=f"{_FLAT}{fill}") + return text + + def _shorten(value: str, hash_bytes: int | None) -> str: """Bare hex in lower case, truncated to ``hash_bytes`` bytes (whole when it is empty).""" raw = value.lower().removeprefix("0x") diff --git a/meshterm/ui/theme.py b/meshterm/ui/theme.py index 0732431..d3a25d7 100644 --- a/meshterm/ui/theme.py +++ b/meshterm/ui/theme.py @@ -173,10 +173,6 @@ MESH_THEME = Theme( "type.repeater": "#a78bfa", "type.room": "#ffffff", "type.sensor": "#fb923c", - # The ``★`` of our node where it stands alone on the page as a mark: after our name - # in a chat, and before the time of a message that names us. It is the yellow of - # the map (``marks.SELF_MARK``), with a name so that the console chooses its slot. - "type.you": "#facc15", # A region name (the scope of a flood). It is not a node, so it has no hue from the # node wheel. It is a light slate. Its slant makes it different from prose, as a # tag is different from a sentence. @@ -416,7 +412,6 @@ MESH_THEME_16 = Theme( "type.repeater": "not bold color(5)", "type.room": "color(15)", "type.sensor": "not bold color(3)", - "type.you": "color(11)", # A region name: the VT has no italic, and each chromatic slot is a node hue. Thus # the style is the plain light grey. The word "scope" in front of it makes it # different from other text. diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index 544890e..77408d9 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -57,7 +57,7 @@ from .marks import ( LabelRgbOf, parse_hex, ) -from .pathline import SELF_GLYPH, PathHop, PathLine, path_line +from .pathline import SELF_GLYPH, PathHop, PathLine, chip_ends, path_line from .termfont import powerline_enabled from .theme import glyph, mark_rgb, name_style, node_style, snr_style @@ -698,8 +698,8 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) -> :attr:`~meshterm.ui.pathline.PathHop.inverse`). A mention of our name is a different mark (refer to :func:`mention_chip`). The chip is - turned over, but it has no star. The star of a message that names us is at the left of - the time of the message (refer to :meth:`~meshterm.ui.chat.ChatScreen._body_lines`). + turned over, but it has no star. The message that names us has the ends of our chip 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. @@ -717,6 +717,29 @@ def self_chip(name: str | None, key: str | None, *, role: str | None = None) -> return hop_chip(PathHop(f"{SELF_GLYPH} {name}", key=key, inverse=True), role=role) +def mention_mark(key: str | None) -> Text: + """The mark of a message that names us: the two ends of our chip, with no label. + + The notch, then the point, in the fill of our chip (refer to + :func:`~meshterm.ui.pathline.chip_ends`). The chat puts it in the two cells 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). It replaced a + ``★``. The mark has the colour of the chip that the mention has in the body, so the eye + joins the two. Where the terminal draws no chips, it is ``>>`` in our hue, the colour + that our chip has there. + + Args: + key: Our public key, which gives the hue. With ``None``, the mark has the keyless + grey, the same as the chip of the mention. + + Returns: + The mark, two cells wide. + """ + if powerline_enabled(): + return chip_ends(PathHop("", key=key or None, inverse=bool(key))) + return Text(">>", style=node_style(key) if key else "node.unknown") + + def mention_hop(name: str, key: str | None, *, you: bool = False) -> PathHop: """The hop of a mention: the node that a message names, as its chip shows it. diff --git a/tests/test_chat.py b/tests/test_chat.py index 12367f4..af6fecd 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1608,7 +1608,7 @@ def test_a_blank_line_closes_each_author_where_the_rows_allow( def test_a_mention_of_our_name_takes_our_chip_with_no_star(powerline) -> None: """The chip of a mention of us is turned over, as our sender chip, but it has no star. - The star of a message that names us is at the left of its time (refer to the next + The mark of a message that names us is at the left of its time (refer to the next test). With chips, no mention has its ``@``. """ from meshterm.ui.theme import node_style @@ -1621,8 +1621,12 @@ def test_a_mention_of_our_name_takes_our_chip_with_no_star(powerline) -> None: assert str(me.style).endswith(f"on {node_style(_OUR_KEY).split()[-1]}") # a light fill -def test_a_message_that_names_us_has_the_star_left_of_its_time() -> None: - """The ``★`` goes in the two cells before the time, and each time moves two cells right.""" +def test_a_message_that_names_us_has_the_mark_left_of_its_time() -> None: + """The mark goes in the two cells before the time, and each time moves two cells right. + + The suite draws no chips, so the mark is ``>>`` in our hue (refer to the widget tests + for the two ends of the chip). + """ from datetime import datetime, timezone at = datetime(2026, 7, 5, 14, 24, tzinfo=timezone.utc) @@ -1633,12 +1637,12 @@ def test_a_message_that_names_us_has_the_star_left_of_its_time() -> None: ] screen = _channel_screen(messages, self_name="Me", self_key=_OUR_KEY) rows = _strip_ansi("\n".join(screen._render_grouped(72))).splitlines() - assert any(row.startswith(f" ★ {stamp} hi") for row in rows) + assert any(row.startswith(f" >>{stamp} hi") for row in rows) assert any(row.startswith(f" {stamp} hi") for row in rows) # the other message screen.handle("up") - screen.handle("up") # the first message: the pointer, then the star + screen.handle("up") # the first message: the pointer, then the mark rows = _strip_ansi("\n".join(screen._render_grouped(72))).splitlines() - assert any(row.startswith(f"❯ ★ {stamp} hi") for row in rows) + assert any(row.startswith(f"❯ >>{stamp} hi") for row in rows) def _channel_screen( @@ -1750,7 +1754,7 @@ def test_the_compose_line_keeps_a_chip_whole_when_it_wraps(powerline) -> None: assert sum(chip in line for line in lines) == 1, width -def test_a_row_whose_last_message_names_us_has_the_star_left_of_its_age( +def test_a_row_whose_last_message_names_us_has_the_mark_left_of_its_age( repo: Repository, ) -> None: """The Messages list marks the row as the transcript marks the message.""" @@ -1760,10 +1764,10 @@ def test_a_row_whose_last_message_names_us_has_the_star_left_of_its_age( named = ChatMessage(text="Bob: hi @[Me]", is_channel=True, channel_id="c0") other = ChatMessage(text="Bob: hi @[Zed]", is_channel=True, channel_id="c0") lanes = _lanes([conv]) - starred = _title(ctx, conv, {"chan:c0": named}, _NO_KEYS, lanes, us=us).plain + marked = _title(ctx, conv, {"chan:c0": named}, _NO_KEYS, lanes, us=us).plain plain = _title(ctx, conv, {"chan:c0": other}, _NO_KEYS, lanes, us=us).plain - assert "★ now " in starred and "★" not in plain - assert starred.index("now") == plain.index("now") # the star takes a cell of the gap + assert ">>now " in marked and ">>" not in plain + assert marked.index("now") == plain.index("now") # the mark takes the cells of the gap def test_channel_typing_clears_selection() -> None: diff --git a/tests/test_widgets.py b/tests/test_widgets.py index e53fa0d..3e83b50 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -30,6 +30,7 @@ from meshterm.ui.widgets import ( hop_chip, mention_chip, mention_hop, + mention_mark, name_chip, path_text, revisit_note, @@ -398,6 +399,26 @@ def test_a_mention_of_us_is_turned_over_with_no_star(powerline) -> None: assert not mention_hop("Me", None, you=True).inverse # no key: no hue to turn over +def test_the_mark_of_a_message_that_names_us_is_the_ends_of_our_chip(powerline) -> None: + """The notch, then the point, in the fill of our chip, and no label between them.""" + powerline(True) + mark = mention_mark("5e7f") + fill = _span_style(mention_chip("Me", "5e7f", you=True), "Me").bgcolor.name + + assert mark.plain == POWERLINE_SEP * 2 + notch, point = (Style.parse(str(span.style)) for span in mark.spans) + assert notch.reverse and notch.color.name == fill # cut out of the fill + assert not point.reverse and point.color.name == fill # the point of the fill + + +def test_the_mark_without_chips_is_two_arrows_in_our_hue(powerline) -> None: + """With no chips, the mark is ``>>``, in the hue that our chip has in arrow mode.""" + powerline(False) + mark = mention_mark("5e7f") + assert mark.plain == ">>" and str(mark.style) == node_style("5e7f") + assert str(mention_mark(None).style) == "node.unknown" + + def test_self_chip_keeps_the_bare_star_without_our_name_or_key(powerline) -> None: """A device that did not report our name or key leaves the star of a route.""" powerline(True)