Section headings carry no icon

Chat's Channels, Rooms, and Direct headings and the trophy case's
discipline headings drop their emoji, for a cleaner look. The trophy
marks stay on the delete list's rows and the record page's header.
section_heading no longer strips icons, since no heading has one.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Haiku 5.5 committed 2026-10-15 23:56:36 -04:00
1 parent 820740c9a0
commit eb2d314551
6 files changed
+25 -34

No files matched your search

+3 -1
View File
@@ -271,7 +271,9 @@ deliberately, one at a time, and say why in the code.
- Grouped-list section headings use `section_heading("Label")` → `── Label ──` in the
bold `heading` style — the frame title's colour, `title.border` (JP, 2026-10-15) —
never the `border` grey: a rule in the border's colour reads as
part of the frame. A chat's day divider wears the same dress. That is also what makes a heading *sticky* (it pins to the top row while its section
part of the frame. A heading is words only, never an icon — icons live in rows (JP,
2026-10-15: Chat's `📡 Channels` and the trophy case's marks came off for a cleaner
look). A chat's day divider wears the same dress. That is also what makes a heading *sticky* (it pins to the top row while its section
scrolls, and the ^PgUp/^PgDn jumps step by it), so build them through it — a hand-rolled
`Separator` is no landmark. Prose written *directly under* a heading (a description of
what the section holds) is its preamble and pins with it, in order, as each row scrolls
+3 -3
View File
@@ -252,7 +252,7 @@ class ChatTool(Tool):
# Refer to Screen.sticky_rows.
items: list = [
Separator(lambda w: _picker_header(lanes, w), pinned=True),
section_heading("📡 Channels"),
section_heading("Channels"),
]
for conversation in channels:
items.append(
@@ -264,7 +264,7 @@ class ChatTool(Tool):
)
)
items.append(section_heading("📌 Rooms"))
items.append(section_heading("Rooms"))
for conversation in rooms:
items.append(
Choice(
@@ -281,7 +281,7 @@ class ChatTool(Tool):
# a room?", and an empty section tells them where to go.
items.append(Separator(" no rooms joined — join one on the Rooms page"))
items.append(section_heading("👤 Direct"))
items.append(section_heading("Direct"))
if companions:
for conversation in direct:
items.append(
+4 -5
View File
@@ -650,10 +650,9 @@ def section_heading(label: str) -> Separator:
to heading. A row built by hand does not get this behaviour. Thus always use this
function.
A heading that starts with an icon (``📡 Channels``, the mark of a trophy discipline)
loses the icon on a platform that draws no icon lane, the same as a command row. A
heading is the same type of label. Also, a substitute glyph next to the rule, which
already draws ``──``, looks like noise (refer to :func:`command_label`).
A heading is words only, with no icon. Icons go on rows. The headings of Chat
(``📡 Channels``) and of the trophy case (the mark of each discipline) had icons. We
removed them for a cleaner look (JP, 2026-10-15).
Grey, never the accent of the frame: a rule in the colour of the border looks like a
part of the frame, not like a heading in it.
@@ -663,7 +662,7 @@ def section_heading(label: str) -> Separator:
:attr:`~meshterm.ui.tui.select.Separator.spaced`). Thus a list does not put a blank
separator before a heading for space.
"""
return Separator(f"── {command_label(label)} ──", style="heading", heading=True, spaced=True)
return Separator(f"── {label} ──", style="heading", heading=True, spaced=True)
def fit_cells(text: str, width: int, *, align: str = "left") -> str:
+8 -10
View File
@@ -127,9 +127,11 @@ _LABEL_W = 12
def discipline_label(category: Category, lane: int) -> str:
"""The mark of one discipline, padded to ``lane`` cells, then its title.
This is the only way to write the name of a board. Thus the seven headings (and the
seven rows of the list that selects a discipline to delete) start their titles in one
column, not in two. The marks do not all have the same width. ``🛣`` and ``🕸`` are
This is the only way to write the name of a board with its mark: on the seven rows of
the list that selects a discipline to delete, and on the header of the record page.
Thus the seven rows start their titles in one column, not in two. A board heading has
no mark. It is only the title (refer to :func:`~meshterm.ui.menus.section_heading`).
The marks do not all have the same width. ``🛣`` and ``🕸`` are
not in Emoji_Presentation, and they are one cell wide, while the other five are two
cells wide. Because of this, ``🛣 Longest distance`` was one column away from
``🎯 Farthest node``. Its gap also looked closed on a terminal that paints the road
@@ -157,7 +159,7 @@ def discipline_lane() -> int:
Refer to :func:`discipline_label`. The width is measured, never written as a constant.
The marks change with the platform. If a substitution becomes narrower later, each
heading becomes one cell narrower, and nothing else moves.
label becomes one cell narrower, and nothing else moves.
"""
return max(cell_len(glyph(category.icon)) for category in CATEGORIES)
@@ -448,7 +450,7 @@ class RecordScreen(Screen):
The record page has this line where the node page has its identity header. It
reads as one sentence: ``🛣 Longest distance — 5th place: 112.0 km``. The
discipline is written the same as its board heading writes it
discipline is written the same as its row in the delete list writes it
(:func:`discipline_label`: the mark is padded to the measured lane, so that ``🛣``
and ``🎯`` start their titles in the same cell). The standing is the rank of the
record on that board. The metric is the score in the unit of the discipline
@@ -913,10 +915,6 @@ async def open_records(ctx: AppContext) -> dict:
# node is matched against these keys.
target_counts = ctx.repo.target_trace_counts()
# The mark column after which each board heading writes its title (refer to
# discipline_label).
mark_lane = discipline_lane()
# The node positions and types for the area drawing of a record. They are collected
# live, the same as the trace tools collect them to score a walk (refer to
# node_geo_lookup). The entry list does not change while the screen is open.
@@ -1001,7 +999,7 @@ async def open_records(ctx: AppContext) -> dict:
def describe(category: Category) -> list:
"""The heading and its wrapped description, as rows that the user cannot select."""
rows: list = [section_heading(discipline_label(category, mark_lane))]
rows: list = [section_heading(category.title)]
desc = category.description
if category.needs_positions and self_pos is None:
desc += " — needs your location (set it in Config) to score"
+2 -2
View File
@@ -2025,7 +2025,7 @@ async def test_picker_lists_companions_only(repo: Repository) -> None:
async def test_picker_pins_its_column_header_over_the_group_heading(
repo: Repository,
) -> None:
"""On a page scrolled into Direct, the lane names stay at the top, over ``👤 Direct``."""
"""On a page scrolled into Direct, the lane names stay at the top, over ``Direct``."""
from types import SimpleNamespace
from meshterm.tools.chat import ChatTool
@@ -2049,7 +2049,7 @@ async def test_picker_pins_its_column_header_over_the_group_heading(
top = [ansi.sub("", row).strip() for row in visible[:2]]
# Short words over a badge and an age, then the message.
assert top[0].split() == ["CONVERSATION", "NEW", "LAST", "MESSAGE"]
assert top[1] == "── 👤 Direct ──"
assert top[1] == "── Direct ──"
assert above is True
# If the width is too small for the whole line, the last label gets shorter and does
# not wrap. The lane has the size of these six-cell names, so this starts at a width
+5 -13
View File
@@ -392,12 +392,10 @@ def test_record_header_names_the_discipline_the_standing_and_the_metric() -> Non
"""One line above the tabs: ``<mark> <discipline> — <nth> place: <score>``.
This is the identity header of the node page, for a record. The line writes the
discipline in the same way as its board heading. The mark has padding to the measured
discipline in the same way as its row in the delete list. The mark has padding to the measured
lane, so a ``🛣`` of one cell and a ``🎯`` of two cells start their titles in the same
cell. The score lane that repeated the metric on Info does not exist now.
"""
from meshterm.ui.records_screen import discipline_label, discipline_lane
screen = _dialog(_record(), shape=_loop_shape())
lines = _plain(screen.render_body(58)).splitlines()
grand_tour = CATEGORY_BY_ID["grand_tour"]
@@ -416,7 +414,7 @@ def test_record_header_names_the_discipline_the_standing_and_the_metric() -> Non
head = _plain(_dialog(partial, rank=5).render_body(58)).splitlines()[0]
assert head.endswith("Longest distance — 5th place: ≥ 8.4 km") # the board spelling of a bound
# The mark lane is the lane of the board heading: the titles of the two marks start in one cell.
# The mark lane is the lane of the delete list: the titles of the two marks start in one cell.
long_haul = CATEGORY_BY_ID["long_haul"]
assert cell_len(discipline_label(long_haul, discipline_lane()).split("Longest")[0]) == cell_len(
discipline_label(grand_tour, discipline_lane()).split("Most")[0]
@@ -828,14 +826,8 @@ async def test_the_board_pins_its_discipline_heading_and_description(tui_ctx) ->
blocks = [
[_plain([line]).strip() for line in rows] for _idx, rows in browser._sticky_headers
]
lane = discipline_lane()
assert [rows[0] for rows in blocks] == [
f"── {discipline_label(c, lane)} ──" for c in CATEGORIES
]
# Also, each of them starts its title in the same column. The marks do not all have
# the same width (🛣 and 🕸 are one cell, and the others are two), so the lane pads.
starts = {cell_len(discipline_label(c, lane)) - cell_len(c.title) for c in CATEGORIES}
assert len(starts) == 1
# A heading is the title only. The mark of the discipline stays on its rows.
assert [rows[0] for rows in blocks] == [f"── {c.title} ──" for c in CATEGORIES]
assert all(len(rows) >= 2 for rows in blocks) # each carries its description too
# A highlight deep in the one board that has records scrolls its heading off the top
# of a short viewport. The heading is the first item that pins, and its description
@@ -844,7 +836,7 @@ async def test_the_board_pins_its_discipline_heading_and_description(tui_ctx) ->
browser.handle("down")
visible, above, _below = frame._visible_slice(browser, browser.render_body(72), 10)
board = CATEGORY_BY_ID["grand_tour"]
assert _plain([visible[0]]).strip() == f"── {discipline_label(board, lane)} ──"
assert _plain([visible[0]]).strip() == f"── {board.title} ──"
assert _plain([visible[1]]).strip().startswith(board.description[:20])
assert above is True
browser.resolve(None) # the Esc key