Column headings say NEW and WHEN, not UNREAD and TIME
The chat picker's unread count is headed NEW and its last-message age WHEN; the channel list's unread count is NEW; the live feed's timestamp lane is WHEN. Short words over a badge and an age, which UNREAD outgrew by a cell and had to borrow the gap for. The command line's columns keep their names: they are a documented contract of their own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6ff39fba89
commit
17f57b1bbc
7 files changed
+19
-16
No files matched your search
@@ -695,7 +695,7 @@ _BADGE_WIDTH = 5
|
||||
_AGE_WIDTH = 3
|
||||
#: Floor for the conversation lane. The header word is ``CONVERSATION`` (12 cells) and the
|
||||
#: lane carries a two-cell gap after it, so anything narrower than this runs the heading
|
||||
#: straight into ``UNREAD`` on a mesh whose every name is ``Bob``.
|
||||
#: straight into ``NEW`` on a mesh whose every name is ``Bob``.
|
||||
_LABEL_MIN = 12
|
||||
#: Ceiling for it. The lane is sized to the longest name it actually holds (see
|
||||
#: :func:`_lanes`), and this is where that stops paying: one very long name would otherwise
|
||||
@@ -767,8 +767,10 @@ 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.
|
||||
UNREAD borrows its lane's trailing gap — the badge lane itself is one cell too narrow
|
||||
for the word — which still leaves a space before the age 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.
|
||||
|
||||
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
|
||||
@@ -778,8 +780,8 @@ def _picker_header(lanes: _Lanes, width: int) -> str:
|
||||
return column_header(
|
||||
[
|
||||
Lane("CONVERSATION", lanes.label + 2),
|
||||
Lane("UNREAD", _BADGE_WIDTH + 2),
|
||||
Lane("TIME", _AGE_WIDTH + 2),
|
||||
Lane("NEW", _BADGE_WIDTH + 2),
|
||||
Lane("WHEN", _AGE_WIDTH + 2),
|
||||
Lane(("LAST MESSAGE", "LAST MSG", "LAST")),
|
||||
],
|
||||
width,
|
||||
|
||||
@@ -540,7 +540,7 @@ def _lanes_header(slot_w: int, name_w: int, scope_w: int, width: int) -> str:
|
||||
Lane("SLOT", slot_w + 2 + cell_len(channel_glyph("Public", None)) + 1),
|
||||
Lane("CHANNEL", name_w + 2),
|
||||
*([Lane("SCOPE", scope_w + 2)] if scope_w else []),
|
||||
Lane("UNREAD", _BADGE_WIDTH + 2),
|
||||
Lane("NEW", _BADGE_WIDTH + 2),
|
||||
# Right-aligned, because the values under them are: an age and a count are
|
||||
# padded to the right edge of their lane, so a left-aligned label would sit
|
||||
# off the digits it names.
|
||||
|
||||
@@ -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 ``TIME`` lane reads down — so
|
||||
stream, and its one order (newest first) is what the ``WHEN`` 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("TIME", _TIME_LANE))
|
||||
header.append(fit_cells("WHEN", _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))
|
||||
|
||||
@@ -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 (``UNREAD`` over a narrower badge lane).
|
||||
shifting every lane after it right (``WHEN`` over the chat picker's 3-cell ages).
|
||||
Zero (the default) for a trailing lane, which just runs to the edge.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1807,6 +1807,7 @@ async def test_picker_pins_its_column_header_over_the_group_heading(
|
||||
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
|
||||
assert top[1] == "── 👤 Direct ──"
|
||||
assert above is True
|
||||
# Too narrow for the whole line, the trailing label shortens — never wraps. The
|
||||
|
||||
@@ -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() == ["TIME", "CLASS", "SUBJECT", "SCOPE", "SNR", "RSSI"]
|
||||
assert header.split() == ["WHEN", "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, "TIME") == _col(row, stamp.group(0))
|
||||
assert _col(header, "WHEN") == _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 "TIME" in lines[0]
|
||||
assert "WHEN" 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 "TIME" in body # the pinned column header is still there
|
||||
assert "WHEN" in body # the pinned column header is still there
|
||||
assert "↓" in body and "more" in body # hidden feed rows are counted below
|
||||
|
||||
|
||||
|
||||
@@ -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] == ["TIME", "CLASS"]
|
||||
assert header.split()[:2] == ["WHEN", "CLASS"]
|
||||
assert _col(header, "CLASS") == _col(row, "chan text")
|
||||
finally:
|
||||
set_platform(REGULAR)
|
||||
@@ -411,12 +411,12 @@ def _channel_header(scopes: dict[int, str]) -> str:
|
||||
|
||||
def test_the_channel_list_draws_scope_only_when_a_channel_has_one() -> None:
|
||||
"""A column of blanks says nothing; one scoped channel brings the lane back."""
|
||||
assert _channel_header({}).split() == ["SLOT", "CHANNEL", "UNREAD", "LAST", "MSGS", "ACTIVITY"]
|
||||
assert _channel_header({}).split() == ["SLOT", "CHANNEL", "NEW", "LAST", "MSGS", "ACTIVITY"]
|
||||
assert _channel_header({1: "harbour"}).split() == [
|
||||
"SLOT",
|
||||
"CHANNEL",
|
||||
"SCOPE",
|
||||
"UNREAD",
|
||||
"NEW",
|
||||
"LAST",
|
||||
"MSGS",
|
||||
"ACTIVITY",
|
||||
|
||||
Reference in new issue
Block a user