From 149eb0cd90e8dd2a302e547d5b4408595e23fad5 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sat, 3 Oct 2026 19:44:26 -0400 Subject: [PATCH] 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) --- meshterm/tools/chat.py | 17 +++++++++-------- meshterm/ui/livefeed_screen.py | 4 ++-- meshterm/ui/menus.py | 2 +- tests/test_chat.py | 6 +++--- tests/test_livefeed.py | 8 ++++---- tests/test_scope_display.py | 2 +- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/meshterm/tools/chat.py b/meshterm/tools/chat.py index 6aa834f..61ee6f3 100644 --- a/meshterm/tools/chat.py +++ b/meshterm/tools/chat.py @@ -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, diff --git a/meshterm/ui/livefeed_screen.py b/meshterm/ui/livefeed_screen.py index aaa1160..b39fcf4 100644 --- a/meshterm/ui/livefeed_screen.py +++ b/meshterm/ui/livefeed_screen.py @@ -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)) diff --git a/meshterm/ui/menus.py b/meshterm/ui/menus.py index f204cba..dcdbaa3 100644 --- a/meshterm/ui/menus.py +++ b/meshterm/ui/menus.py @@ -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. """ diff --git a/tests/test_chat.py b/tests/test_chat.py index 646d795..457b5f0 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -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) diff --git a/tests/test_livefeed.py b/tests/test_livefeed.py index 233295b..f613e62 100644 --- a/tests/test_livefeed.py +++ b/tests/test_livefeed.py @@ -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 diff --git a/tests/test_scope_display.py b/tests/test_scope_display.py index 8077832..09c46e5 100644 --- a/tests/test_scope_display.py +++ b/tests/test_scope_display.py @@ -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)