Theme: the cursor row is white, not the wordmark's teal

The row `❯` points at borrowed `brand` — so the app's identity ink and
"the row you're on" were the same hue, and worse, teal/cyan is itself a
node hue: a cyan-keyed node's name vanished into its own highlight.

Give the highlight its own name. `cursor` is white on both platforms
(slot 15 on the VT, the top of the bright bank, so the row's bold cannot
promote it further and every dim-slot `not bold` pin inside a highlighted
row goes on meaning what it meant). White sits outside the per-node
spectrum, so it can never collide with an identity; it does share ink
with `you`, which is the accepted cost — spans that set their own colour
keep it over the base, so only unstyled text goes white, and only on the
row already under the reader's eye.

Every surface that draws its own cursor row follows: the select list and
its reorder mode, the feed, chat, routes and actions on the node page,
the composer, records, trace, tx, the walk, the paths screen, the prompt's
suggestions. The composer's insertion-slot chip takes the same white — its
docstring already tied it to "the colour the list cursor below it wears",
and the chip ink was already picked to stay readable on white.

Reverse-video chips keep `selected`: on the VT a reversed white lands on
the light grey the F-key lane already owns, so a white default button
would stop reading as the committing verb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-23 23:51:37 -04:00
1 parent 1b6a5cc1e7
commit 602bceea3b
18 files changed
+75 -49

No files matched your search

+9
View File
@@ -180,6 +180,15 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
characters. `node.unknown` is deliberately not `muted`: muted is chrome and may sit a
step darker, while an unidentified node is content you can still act on. Our own node
is the pure-white `you` style; a context colouring (chart quality) may still win.
- The cursor row — the row `❯` points at, in a select list or in a screen drawing its own
rows — wears `cursor` (white), never `brand`: the wordmark's teal is the app's identity,
not a selection, and teal/cyan is itself a node hue, so a cyan-keyed node used to vanish
into its own highlight. White is outside the node spectrum, so it can never collide with
an identity. It rides *under* the row's spans, so every lane keeps its own colour and
only the gaps take the tint; the path composer's insertion-slot chip takes the same white
so the row you pick and the slot it lands in read as one gesture. Reverse-video chips
(dialog buttons, the editor cursor) keep their own `selected` fill — on the VT a reversed
white lands on the light grey the F-key lane already owns.
Recency heat colours heard/first-heard ages, never names — seven steps on plain human
boundaries (`widgets._HEAT_STEPS`), each `heat.*` style named for how old the node it
colours is: white under 5 minutes, then yellow, light red, brown, red, light grey, and
+5 -5
View File
@@ -468,12 +468,12 @@ class ChatScreen(Screen):
sender — otherwise a run of same-sender messages would appear to share one time.
A long body wraps with a hanging indent so continuation lines align under the body
rather than under the timestamp gutter. When ``selected``, the line is marked as the
reply target (matching the select screen's ``❯`` pointer and brand highlight).
reply target (matching the select screen's ``❯`` pointer and cursor highlight).
"""
stamp = message.created_at.astimezone()
prefix = Text()
prefix.append("❯ " if selected else " ", style="brand" if selected else None)
prefix.append(f"{stamp:%H:%M} ", style="brand" if selected else "muted")
prefix.append("❯ " if selected else " ", style="cursor" if selected else None)
prefix.append(f"{stamp:%H:%M} ", style="cursor" if selected else "muted")
body_text = self._body_text(body, message, selected=selected)
return render_hanging(prefix, body_text, width, indent=prefix.cell_len)
@@ -499,10 +499,10 @@ class ChatScreen(Screen):
The reply flow primes the compose line with an ``@[Name]`` token (see
:meth:`_begin_reply`); here it reads back as a bare ``@Name`` colored in that
sender's stable hue, so a mention is visually tied to the person it names. Text
around the mentions keeps the line's base style (brand when the message is the
around the mentions keeps the line's base style (``cursor`` when the message is the
picked reply target, otherwise unstyled).
"""
base = "brand" if selected else None
base = "cursor" if selected else None
text = Text()
pos = 0
for match in _MENTION.finditer(body):
+8 -8
View File
@@ -478,13 +478,13 @@ class LiveFeedScreen(Screen):
The very rows :func:`~meshterm.ui.menus.back_rows` gives a select list, drawn here
by hand because the feed is a rendered screen rather than a list of choices — same
blank line, same bare word, no arrow and no icon, and the same ``❯`` cursor over a
brand row when it is the stop the cursor rests on.
highlighted row when it is the stop the cursor rests on.
"""
selected = self._selected is None
row = Text(_CURSOR if selected else " ", style="brand" if selected else "")
row = Text(_CURSOR if selected else " ", style="cursor" if selected else "")
row.append("Back")
if selected:
row.style = "brand"
row.style = "cursor"
return ["", render_to_ansi(row, width, no_wrap=True)]
def _heading(self) -> Text:
@@ -563,14 +563,14 @@ class LiveFeedScreen(Screen):
72-column screen precisely because it doesn't try to, and the route is drawn
properly one keypress away, in the packet viewer.
The highlight is the app's own: the ``❯`` pointer and a brand base style under the
The highlight is the app's own: the ``❯`` pointer and a ``cursor`` base under the
whole row, exactly as every select list draws its cursor. The lanes keep their own
colours over it — the SNR its quality hue, a name its palette hue — because those
colours are the content; the pointer and the base tint are what say "this row".
The one row that can wear a *different* pointer is the newest, which takes ``^``
while the cursor is pinned to the top of the stream rather than to the packet
currently sitting there (:data:`_PIN_CURSOR`, and the module docstring). Same lane,
same brand tint, same one cell — only the mark differs, because only the meaning
same cursor tint, same one cell — only the mark differs, because only the meaning
does.
A row that runs past the right edge — a terminal narrower than the lanes need —
@@ -582,7 +582,7 @@ class LiveFeedScreen(Screen):
"""
row = Text(no_wrap=True, overflow="ellipsis")
cursor = _PIN_CURSOR if self._pinned else _CURSOR
row.append(cursor if selected else " ", style="brand" if selected else "")
row.append(cursor if selected else " ", style="cursor" if selected else "")
avail = max(1, width - 2)
body = Text(no_wrap=True, overflow="ellipsis")
@@ -626,9 +626,9 @@ class LiveFeedScreen(Screen):
body = crop_cells(body, self._hshift, avail)
row.append_text(body)
if selected:
# The brand tint rides *under* the row's spans (the select list's own
# The cursor tint rides *under* the row's spans (the select list's own
# convention), so the lanes keep their colours and only the gaps take it.
row.style = "brand"
row.style = "cursor"
return row
def _feed_subject(self, entry: PacketEntry) -> Text:
+2 -2
View File
@@ -319,7 +319,7 @@ class MessagePathsScreen(Screen):
left_more = 1 if shift > 0 else 0
right_more = 1 if shift + avail < full.cell_len else 0
inner = max(1, avail - left_more - right_more)
line = Text("❯ ", style="brand", no_wrap=True)
line = Text("❯ ", style="cursor", no_wrap=True)
# The marks stand for the cell just outside the window on their own side, so each
# takes its colour from the nearest cell still drawn — the chip the reader can see
# being the one that visibly runs on.
@@ -328,7 +328,7 @@ class MessagePathsScreen(Screen):
line.append_text(crop_cells(full, shift + left_more, inner))
if right_more:
line.append_text(cut_mark(full, shift + left_more + inner - 1, ELIDE_TAIL))
line.style = "brand"
line.style = "cursor"
return render_to_ansi(line, width, no_wrap=True)
# -- the graph --
+5 -5
View File
@@ -975,7 +975,7 @@ class NodeDetailScreen(Screen):
The route keeps its per-node colours even when selected (the pointer, and the white
line in the graph above, carry the selection) — unlike the plain action rows, which
go fully brand, since here the colour *is* the content. The trailing ``…`` is the
take the highlight, since here the colour *is* the content. The trailing ``…`` is the
app-wide opens-further-prompts mark: Enter on the row arms a trace on it. It rides
*outside* the lane (:func:`~meshterm.ui.pathline.with_action_mark`) — chrome buys no
cells off the route, so a chain that fits stays whole and closes on its rounded cap
@@ -999,7 +999,7 @@ class NodeDetailScreen(Screen):
"""
indent = _ROUTE_INDENT
avail = max(1, width - indent)
pointer = Text("❯ " if selected else " ", style="brand" if selected else "")
pointer = Text("❯ " if selected else " ", style="cursor" if selected else "")
marked = route.path
if selected and self._hshift:
total = cell_len(marked.plain)
@@ -1042,8 +1042,8 @@ class NodeDetailScreen(Screen):
return lines
def _action_line(self, action: _Action, selected: bool, width: int) -> str:
"""One action row: ``❯`` + icon + label, the whole row brand when it is the cursor's."""
text = Text("❯ " if selected else " ", style="brand" if selected else "")
"""One action row: ``❯`` + icon + label, the whole row lit when it is the cursor's."""
text = Text("❯ " if selected else " ", style="cursor" if selected else "")
# The icon lane is decoration a platform may drop whole (command_icon); the label
# is what names the action, so an emptied lane simply gives it the cells.
mark = command_icon(action.glyph) if action.glyph else ""
@@ -1051,7 +1051,7 @@ class NodeDetailScreen(Screen):
text.append(f"{mark} ", style=action.glyph_style)
text.append(action.label)
if selected:
text.style = "brand"
text.style = "cursor"
text.no_wrap = True
text.truncate(width, overflow="ellipsis")
return render_to_ansi(text, width)
+4 -4
View File
@@ -26,7 +26,7 @@ Interaction, following the reorder screen's cursor-over-rows-and-actions pattern
* ↑/↓ move over suggestions and the action rows; Enter on a suggestion inserts it
at the route's insertion cursor.
* ←/→ slide that insertion cursor along the editable leg — it rides *in* the route as
a hop of its own, the brand-accent ``+`` slot the next chosen node drops into — so a
a hop of its own, the ``cursor``-white ``+`` slot the next chosen node drops into — so a
hop can be spliced in (at the slot) or removed (⌫, to its left) anywhere in the
route, not just at the end. It opens at the end of the composed leg, where inserting
is appending — the classic flow unchanged. The suggestion list follows: it always
@@ -496,7 +496,7 @@ class PathComposerScreen(Screen):
The insertion cursor is *in* the route, not between two of its hops: the
``+`` slot (:data:`~meshterm.ui.pathline.CURSOR_GLYPH`) sits where the next
chosen node will, in the brand accent the row cursor below it wears. Drawing an
chosen node will, in the ``cursor`` white the row cursor below it wears. Drawing an
insertion point as a position rather than as a gap is what lets the preview stay
the picture the trace window paints — chips and all, rather than falling back to
arrows for the sake of a seam to sit in — and lets a wrapped route carry the
@@ -610,10 +610,10 @@ class PathComposerScreen(Screen):
if kind == "action" and (i == 0 or rows[i - 1][0] != "action"):
entries.append((None, "")) # a spacer sets the action group apart
is_sel = i == self._index
text = Text("❯ " if is_sel else " ", style="brand" if is_sel else "")
text = Text("❯ " if is_sel else " ", style="cursor" if is_sel else "")
text.append_text(self._row_text(kind, payload))
if is_sel:
text.style = "brand"
text.style = "cursor"
text.no_wrap = True
# A suggestion row names a node in the route's own vocabulary, so it is cut
# like one: a chip that runs off the dialog cracks, prose keeps the ellipsis.
+5 -4
View File
@@ -187,7 +187,7 @@ class PathHop:
allowed) or theme name — for context colourings that outrank identity.
Plain mode uses it as the label style; powerline as the chip fill.
cursor: This hop is an editor's insertion slot rather than a node — the
:data:`CURSOR_GLYPH` in the app's brand accent, the same colour the list
:data:`CURSOR_GLYPH` in the app's ``cursor`` white, the same colour the list
cursor (``❯``) below it wears, so the two halves of one gesture (the row
you pick, the slot it lands in) read as one thing.
gap: This hop is a mark standing *between* two nodes rather than being one — the
@@ -894,15 +894,16 @@ class PathLine:
"""A chip's fill: cursor accent, override, dim slate, our grey, hue, keyless grey.
The insertion slot outranks everything — it is the one chip that isn't a node,
and it wears the brand accent so it reads as chrome among identities rather than
as a hop with an unlucky hue. The fade comes next, outranking identity including
and it wears the ``cursor`` white so it reads as chrome among identities rather
than as a hop with an unlucky hue (white sits outside the node spectrum, and the
chip ink is already picked to stay readable on it). The fade comes next, outranking identity including
our own: a dimmed hop is one nobody composed (an automatic landing back home, a
mirrored return leg), and our end must recede with the rest of that automatic
half rather than keep its yellow among the greys. Plain mode says the same thing
by fading the name.
"""
if hop.cursor:
return _style_hex("brand") or _KEYLESS_BG
return _style_hex("cursor") or _KEYLESS_BG
if hop.style:
resolved = _style_hex(hop.style)
if resolved:
+2 -2
View File
@@ -471,7 +471,7 @@ class RecordDialog(Screen):
if key == "back":
lines.append("")
selected = i == self._index
row = Text("❯ " if selected else " ", style="brand" if selected else "")
row = Text("❯ " if selected else " ", style="cursor" if selected else "")
if key == "trace":
row.append_text(marked_label(
"👣", "Trace this path — reopen in Trace path", "accent"
@@ -481,7 +481,7 @@ class RecordDialog(Screen):
else:
row.append("Back")
if selected:
row.style = "brand"
row.style = "cursor"
self._cursor = len(lines)
row.no_wrap = True
row.truncate(width, overflow="ellipsis")
+16
View File
@@ -32,6 +32,18 @@ MESH_THEME = Theme(
# theme name with an attribute (e.g. "reverse brand" renders as plain text), so the
# reverse is baked into the definition here rather than tacked on at the call site.
"selected": "reverse bold #5eead4",
# THE cursor row — the one row the ❯ points at, in every select list and in every
# screen that draws its own rows (feed, routes, records, walk, composer). White,
# and deliberately *not* ``brand``: the highlight used to borrow the wordmark's
# teal, which put the app's identity ink and "the row you're on" on the same hue,
# and worse, teal/cyan is itself a node hue — a cyan-keyed node's name vanished
# into its own highlight. White is outside the per-node spectrum (see node_style)
# so it can never collide with an identity. It does share ink with ``you``, which
# is the accepted cost: spans that set their own colour keep it over the base, so
# only *unstyled* text goes white, and only on the row already under the reader's
# eye. Bold like ``brand`` was, so the dim-slot ``not bold`` pins that protect a
# span inside a highlighted row (see MESH_THEME_16) go on meaning what they meant.
"cursor": "bold #ffffff",
# 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",
@@ -246,6 +258,10 @@ MESH_THEME_16 = Theme(
"brand": "bold color(14)",
"accent": "bold color(12)",
"selected": "reverse bold color(14)",
# Slot 15, the top of the bright bank — so the row's bold cannot promote it
# further, and every dim-slot span inside it behaves exactly as it did under the
# old bold slot-14 highlight.
"cursor": "bold color(15)",
"err.reverse": "reverse bold color(9)",
"you": "bold color(15)",
"device.known": "bold color(15)",
+2 -2
View File
@@ -890,7 +890,7 @@ class TraceScreen(Screen):
def _action_text(self, key: str, selected: bool) -> Text:
"""One action row: pointer, glyph, and label (current value inlined)."""
text = Text("❯ " if selected else " ", style="brand" if selected else "")
text = Text("❯ " if selected else " ", style="cursor" if selected else "")
if key == "compose":
_icon(text, "✎", "brand")
text.append("Compose path")
@@ -924,7 +924,7 @@ class TraceScreen(Screen):
else:
text.append("Back")
if selected:
text.style = "brand"
text.style = "cursor"
return text
def _route_value(self, current: Optional[TraceResult], width: int) -> list[Text]:
+1 -1
View File
@@ -806,7 +806,7 @@ class AutocompleteScreen(_KeylessDialog):
self._sugg = max(0, min(self._sugg, len(suggestions) - 1)) if suggestions else 0
for i, sug in enumerate(suggestions):
is_sel = i == self._sugg
row = Text(("❯ " if is_sel else " ") + sug, style="brand" if is_sel else "muted")
row = Text(("❯ " if is_sel else " ") + sug, style="cursor" if is_sel else "muted")
row.truncate(width)
parts.append(row)
if self._error:
+5 -5
View File
@@ -631,7 +631,7 @@ class SelectScreen(Screen):
else:
label = item.text(max(1, width - 2))
pointer = "❯ " if is_sel else " "
style = "brand" if is_sel else ""
style = "cursor" if is_sel else ""
# A Text label carries its own spans (e.g. a red badge); keep them and lay the
# row's base style underneath, so the highlight tints the row while the badge
# keeps its colour. A plain string is styled uniformly as before.
@@ -876,9 +876,9 @@ class ReorderScreen(Screen):
for pos, orig in enumerate(self._order):
is_cursor = pos == self._index
if is_cursor and self._grabbed:
pointer, style = "▸ ", "brand"
pointer, style = "▸ ", "cursor"
elif is_cursor:
pointer, style = "❯ ", "brand"
pointer, style = "❯ ", "cursor"
else:
pointer, style = " ", ""
text = Text(pointer + self._labels[orig], style=style, no_wrap=True,
@@ -888,9 +888,9 @@ class ReorderScreen(Screen):
lines.append("")
for i, (_key, label) in enumerate(self._actions()):
if n + i == self._index:
# The cursor row goes full-brand like the list rows above it,
# The cursor row takes the highlight like the list rows above it,
# trading the ✓/✗ tint for the highlight.
text = Text("❯ " + label.plain, style="brand", no_wrap=True)
text = Text("❯ " + label.plain, style="cursor", no_wrap=True)
else:
text = Text(" ", no_wrap=True)
text.append_text(label)
+2 -2
View File
@@ -400,7 +400,7 @@ class TxSweepScreen(Screen):
def _action_text(self, key: str, selected: bool) -> Text:
"""One action row: pointer, glyph, and label (current value inlined)."""
text = Text("❯ " if selected else " ", style="brand" if selected else "")
text = Text("❯ " if selected else " ", style="cursor" if selected else "")
if key == "route":
_icon(text, "✎", "brand")
if self.route_hops:
@@ -430,7 +430,7 @@ class TxSweepScreen(Screen):
else:
text.append("Back")
if selected:
text.style = "brand"
text.style = "cursor"
return text
def _header_lines(self, width: int) -> list[str]:
+4 -4
View File
@@ -1230,7 +1230,7 @@ class WalkScreen(Screen):
"""One neighbour row: glyph, name, hash, SNR + bar, evidence, onward count."""
glyph, glyph_style = self._glyph(other)
row = Text()
row.append("❯ " if selected else " ", style="brand" if selected else "")
row.append("❯ " if selected else " ", style="cursor" if selected else "")
row.append(glyph, style=glyph_style)
row.append(" ")
name_style_ = self._list_name_style(other)
@@ -1258,7 +1258,7 @@ class WalkScreen(Screen):
if onward:
row.append(f" ⋯ {onward}", style="faint")
if selected:
row.style = "brand"
row.style = "cursor"
return row
def _match_row(
@@ -1267,7 +1267,7 @@ class WalkScreen(Screen):
"""One find match: glyph, name, hash, and how far out it sits."""
glyph, glyph_style = self._glyph(node)
row = Text()
row.append("❯ " if selected else " ", style="brand" if selected else "")
row.append("❯ " if selected else " ", style="cursor" if selected else "")
row.append(glyph, style=glyph_style)
row.append(" ")
row.append(fit_cells(self._label(node), name_w), style=self._list_name_style(node))
@@ -1283,7 +1283,7 @@ class WalkScreen(Screen):
ring = depths[node]
row.append(f"{ring} hop{'s' if ring != 1 else ''} out", style="muted")
if selected:
row.style = "brand"
row.style = "cursor"
return row
def _list_name_style(self, node: str) -> str:
+2 -2
View File
@@ -278,14 +278,14 @@ def test_livefeed_column_header_is_pinned_and_carries_no_sort_cue() -> None:
def test_livefeed_highlight_uses_the_app_wide_cursor() -> None:
"""The feed marks its row like every other list: a ``❯`` pointer over a brand row."""
"""The feed marks its row like every other list: ``❯`` over a ``cursor``-white row."""
screen = _screen(seed=[_obs(node="n0", age_s=1), _obs(node="n1", age_s=0)])
screen.handle("down") # off the pin, onto the newest packet normally selected
rows = _rows(screen, 100)
assert rows[0].startswith("❯ ") and rows[1].startswith(" ")
assert "▸" not in "\n".join(rows) # the old odd-one-out mark is gone
raw = screen.render_body(100)[2]
assert raw.startswith("\x1b[") # the pointer carries the brand style, not bare text
assert raw.startswith("\x1b[") # the pointer carries the cursor style, not bare text
def test_livefeed_opens_pinned_and_the_pin_rides_the_newest_packet() -> None:
+1 -1
View File
@@ -113,7 +113,7 @@ def test_paths_screen_shows_unknown_relay_as_grey_mode_width_hash() -> None:
row = next(ln for ln in lines if "e839f2" in _plain([ln]))
assert "(e8)" not in _plain([row]) # the mode-width identity stands alone
assert "38;2;148;163;184" in row # muted grey over the hash
assert "38;2;94;234;212" not in row # never the brand highlight
assert not _plain([row]).startswith("❯") # unselected, so no cursor highlight over it
graph = _plain(lines).split("origin →")[0]
assert "e8" in graph # the row's byte still cross-references the graph label
+1 -1
View File
@@ -136,7 +136,7 @@ def test_the_cursor_is_a_hop_of_its_own_in_either_mode() -> None:
chips = PathLine(hops, mode="powerline").text()
assert chips.plain == f" a {POWERLINE_SEP} {CURSOR_GLYPH} {POWERLINE_SEP} b {POWERLINE_SEP}"
slot = next(s for s in chips.spans if chips.plain[s.start : s.end] == CURSOR_GLYPH)
assert str(slot.style).endswith(f"on {_style_hex('brand')}") # the brand accent fill
assert str(slot.style).endswith(f"on {_style_hex('cursor')}") # the cursor-white fill
def test_ellipsized_elides_the_middle_and_keeps_both_endpoints() -> None:
+1 -1
View File
@@ -102,7 +102,7 @@ def test_unknown_node_grey_survives_a_selected_row() -> None:
theme=MESH_THEME_16, width=20, file=StringIO(),
force_terminal=True, color_system="standard", highlight=False,
)
for base in (None, "brand"): # unselected row, then the bold cursor row
for base in (None, "cursor"): # unselected row, then the bold cursor row
row = Text(" ")
row.append("3d", style="node.unknown")
if base: