The chat list's columns say LAST and MESSAGE; the live feed keeps TIME

The conversation list now reads CONVERSATION, NEW, LAST, MESSAGE: LAST
over the last message's age, MESSAGE over the message, shortening to MSG
on a narrow terminal. The live feed's timestamp lane is TIME again, and
the channel list keeps NEW.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-03 19:44:28 -04:00
1 parent fedd5f9608
commit 149eb0cd90
6 files changed
+20 -19

No files matched your search

+9 -8
View File
@@ -767,22 +767,23 @@ def _picker_header(lanes: _Lanes, width: int) -> str:
The indent covers the select screen's pointer column (2 cells, drawn on choice rows but
not separators) plus the marker lane, so each header lands exactly over its column.
``NEW`` counts the unread messages and ``WHEN`` is the last message's age — short words,
because the lanes under them are a badge and an age, and a heading longer than its lane
runs into the next one's. ``WHEN`` still borrows its lane's trailing gap (the age lane is
three cells), which leaves a space before the preview.
``NEW`` counts the unread messages, ``LAST`` is the last message's age, and ``MESSAGE``
is that message — short words, because the lanes under the first two are a badge and an
age, and a heading longer than its lane runs into the next one's. ``LAST`` still borrows
its lane's trailing gap (the age lane is three cells), which leaves a space before the
preview.
Resolved against the render width (the header row is pinned, so it must stay one row):
on a terminal too narrow for the whole line, ``LAST MESSAGE`` gives its cells back a
word at a time rather than the line wrapping or losing the label (see
on a terminal too narrow for the whole line, ``MESSAGE`` gives its cells back as ``MSG``
rather than the line wrapping or losing the label (see
:func:`~meshterm.ui.menus.column_header`).
"""
return column_header(
[
Lane("CONVERSATION", lanes.label + 2),
Lane("NEW", _BADGE_WIDTH + 2),
Lane("WHEN", _AGE_WIDTH + 2),
Lane(("LAST MESSAGE", "LAST MSG", "LAST")),
Lane("LAST", _AGE_WIDTH + 2),
Lane(("MESSAGE", "MSG")),
],
width,
indent=2 + lanes.marker,
+2 -2
View File
@@ -588,7 +588,7 @@ class LiveFeedScreen(Screen):
Laid out lane for lane against :meth:`_feed_row`, the pointer column included, so
every label sits over the values it names. Nothing here sorts — the feed is a
stream, and its one order (newest first) is what the ``WHEN`` lane reads down — so
stream, and its one order (newest first) is what the ``TIME`` lane reads down — so
no column carries the contact list's sort triangle or its lit active-column style;
these are signposts, not controls. On a terminal too narrow for the class label
the header drops its ``CLASS`` too, leaving the icon lane unlabelled rather than
@@ -600,7 +600,7 @@ class LiveFeedScreen(Screen):
down the whole column.
"""
header = Text(" ", style="muted") # the pointer lane
header.append(fit_cells("WHEN", _TIME_LANE))
header.append(fit_cells("TIME", _TIME_LANE))
header.append(fit_cells("CLASS", _ICON_LANE + _FEED_CLASS_WIDTH + _LANE_GAP))
header.append(fit_cells("SUBJECT", lanes.subject + _LANE_GAP))
header.append(fit_cells("SCOPE", _SCOPE_LANE))
+1 -1
View File
@@ -364,7 +364,7 @@ class Lane:
terminal. Only a lane the line actually overruns is ever shortened.
width: Cells the label is padded to — the lane's own width *plus* the gap before
the next lane, so a label wider than its column absorbs that gap instead of
shifting every lane after it right (``WHEN`` over the chat picker's 3-cell ages).
shifting every lane after it right (``LAST`` over the chat picker's 3-cell ages).
Zero (the default) for a trailing lane, which just runs to the edge.
"""
+3 -3
View File
@@ -1806,14 +1806,14 @@ async def test_picker_pins_its_column_header_over_the_group_heading(
screen.handle("down")
visible, above, _below = frame._visible_slice(screen, screen.render_body(72), 8)
top = [ansi.sub("", row).strip() for row in visible[:2]]
assert top[0].startswith("CONVERSATION") and top[0].endswith("LAST MESSAGE")
assert top[0].split()[1:3] == ["NEW", "WHEN"] # short words over a badge and an age
# Short words over a badge and an age, then the message itself.
assert top[0].split() == ["CONVERSATION", "NEW", "LAST", "MESSAGE"]
assert top[1] == "── 👤 Direct ──"
assert above is True
# Too narrow for the whole line, the trailing label shortens — never wraps. The
# lane is sized to these six-cell names now, so the squeeze starts well below any
# real terminal: the fallback is what is under test, not the width it happens at.
assert header.text(40).endswith("LAST MSG")
assert header.text(36).endswith(" MSG")
assert "\n" not in header.text(30)
+4 -4
View File
@@ -263,14 +263,14 @@ def test_livefeed_column_header_sits_over_the_lanes_it_names() -> None:
"""The header names each lane, and every label lands on the values beneath it."""
screen = _screen(seed=[_obs(node="3d63", kind="telemetry", snr=12.8, rssi=-105.0)])
header, row = _stripped(screen.render_body(90))[:2]
assert header.split() == ["WHEN", "CLASS", "SUBJECT", "SCOPE", "SNR", "RSSI"]
assert header.split() == ["TIME", "CLASS", "SUBJECT", "SCOPE", "SNR", "RSSI"]
# Measured in display cells, not characters — the class icon is one character wide
# but two cells, so a character index would report every later lane one column early.
# The clock is read off the row itself: a literal needle would only match during the
# minute it was written in.
stamp = re.search(r"\d\d:\d\d:\d\d", row)
assert stamp is not None
assert _col(header, "WHEN") == _col(row, stamp.group(0))
assert _col(header, "TIME") == _col(row, stamp.group(0))
assert _col(header, "CLASS") == _col(row, "telemetry") - _ICON_LANE
assert _col(header, "SUBJECT") == _col(row, "Hub")
# Each reading's label starts where its lane does, like every other label.
@@ -285,7 +285,7 @@ def test_livefeed_column_header_is_pinned_and_carries_no_sort_cue() -> None:
screen = _screen(seed=[_obs(node=f"n{i}", age_s=i) for i in range(40)])
screen.note_viewport(12)
lines = _stripped(screen.render_body(100))
assert "WHEN" in lines[0]
assert "TIME" in lines[0]
assert not any(mark in lines[0] for mark in ("▲", "▼")) # nothing here sorts
screen.handle("end") # the list's last row — Back
screen.handle("up") # …and one up from it, the oldest packet: the window scrolls
@@ -475,7 +475,7 @@ def test_livefeed_windows_inside_the_fixed_screen() -> None:
lines = screen.render_body(100)
assert len(lines) <= 24 # column header + feed window == the viewport, never more
body = _plain(lines)
assert "WHEN" in body # the pinned column header is still there
assert "TIME" in body # the pinned column header is still there
assert "↓" in body and "more" in body # hidden feed rows are counted below
+1 -1
View File
@@ -218,7 +218,7 @@ def test_the_picocalc_feed_names_the_class_without_an_icon() -> None:
try:
screen = _feed(_scoped("harbour"))
header, row = _stripped(screen.render_body(53))[:2]
assert header.split()[:2] == ["WHEN", "CLASS"]
assert header.split()[:2] == ["TIME", "CLASS"]
assert _col(header, "CLASS") == _col(row, "chan text")
finally:
set_platform(REGULAR)