Hash and name conventions: display budget, shared hue palette
highlighted_hash grows a width budget - shorter than the key shows an ellipsis, shorter keys pad so lanes align - making it THE hash widget. The chat sender palette moves to theme.name_style as the app-wide node name colour rule; our own node is always the pure-white "you" style (the nodes table's self row now follows it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9411bfc9b
commit
b50d201c24
5 files changed
+72
-38
No files matched your search
+7
-19
@@ -24,7 +24,7 @@ from rich.text import Text
|
||||
|
||||
from ..core.events import EventKind, MeshEvent
|
||||
from ..core.models import ChatMessage, Contact, Conversation, Message, utcnow
|
||||
from .theme import snr_style
|
||||
from .theme import name_style, snr_style
|
||||
from .tui.prompt import _LineEditor
|
||||
from .tui.render import render_hanging, render_lines
|
||||
from .tui.screen import CANCEL, Screen
|
||||
@@ -36,20 +36,6 @@ if TYPE_CHECKING:
|
||||
#: How many past messages to load into the transcript when a conversation opens.
|
||||
_HISTORY_LIMIT = 200
|
||||
|
||||
#: Palette of distinct, dark-theme-friendly colors cycled through to give each channel
|
||||
#: sender its own hue (red is reserved for errors, so it's excluded). ``you`` and unknown
|
||||
#: senders are styled separately.
|
||||
_SENDER_COLORS = (
|
||||
"bold #f472b6", # pink
|
||||
"bold #60a5fa", # blue
|
||||
"bold #34d399", # green
|
||||
"bold #a78bfa", # violet
|
||||
"bold #fb923c", # orange
|
||||
"bold #22d3ee", # cyan
|
||||
"bold #a3e635", # lime
|
||||
"bold #e879f9", # fuchsia
|
||||
)
|
||||
|
||||
#: Matches the ``Name: message`` convention channel senders use to identify themselves
|
||||
#: (the protocol carries no sender field). The name is 1–20 non-colon characters and must
|
||||
#: be followed by ``": "`` — conservative enough to leave ``http://…`` and ``note:x`` alone.
|
||||
@@ -89,11 +75,13 @@ _BYTES_ORANGE = "bold #ff9500"
|
||||
def _sender_hue(sender: str) -> str:
|
||||
"""The stable per-sender colour a name is drawn in, keyed on the name's characters.
|
||||
|
||||
Shared by the live transcript (sender headers, ``@mentions``) and the conversation list
|
||||
(a contact's colour dot, a channel preview's inline sender), so a person reads the same
|
||||
colour everywhere. ``you`` and unknown (``·``) senders are handled by the caller.
|
||||
The app-wide name palette (:func:`~meshterm.ui.theme.name_style`), shared by the live
|
||||
transcript (sender headers, ``@mentions``), the conversation list (a contact's colour
|
||||
dot, a channel preview's inline sender), the dashboard feed, and the packet viewer, so
|
||||
a person reads the same colour everywhere. ``you`` and unknown (``·``) senders are
|
||||
handled by the caller.
|
||||
"""
|
||||
return _SENDER_COLORS[sum(map(ord, sender)) % len(_SENDER_COLORS)]
|
||||
return name_style(sender)
|
||||
|
||||
|
||||
def _split_channel_sender(text: str) -> tuple[Optional[str], str]:
|
||||
|
||||
+34
-2
@@ -17,8 +17,8 @@ MESH_THEME = Theme(
|
||||
# Reversed error (the text-editor cursor sitting on an over-budget character). Baked
|
||||
# in for the same reason as ``selected`` — "reverse err" would render as plain text.
|
||||
"err.reverse": "reverse bold #f87171",
|
||||
# Our own messages in a chat: pure white, deliberately outside the per-sender hue
|
||||
# palette (see meshterm.ui.chat._SENDER_COLORS) so "you" is always easy to spot.
|
||||
# Our own node, anywhere it is named: pure white, deliberately outside the
|
||||
# per-name hue palette (see NAME_COLORS below) so "you" is always easy to spot.
|
||||
"you": "bold #ffffff",
|
||||
# A confirmed companion's name on the startup device picker: pure white so the
|
||||
# devices we've actually talked to before jump out above the merely-detected ports.
|
||||
@@ -121,6 +121,38 @@ def hint_style(border_style: str) -> str:
|
||||
return name if name in MESH_THEME.styles else "muted"
|
||||
|
||||
|
||||
#: Palette of distinct, dark-theme-friendly colors cycled through to give each node or
|
||||
#: sender name its own stable hue (red is reserved for errors, so it's excluded). The
|
||||
#: app-wide rule: a node's *name* is always coloured — by this palette, unless the
|
||||
#: context already colours it (a picker's recency heat, a chart's quality tint) — and
|
||||
#: our own node is always the pure-white ``you`` style instead, so "us" never blends
|
||||
#: into the crowd. Shared by the chat transcript, the dashboard feed, and the packet
|
||||
#: viewer, so one node reads as one colour everywhere.
|
||||
NAME_COLORS = (
|
||||
"bold #f472b6", # pink
|
||||
"bold #60a5fa", # blue
|
||||
"bold #34d399", # green
|
||||
"bold #a78bfa", # violet
|
||||
"bold #fb923c", # orange
|
||||
"bold #22d3ee", # cyan
|
||||
"bold #a3e635", # lime
|
||||
"bold #e879f9", # fuchsia
|
||||
)
|
||||
|
||||
|
||||
def name_style(name: str) -> str:
|
||||
"""The stable per-name colour a node or sender is drawn in, keyed on its characters.
|
||||
|
||||
Args:
|
||||
name: The display name (not a hash — hashes stay muted).
|
||||
|
||||
Returns:
|
||||
A style string from :data:`NAME_COLORS`; the same name always maps to the
|
||||
same hue, so a node keeps its colour across screens and sessions.
|
||||
"""
|
||||
return NAME_COLORS[sum(map(ord, name)) % len(NAME_COLORS)]
|
||||
|
||||
|
||||
def snr_style(snr: float | None) -> str:
|
||||
"""Return a theme style name describing an SNR value's quality.
|
||||
|
||||
|
||||
@@ -1244,7 +1244,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
|
||||
title = Text(f"{w} byte{'s' if w > 1 else ' '}")
|
||||
if sample:
|
||||
title.append(" ")
|
||||
title.append_text(highlighted_hash(sample[:16], w))
|
||||
title.append_text(highlighted_hash(sample, w, width=16))
|
||||
if w == device_width:
|
||||
title.append(" · device default", style="muted")
|
||||
items.append(Choice(title=title, value=w))
|
||||
|
||||
+28
-14
@@ -233,29 +233,41 @@ def traces_table(
|
||||
return table
|
||||
|
||||
|
||||
def highlighted_hash(value: str, prefix_bytes: int) -> Text:
|
||||
"""Render a full hex key with its leading path-hash prefix highlighted.
|
||||
def highlighted_hash(value: str, prefix_bytes: int, width: Optional[int] = None) -> Text:
|
||||
"""Render a hex key with its leading path-hash prefix highlighted — THE hash widget.
|
||||
|
||||
The first ``prefix_bytes`` bytes are the slice other nodes address in a forced trace
|
||||
path (the path-hash); they are shown in the brand colour and the remainder muted, so
|
||||
the addressable prefix stands out within the otherwise full key.
|
||||
The one way MeshTerm displays a hash, wherever one appears: the first
|
||||
``prefix_bytes`` bytes are the slice other nodes address in a forced trace path
|
||||
(the path-hash), shown in the brand colour with the remainder muted, so the
|
||||
addressable prefix stands out within the otherwise full key. A ``width`` budget
|
||||
shorter than the key ellipsizes it (the ``…`` takes the colour of the digit it
|
||||
replaces, so a highlight wider than the budget still reads as one).
|
||||
|
||||
Args:
|
||||
value: The full key as hex, optionally ``0x``-prefixed and mixed-case.
|
||||
value: The key as hex, optionally ``0x``-prefixed and mixed-case.
|
||||
prefix_bytes: Number of leading bytes the current path-hash mode addresses; ``0``
|
||||
(or negative) leaves the whole key un-highlighted.
|
||||
width: Display budget in cells; a longer key shows ``width - 1`` digits plus an
|
||||
ellipsis, a shorter one is right-padded to the budget so lanes stay aligned.
|
||||
``None`` shows the key whole, unpadded.
|
||||
|
||||
Returns:
|
||||
A styled :class:`Text` of the full key.
|
||||
A styled :class:`Text` of the key (exactly ``width`` cells when given).
|
||||
"""
|
||||
raw = value.lower().removeprefix("0x")
|
||||
split = max(0, prefix_bytes) * 2
|
||||
pad = 0
|
||||
ellipsis = False
|
||||
if width is not None and len(raw) > width:
|
||||
raw, ellipsis = raw[: max(0, width - 1)], True
|
||||
elif width is not None:
|
||||
pad = width - len(raw)
|
||||
text = Text()
|
||||
if split:
|
||||
text.append(raw[:split], style="brand")
|
||||
text.append(raw[split:], style="muted")
|
||||
else:
|
||||
text.append(raw, style="muted")
|
||||
text.append(raw[:split], style="brand")
|
||||
text.append(raw[split:], style="muted")
|
||||
if ellipsis:
|
||||
text.append("…", style="brand" if len(raw) < split else "muted")
|
||||
text.append(" " * pad)
|
||||
return text
|
||||
|
||||
|
||||
@@ -627,7 +639,8 @@ def nodes_table(
|
||||
) -> Group:
|
||||
"""List this node and its known contacts with recency, packets, type, key, and legend.
|
||||
|
||||
Our own node is the first row (``★``, name in accent); contacts follow in ``sort`` order.
|
||||
Our own node is the first row (``★``, name in the white ``you`` style); contacts follow
|
||||
in ``sort`` order.
|
||||
A per-type glyph marks each node in the app's shared colours, the name is coloured by how
|
||||
recently it was last heard (brighter = fresher), and the full key is shown with its
|
||||
path-hash prefix lit — chopped with an ellipsis only when the terminal is too narrow.
|
||||
@@ -676,7 +689,8 @@ def nodes_table(
|
||||
unknown = Text("?", style="muted")
|
||||
table.add_row(
|
||||
Text(_SELF[0], style=_SELF[1]),
|
||||
Text.assemble((self_name, "accent"), (" (you)", "muted")),
|
||||
# Our own name is the app-wide pure-white "you" style, never a palette hue.
|
||||
Text.assemble((self_name, "you"), (" (you)", "muted")),
|
||||
Text("—", style="faint"),
|
||||
Text("—", style="faint"),
|
||||
highlighted_hash(self_key, prefix_bytes) if self_key else unknown,
|
||||
|
||||
+2
-2
@@ -670,7 +670,7 @@ def test_at_mention_renders_as_name_in_sender_hue() -> None:
|
||||
"""An ``@[Name]`` token renders as a bare ``@Name`` colored in that sender's hue."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from meshterm.ui.chat import _SENDER_COLORS
|
||||
from meshterm.ui.theme import NAME_COLORS as _SENDER_COLORS
|
||||
|
||||
base = datetime(2026, 7, 5, 14, 24, tzinfo=timezone.utc)
|
||||
conv = Conversation(label="#public", is_channel=True, channel_idx=0)
|
||||
@@ -908,7 +908,7 @@ def test_channel_self_style_keyed_on_concept_not_label() -> None:
|
||||
A remote sender who happens to be named 'you' must still get a palette hue, never the
|
||||
white style reserved for us.
|
||||
"""
|
||||
from meshterm.ui.chat import _SENDER_COLORS
|
||||
from meshterm.ui.theme import NAME_COLORS as _SENDER_COLORS
|
||||
|
||||
screen = _channel_screen([])
|
||||
assert screen._sender_style("you", is_self=True) == "you" # us → white
|
||||
|
||||
Reference in new issue
Block a user