In a chat, a blank line comes after the messages of each author

The transcript asked for a gap between sender groups, but an empty Text
renders as no line at all, so the gap never showed. A blank line now
closes each author's messages, also before a new day's divider, where
the platform has 20 rows or more. The 14 rows of the Cardputer Zero keep
the transcript dense. A cached slice that starts with the gap pins its
day divider one line down.

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-13 02:00:31 -04:00
1 parent a19eadd81b
commit 03b3c0926b
3 files changed
+69 -7

No files matched your search

+3
View File
@@ -18,6 +18,9 @@ can change how things behave. It does not only add new things.
message has the same mark, so a message that names you is easy to see. On the PicoCalc,
the console cannot draw a light fill, so there the `★` alone marks your messages. Before,
your messages showed only the `★`.
- **In a chat, a blank line comes after the messages of each author.** Thus the
conversation is easier to follow when many people write. The Cardputer Zero has no
blank line there, because its screen has only 14 rows and each row shows a message.
## [0.11.1] - 2026-10-13
+25 -7
View File
@@ -75,11 +75,26 @@ _URL_GUTTER = 2
_URL_CODES = False
#: Whether a blank line closes the messages of each author. It costs one row for each
#: group. The PicoCalc (26 rows) and a terminal (24) have the rows for it. The 14 rows of
#: the Cardputer Zero do not: there a blank line on each group is a message less on the
#: screen (JP, 2026-10-13).
_GROUP_GAP = True
#: The fewest rows (``Platform.readable_rows``) for which a transcript has that blank line.
_GROUP_GAP_MIN_ROWS = 20
@on_platform
def _bind(platform: Platform) -> None:
"""Hang codes under URLs where the platform draws them (now and at each switch)."""
global _URL_CODES
"""Bind the transcript to the platform (now and at each switch).
The platform says whether codes hang under URLs, and whether it has the rows for a
blank line after each author.
"""
global _URL_CODES, _GROUP_GAP
_URL_CODES = platform.url_codes
_GROUP_GAP = platform.readable_rows >= _GROUP_GAP_MIN_ROWS
#: The maximum width of a footer hint, in cells (refer to the UX standards).
@@ -613,16 +628,21 @@ class ChatScreen(Screen):
and cached_ids[idx] == id(message)
and cached_acked[idx] == message.acked
)
# A blank line closes the messages of an author, where the platform has the rows
# for it (:data:`_GROUP_GAP`). It is the first line of the slice of the message
# that starts the next group, so a cached slice brings it back with it.
gap = _GROUP_GAP and bool(lines) and (new_day or group != prev_group)
if reusable:
start = cached_boundaries[idx - 1] if idx > 0 else 0
slice_lines = cached_lines[start : cached_boundaries[idx]]
if new_day:
self._sticky_headers.append((len(lines), [slice_lines[0]]))
at = int(gap) # the divider comes after the gap
self._sticky_headers.append((len(lines) + at, [slice_lines[at]]))
lines += slice_lines
else:
if gap:
lines.append("")
if new_day:
if lines:
lines += render_lines(Text(""), width)
# A day divider is the section heading of this transcript: it pins to the
# top row while its day scrolls under it, and ^PgUp/^PgDn step by it.
# Thus it has the heading style of the app (``── Label ──`` in the bold
@@ -634,8 +654,6 @@ class ChatScreen(Screen):
self._sticky_headers.append((len(lines), [divider[0]]))
lines += divider
if new_day or group != prev_group:
if not new_day and lines:
lines += render_lines(Text(""), width) # gap between sender groups
header = self._group_header(sender, message)
lines += render_lines(header, width)
if selected:
+41
View File
@@ -1545,6 +1545,47 @@ def test_channel_own_messages_do_not_merge_with_remote_namesake() -> None:
assert rendered.count("you") == 1 and rendered.count("★ Me") == 1
def test_a_blank_line_closes_each_author_where_the_rows_allow() -> None:
"""A terminal and the PicoCalc put a blank line after the messages of each author.
The blank line also comes before the divider of a new day, and never before the first
line. The 14 rows of the Cardputer Zero have no row to spare for it. A second paint
reads the cached lines, and the divider of the second day still pins at its own line,
not at the blank line before it.
"""
from datetime import datetime, timedelta, timezone
from meshterm.platforms import CARDPUTER_ZERO, REGULAR, set_platform
base = datetime(2026, 7, 5, 14, 24, tzinfo=timezone.utc)
def at(minutes: int) -> datetime:
return base + timedelta(minutes=minutes)
def transcript() -> list[ChatMessage]:
return [
ChatMessage(text="Alice: hi", is_channel=True, channel_idx=0, created_at=at(0)),
ChatMessage(text="Alice: again", is_channel=True, channel_idx=0, created_at=at(1)),
ChatMessage(text="Bob: yo", is_channel=True, channel_idx=0, created_at=at(2)),
ChatMessage(text="Bob: later", is_channel=True, channel_idx=0, created_at=at(1440)),
]
set_platform(REGULAR)
screen = _channel_screen(transcript())
lines = [_strip_ansi(line) for line in screen._render_grouped(53)]
assert lines[0].startswith("──") and lines[1] == "Alice"
assert lines[4] == "" and lines[5] == "Bob" # after the two messages of Alice
assert lines[7] == "" and lines[8].startswith("── Mon") # after Bob, before the new day
assert lines.count("") == 2
first = list(screen._sticky_headers)
screen._render_grouped(53) # the cached lines
assert screen._sticky_headers == first and [at for at, _ in first] == [0, 8]
set_platform(CARDPUTER_ZERO)
screen = _channel_screen(transcript())
assert "" not in [_strip_ansi(line) for line in screen._render_grouped(53)]
def test_a_mention_of_our_name_takes_our_chip() -> None:
"""A message that names us is easy to see. A mention of each other node is as before."""
screen = _channel_screen([], self_name="Me", self_key=_OUR_KEY)