Node colour: the hue now rides the key, and heat moves to the age
The per-node palette hue was keyed on the display name's characters; it is now derived from the node's key — the first byte picks the colour, so a 2-hex path hop, the stored 12-hex id, and the full 64-hex public key all agree, and a rename keeps its colour. name_style grows a key parameter (theme.node_style is the raw derivation); the name-character fallback remains only for genuinely keyless senders (a channel message's inline name). Keys are threaded through the path widget, the route graph's labels, the atlas lists, the trophy case's far point and area pins, and the packet viewer's node lane. In the Nodes screen and the Time Machine (the shared node list, the static CLI table, and the mesh page's arrivals), names drop their recency-heat colouring for the node's palette hue; the heat now glows on the HEARD/first-heard age lane instead, where the recency actually lives. A nameless node's "unknown" placeholder stays muted — colour marks a name, and the hash lane already carries the identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b40caf6891
commit
79d00defdd
8 files changed
+113
-63
No files matched your search
@@ -22,8 +22,9 @@ them instead of hand-rolling:
|
||||
- `ui/menus.py` — `back_rows`, `exit_rows`, `menu_rows`, `lane_row`, `section_heading`,
|
||||
`confirm_discard`, `fit_cells`.
|
||||
- `ui/widgets.py` — `highlighted_hash` (THE hash widget), `format_ago` (prose ages),
|
||||
`_format_age` (column ages), `channel_glyph`, `_NODE_GLYPHS`, node heat colouring.
|
||||
- `ui/theme.py` — `name_style` (per-name hues), `snr_style`, the `you` white.
|
||||
`_format_age` (column ages), `channel_glyph`, `_NODE_GLYPHS`, heard-age heat colouring.
|
||||
- `ui/theme.py` — `name_style`/`node_style` (per-node hues, hash-derived), `snr_style`,
|
||||
the `you` white.
|
||||
|
||||
### Lexicon — one term per concept
|
||||
|
||||
@@ -104,9 +105,12 @@ Relative ages: `format_ago` for prose ("now", "5m ago", "never" — never "now a
|
||||
|
||||
### Colour
|
||||
|
||||
- Node names are always coloured: `name_style(name)` palette hue; our own node is the
|
||||
pure-white `you` style; a context colouring (recency heat, chart quality) may win.
|
||||
Bare hashes stay muted — colour is the "this is a name" signal.
|
||||
- Node names are always coloured: `name_style(name, key)` palette hue, derived from the
|
||||
node's key (its first byte — any known prefix agrees) so a rename keeps the colour;
|
||||
the name's characters seed it only when no key exists (a channel sender). Our own node
|
||||
is the pure-white `you` style; a context colouring (chart quality) may still win.
|
||||
Recency heat colours heard/first-heard ages, never names. Bare hashes stay muted —
|
||||
colour is the "this is a name" signal.
|
||||
- SNR always through `snr_style`; timelines oldest→now left-to-right, grey baseline = 0,
|
||||
drawn via `ui/braillechart` only.
|
||||
|
||||
|
||||
@@ -794,13 +794,13 @@ class AtlasScreen(Screen):
|
||||
return row
|
||||
|
||||
def _list_name_style(self, node: str) -> str:
|
||||
"""The list's name colour: the app-wide palette hue, us in pure white."""
|
||||
"""The list's name colour: the app-wide palette hue (hash-derived), us in pure white."""
|
||||
if node == self._topo.self_id:
|
||||
return "you"
|
||||
label = self._label(node)
|
||||
if label == node[:8]: # a bare hash is not a name — colour is the name signal
|
||||
return "muted"
|
||||
return name_style(label)
|
||||
return name_style(label, node)
|
||||
|
||||
def _empty_state(self, width: int) -> list[str]:
|
||||
"""A friendly explanation while the evidence graph is still empty."""
|
||||
|
||||
+24
-18
@@ -21,6 +21,7 @@ from rich.text import Text
|
||||
|
||||
from .map_render import _SELF
|
||||
from .menus import fit_cells
|
||||
from .theme import name_style
|
||||
from .tui.select import Choice, SelectScreen, Separator
|
||||
from .widgets import (
|
||||
_DEFAULT_GLYPH,
|
||||
@@ -79,14 +80,15 @@ class NodeRow:
|
||||
value: What the row's :class:`~meshterm.ui.tui.select.Choice` resolves with —
|
||||
also the identity a re-sort uses to keep the highlight on its node, so it
|
||||
should be unique across the list.
|
||||
name: The display name; ``None`` renders the heat-coloured ``unknown`` (an
|
||||
own-node row falls back to a bare ``you`` instead).
|
||||
name: The display name, drawn in the node's hash-derived palette hue; ``None``
|
||||
renders a muted ``unknown`` (an own-node row falls back to a bare ``you``
|
||||
instead).
|
||||
key: The hex the hash lane shows — as full a key as the caller could resolve;
|
||||
empty renders a muted ``?``.
|
||||
also the seed of the name's palette hue. Empty renders a muted ``?``.
|
||||
node_type: The node's type for the leading glyph (``None`` = the plain-node
|
||||
``●``; ignored on the own-node row, which always leads with the yellow ``★``).
|
||||
last_seen: When the node was last heard (aware UTC) — colours the name by recency
|
||||
heat and fills the heard lane; ``None`` reads ``never`` and the cold style.
|
||||
last_seen: When the node was last heard (aware UTC) — fills the heard lane,
|
||||
coloured by recency heat; ``None`` reads ``never`` in the cold style.
|
||||
count: The packet tally; ``None`` renders a faint ``—`` (never overheard).
|
||||
you: Whether this is our own node: the ``★`` marker, the pure-white ``you`` name
|
||||
style with a muted ``(you)`` tag, faint ``—`` heard/packet lanes (we never
|
||||
@@ -182,14 +184,15 @@ def _lane(row: NodeRow, name_w: int, prefix_bytes: int, hash_w: int) -> Text:
|
||||
"""One node as fixed, colour-coded lanes under :func:`_header`'s columns.
|
||||
|
||||
The type glyph leads (the app's shared marker palette), so the mark reads ``▲`` for a
|
||||
repeater, ``■`` for a room, ``◉`` for a sensor, ``●`` for a plain node. The name is
|
||||
coloured by recency heat, ``unknown`` included, so a freshly heard mystery node still
|
||||
reads hot, and takes the flexing name lane (``name_w`` cells). Last-heard age and
|
||||
packet count follow, right-aligned under their headers — a node never overheard reads
|
||||
a faint ``—`` in the packets lane; the hash closes the row in the shared hash widget,
|
||||
its path-hash prefix lit at the device's routing width, or a muted ``?`` when no key
|
||||
is known at all. An own-node row (:attr:`NodeRow.you`) takes its own drawing —
|
||||
see :func:`_you_lane`.
|
||||
repeater, ``■`` for a room, ``◉`` for a sensor, ``●`` for a plain node. The name takes
|
||||
the node's hash-derived palette hue (a nameless node's ``unknown`` placeholder stays
|
||||
muted — colour marks a name, and the hash lane already carries the identity) and the
|
||||
flexing name lane (``name_w`` cells). The last-heard age follows, glowing with recency
|
||||
heat (brighter = fresher) so a freshly heard node still reads hot at a glance; then the
|
||||
packet count, right-aligned — a node never overheard reads a faint ``—``; the hash
|
||||
closes the row in the shared hash widget, its path-hash prefix lit at the device's
|
||||
routing width, or a muted ``?`` when no key is known at all. An own-node row
|
||||
(:attr:`NodeRow.you`) takes its own drawing — see :func:`_you_lane`.
|
||||
|
||||
Args:
|
||||
row: The node's lane data.
|
||||
@@ -205,9 +208,12 @@ def _lane(row: NodeRow, name_w: int, prefix_bytes: int, hash_w: int) -> Text:
|
||||
text = Text(no_wrap=True, overflow="ellipsis")
|
||||
text.append(glyph, style=glyph_style)
|
||||
text.append(" ")
|
||||
text.append(fit_cells(row.name or "unknown", name_w), style=_recency_style(secs))
|
||||
text.append(
|
||||
fit_cells(row.name or "unknown", name_w),
|
||||
style=name_style(row.name, row.key) if row.name else "muted",
|
||||
)
|
||||
text.append(" ")
|
||||
text.append(f"{_format_age(secs):>5}", style="muted")
|
||||
text.append(f"{_format_age(secs):>5}", style=_recency_style(secs))
|
||||
text.append(" ")
|
||||
if row.count is None:
|
||||
text.append(f"{'—':>5}", style="faint")
|
||||
@@ -252,9 +258,9 @@ def _ordered(rows: list[NodeRow], sort: NodesSort) -> list[NodeRow]:
|
||||
class NodeListScreen(SelectScreen):
|
||||
"""A full-screen, sortable, filterable node list in the shared lane layout.
|
||||
|
||||
The rows run in aligned lanes — name (coloured by recency heat), last-heard age,
|
||||
packet count, and key hash — own-node rows first, then the rest in the active sort
|
||||
order. The lanes anchor to the left: the name lane is sized to its widest name (not
|
||||
The rows run in aligned lanes — name (in the node's hash-derived palette hue),
|
||||
last-heard age (coloured by recency heat), packet count, and key hash — own-node rows
|
||||
first, then the rest in the active sort order. The lanes anchor to the left: the name lane is sized to its widest name (not
|
||||
the terminal), so the columns stay put as the window widens and the freed width flows
|
||||
to the hash lane, which shows each key as fully as it fits (see :meth:`_lane_widths`).
|
||||
It is a full-screen list, not a floating popup.
|
||||
|
||||
@@ -221,7 +221,7 @@ def node_label(
|
||||
return (entry.node or "?", "muted")
|
||||
if self_name and name == self_name:
|
||||
return (name, "you")
|
||||
return (name, name_style(name))
|
||||
return (name, name_style(name, entry.node))
|
||||
|
||||
|
||||
class PacketViewer(Screen):
|
||||
|
||||
@@ -132,6 +132,7 @@ class RecordDialog(Screen):
|
||||
device_label: str,
|
||||
device_hash: Optional[str],
|
||||
far_label: Optional[str] = None,
|
||||
far_id: Optional[str] = None,
|
||||
shape: Optional[Sequence[WalkVertex]] = None,
|
||||
reliability: Optional[tuple[float, int, int]] = None,
|
||||
type_of: Optional[TypeOf] = None,
|
||||
@@ -147,6 +148,8 @@ class RecordDialog(Screen):
|
||||
device_hash: Our public key, annotated at the record's width.
|
||||
far_label: The farthest node's name, shown beside its distance; ``None`` when
|
||||
no positioned hop was named.
|
||||
far_id: The farthest node's id, seeding the name's hash-derived hue; ``None``
|
||||
leaves the label on the name-derived fallback.
|
||||
shape: The walk's positioned circuit, projected for the area drawing; ``None``
|
||||
below the three points a polygon needs.
|
||||
reliability: ``(rate, successes, total)`` of traces to the walk's far node, or
|
||||
@@ -163,6 +166,7 @@ class RecordDialog(Screen):
|
||||
self._device_label = device_label
|
||||
self._device_hash = device_hash
|
||||
self._far_label = far_label
|
||||
self._far_id = far_id
|
||||
self._shape = list(shape) if shape else None
|
||||
self._reliability = reliability
|
||||
self._type_of = type_of
|
||||
@@ -284,7 +288,7 @@ class RecordDialog(Screen):
|
||||
value = Text(f"{far:.1f} km")
|
||||
if self._far_label:
|
||||
value.append(" ")
|
||||
value.append(self._far_label, style=name_style(self._far_label))
|
||||
value.append(self._far_label, style=name_style(self._far_label, self._far_id))
|
||||
lanes.append(self._lane("far point", value))
|
||||
area = stats.get("area_km2")
|
||||
if area is not None:
|
||||
@@ -533,7 +537,7 @@ async def open_records(ctx: "AppContext") -> dict:
|
||||
# the neutral shape tone so it never masquerades as a coloured name.
|
||||
name = resolve(node_id)
|
||||
hop_glyph = node_marker(ntype)[0]
|
||||
hop_color = name_rgb(name) if name and name != node_id else _AREA_TONE
|
||||
hop_color = name_rgb(name, node_id) if name and name != node_id else _AREA_TONE
|
||||
verts.append(WalkVertex(east, north, hop_glyph, hop_color, False))
|
||||
dist = haversine_km(self_pos[0], self_pos[1], pos[0], pos[1])
|
||||
if dist > far_dist:
|
||||
@@ -706,7 +710,7 @@ async def open_records(ctx: "AppContext") -> dict:
|
||||
action = await session.run_screen(RecordDialog(
|
||||
record, category, rank,
|
||||
resolve=resolve, device_label=device_label, device_hash=device_hash,
|
||||
far_label=far_label, shape=shape,
|
||||
far_label=far_label, far_id=far_id, shape=shape,
|
||||
reliability=walk_reliability(far_id, far_label),
|
||||
type_of=lambda node_id: node_geo(node_id)[1],
|
||||
))
|
||||
|
||||
+42
-12
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
@@ -128,13 +130,14 @@ 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.
|
||||
#: Palette of distinct, dark-theme-friendly colors that give each node 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, keyed on the node's key (see :func:`node_style`) so
|
||||
#: the colour is the node's identity, surviving renames and colouring every surface that
|
||||
#: knows any prefix of the key identically — and our own node is always the pure-white
|
||||
#: ``you`` style instead, so "us" never blends into the crowd. Shared by the node lists,
|
||||
#: 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
|
||||
@@ -147,16 +150,43 @@ NAME_COLORS = (
|
||||
)
|
||||
|
||||
|
||||
def name_style(name: str) -> str:
|
||||
"""The stable per-name colour a node or sender is drawn in, keyed on its characters.
|
||||
def node_style(key: str) -> str:
|
||||
"""The stable palette hue a node's *key* selects — the hash-derived node colour.
|
||||
|
||||
Only the first byte picks the colour, so any prefix of the key a surface happens to
|
||||
hold — a 2-hex path hop, the stored 12-hex id, the full 64-hex public key — lands on
|
||||
the same hue: one node, one colour, however it was learned.
|
||||
|
||||
Args:
|
||||
name: The display name (not a hash — hashes stay muted).
|
||||
key: The node's key/hash as hex (any length ≥ 1 byte, ``0x``/mixed-case tolerated).
|
||||
|
||||
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.
|
||||
A style string from :data:`NAME_COLORS`.
|
||||
"""
|
||||
raw = key.lower().removeprefix("0x")
|
||||
try:
|
||||
return NAME_COLORS[int(raw[:2], 16) % len(NAME_COLORS)]
|
||||
except ValueError: # not hex — fall back to the character sum so *something* stable shows
|
||||
return NAME_COLORS[sum(map(ord, raw)) % len(NAME_COLORS)]
|
||||
|
||||
|
||||
def name_style(name: str, key: Optional[str] = None) -> str:
|
||||
"""The stable colour a node or sender name is drawn in — keyed on the node's key.
|
||||
|
||||
Args:
|
||||
name: The display name.
|
||||
key: Any known prefix of the node's key/hash; when given, the hue is
|
||||
:func:`node_style`'s — hash-derived, so a rename keeps the colour and every
|
||||
surface that knows the key agrees. ``None``/empty marks a genuinely keyless
|
||||
sender (a channel message's inline name), coloured by the name's characters
|
||||
as the stable fallback.
|
||||
|
||||
Returns:
|
||||
A style string from :data:`NAME_COLORS`; the same node always maps to the
|
||||
same hue, so it keeps its colour across screens and sessions.
|
||||
"""
|
||||
if key:
|
||||
return node_style(key)
|
||||
return NAME_COLORS[sum(map(ord, name)) % len(NAME_COLORS)]
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from ..core.models import utcnow
|
||||
from .braillechart import _TICK_GAP, GAP, axis_chart, chart_span, timeline_rows
|
||||
from .menus import fit_cells, section_heading
|
||||
from .nodelist import SORT_COLUMNS, SORT_OPENS_ASCENDING, NodeListScreen, NodeRow
|
||||
from .theme import snr_style
|
||||
from .theme import name_style, snr_style
|
||||
from .tui.render import render_lines
|
||||
from .tui.screen import CANCEL, Screen
|
||||
from .tui.select import Choice, Separator
|
||||
@@ -896,8 +896,9 @@ def _mesh_sections(
|
||||
if not arrivals:
|
||||
out.append(Text("none in this window", style="muted"))
|
||||
else:
|
||||
# Aligned lanes under column labels, the picker's presentation: the name
|
||||
# coloured by heat ("unknown" included), the hash lit at the routing width.
|
||||
# Aligned lanes under column labels, the picker's presentation: the name in
|
||||
# the node's hash-derived hue (a nameless arrival's "unknown" stays muted),
|
||||
# the hash lit at the routing width, the age glowing with recency heat.
|
||||
# The FIRST HEARD header carries what used to be repeated on every row.
|
||||
# A nameless arrival first asks the resolver (the device may know the node
|
||||
# as a contact even though its stored observations never carried a name).
|
||||
@@ -921,12 +922,15 @@ def _mesh_sections(
|
||||
for node, name, first in listed:
|
||||
secs = _age_seconds(first)
|
||||
line = Text(" ", no_wrap=True, overflow="ellipsis")
|
||||
line.append(fit_cells(name or "unknown", name_w), style=_recency_style(secs))
|
||||
line.append(
|
||||
fit_cells(name or "unknown", name_w),
|
||||
style=name_style(name, node) if name else "muted",
|
||||
)
|
||||
line.append(" ")
|
||||
line.append_text(highlighted_hash(node, prefix_bytes, width=_PICK_HASH_W))
|
||||
line.append(" ")
|
||||
line.append(_when_label(first))
|
||||
line.append(f" ({format_ago(secs)})", style="muted")
|
||||
line.append(f" ({format_ago(secs)})", style=_recency_style(secs))
|
||||
out.append(line)
|
||||
|
||||
all_days = ctx.repo.daily_activity()
|
||||
|
||||
+19
-17
@@ -350,7 +350,7 @@ def _path_node(
|
||||
elif self_name and named == self_name:
|
||||
style = "you"
|
||||
else:
|
||||
style = name_style(named)
|
||||
style = name_style(named, hop)
|
||||
text = Text(named, style=style)
|
||||
if show_hash:
|
||||
text.append(f" ({_shorten_hash(hop, hash_bytes)})", style=note_style)
|
||||
@@ -437,19 +437,20 @@ def _shorten_hash(value: str, hash_bytes: Optional[int]) -> str:
|
||||
_SELF_RGB: RGB = (255, 255, 255)
|
||||
|
||||
|
||||
def _name_rgb(name: str) -> RGB:
|
||||
"""The RGB of a name's stable palette hue (``theme.name_style`` minus its bold)."""
|
||||
return parse_hex(name_style(name).split()[-1])
|
||||
def _name_rgb(name: str, key: Optional[str] = None) -> RGB:
|
||||
"""The RGB of a node's stable palette hue (``theme.name_style`` minus its bold)."""
|
||||
return parse_hex(name_style(name, key).split()[-1])
|
||||
|
||||
|
||||
def name_rgb(name: str) -> RGB:
|
||||
"""The truecolour of a node name's stable palette hue, for a braille canvas.
|
||||
def name_rgb(name: str, key: Optional[str] = None) -> RGB:
|
||||
"""The truecolour of a node's stable palette hue, for a braille canvas.
|
||||
|
||||
The public face of :func:`_name_rgb` — the same hue :func:`~meshterm.ui.theme.name_style`
|
||||
paints a name in, as an ``(r, g, b)`` tuple a raster can plot. A caller colouring node
|
||||
markers by their mesh name (the trophy case's area drawing) mints them through here.
|
||||
paints a name in (hash-derived when ``key`` is given), as an ``(r, g, b)`` tuple a
|
||||
raster can plot. A caller colouring node markers by their mesh identity (the trophy
|
||||
case's area drawing) mints them through here.
|
||||
"""
|
||||
return _name_rgb(name)
|
||||
return _name_rgb(name, key)
|
||||
|
||||
|
||||
#: Maps a relay hash to its node type (see the ``NODE_TYPE_*`` constants), or ``None`` when
|
||||
@@ -523,7 +524,7 @@ def route_graph_style(
|
||||
return _name_rgb(source) if source else parse_hex(_UNKNOWN[1])
|
||||
named = resolve(node)
|
||||
if named and named != node:
|
||||
return _name_rgb(named)
|
||||
return _name_rgb(named, node)
|
||||
return parse_hex(glyph_of(node)[1])
|
||||
|
||||
return glyph_of, label_of, label_rgb_of
|
||||
@@ -682,7 +683,7 @@ def self_marker() -> tuple[str, RGB]:
|
||||
"""Our own node's map marker — the yellow ``★`` — as a ``(glyph, rgb)`` pair."""
|
||||
return _SELF[0], parse_hex(_SELF[1])
|
||||
|
||||
# A heat-map gradient for a contact's name, hottest (most recently heard) to coldest: white
|
||||
# A heat-map gradient for a node's heard age, hottest (most recently heard) to coldest: white
|
||||
# → yellow → orange → red → grey. Each stop pairs an age anchor (log10 of seconds since heard)
|
||||
# with an RGB colour; :func:`_recency_style` interpolates continuously between them, so the
|
||||
# colour glides with recency rather than snapping between a handful of discrete shades.
|
||||
@@ -733,7 +734,7 @@ def format_ago(secs: Optional[float]) -> str:
|
||||
|
||||
|
||||
def _recency_style(secs: Optional[float]) -> str:
|
||||
"""The heat-map name colour for a contact last heard ``secs`` ago (hotter = more recent).
|
||||
"""The heat-map colour for a node's heard age of ``secs`` (hotter = more recent).
|
||||
|
||||
Interpolates the RGB channels between the two :data:`_HEAT_STOPS` bracketing ``secs`` (in
|
||||
log-age space), clamping to white below the first stop and cold slate above the last.
|
||||
@@ -901,9 +902,10 @@ def nodes_table(
|
||||
|
||||
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.
|
||||
A per-type glyph marks each node in the app's shared colours, the name takes the node's
|
||||
hash-derived palette hue, the heard age glows with recency heat (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.
|
||||
|
||||
Args:
|
||||
self_name: This node's advertised name.
|
||||
@@ -962,8 +964,8 @@ def nodes_table(
|
||||
pkts = _contact_pkts(c, counts)
|
||||
table.add_row(
|
||||
Text(glyph, style=glyph_style),
|
||||
Text(c.name, style=_recency_style(secs)),
|
||||
Text(_format_age(secs), style="muted"),
|
||||
Text(c.name, style=name_style(c.name, c.public_key or c.key_prefix)),
|
||||
Text(_format_age(secs), style=_recency_style(secs)),
|
||||
Text(str(pkts), style="muted") if pkts else Text("—", style="faint"),
|
||||
highlighted_hash(c.public_key, prefix_bytes) if c.public_key else unknown,
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user