diff --git a/CLAUDE.md b/CLAUDE.md index f90daff..77eb80e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/meshterm/tools/chat.py b/meshterm/tools/chat.py index 6ca668a..b494b9c 100644 --- a/meshterm/tools/chat.py +++ b/meshterm/tools/chat.py @@ -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( diff --git a/meshterm/ui/menus.py b/meshterm/ui/menus.py index 29b04b7..7f8d6c7 100644 --- a/meshterm/ui/menus.py +++ b/meshterm/ui/menus.py @@ -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: diff --git a/meshterm/ui/records_screen.py b/meshterm/ui/records_screen.py index e3ff894..74941c3 100644 --- a/meshterm/ui/records_screen.py +++ b/meshterm/ui/records_screen.py @@ -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" diff --git a/tests/test_chat.py b/tests/test_chat.py index b77c620..d1b5a11 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -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 diff --git a/tests/test_records_screen.py b/tests/test_records_screen.py index fc8298c..80a3ed5 100644 --- a/tests/test_records_screen.py +++ b/tests/test_records_screen.py @@ -392,12 +392,10 @@ def test_record_header_names_the_discipline_the_standing_and_the_metric() -> Non """One line above the tabs: `` — place: ``. 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