On a bordered frame the map's credit moves into the bottom rule, out of the map
The credit was stamped over the right end of the drawing's own last row on both platforms. That is the traditional corner and it is where a frame with no bottom border has to put it, but where there *is* a bottom border there is already a rule under the body doing nothing except closing the box — and a caption set into it costs the picture nothing at all. JP, looking at the desktop map: "that way it's not in the map." So a screen may now hand the frame a bottom_caption, which _panel_box sets into the bottom rule right-justified through Rich's own subtitle machinery — landing as "└──… © OpenStreetMap ─┘", one rule cell before the corner, the mirror of the way a title sits in the top rule. The rule's other occupant keeps its place: the clip arrows still lead, centred when they are alone and joined by the caption, right, when both are wanted, because dropping either would be the frame deciding something neither screen asked of it. The caption joins the base box memo's key as well — the credit collapses on a keystroke that need not change a single body line, and a memo blind to it would go on serving a rule the reader has already grown out of. The map asks attribution for both halves and tests nothing itself: rule_caption gives the caption where the platform's frame has a rule, map_body stamps the drawing where it hasn't, and exactly one of them draws. Which it is, is bound once at platform-switch time rather than asked per paint. Nothing about the PicoCalc changes — same strings, same corner, same collapse, and the minimap still stamps on both platforms, having no frame of its own to set anything into. JP read that form on the device's own panel on 13 September and signed it off as it stands; the docstring now says so, so nobody goes tidying it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8916544bfb
commit
8a1bb875a7
9 files changed
+348
-73
No files matched your search
+12
-10
@@ -25,16 +25,18 @@ allowed to change behaviour, not just add to it.
|
||||
build actually installed — at the root of the archive, and the same three files ride
|
||||
beside the downloads on the GitHub release page.
|
||||
|
||||
- **The map credits OpenStreetMap on the map.** The full-screen map is arrived at with
|
||||
`© OpenMapTiles · Data from OpenStreetMap` in its bottom-right corner — the corner a
|
||||
paper map puts it in — and the first pan, zoom, reframe or find keystroke collapses it
|
||||
to `© OpenStreetMap`, which is a complete credit in its own right, so nothing is ever
|
||||
taken back. It costs no chrome: no title atom, no footer character, no body row, just
|
||||
fifteen cells of the drawing's own bottom line that a keypress can pan out from under.
|
||||
The node page's location preview carries the short form throughout, being a picture
|
||||
rather than a place. The licence and its URL live where they can be read — the About
|
||||
page, which also now names **OpenMapTiles**, as OpenFreeMap asks and neither the page
|
||||
nor the README did.
|
||||
- **The map credits OpenStreetMap in its own bottom-right corner.** The full-screen map is
|
||||
arrived at with `© OpenMapTiles · Data from OpenStreetMap` there — the corner a paper map
|
||||
puts it in — and the first pan, zoom, reframe or find keystroke collapses it to
|
||||
`© OpenStreetMap`, which is a complete credit in its own right, so nothing is ever taken
|
||||
back. It costs no chrome at all: no title atom, no footer character, no body row. On a
|
||||
desktop terminal it is set into the panel's bottom border rule, right-justified, the way
|
||||
a title sits in the top one, so the drawing keeps every cell it had; on the PicoCalc,
|
||||
whose frame has no bottom rule to set anything into, it rides the right end of the map's
|
||||
own last row, where a pan can move the ground out from under it. The node page's location
|
||||
preview carries the short form throughout, being a picture rather than a place. The
|
||||
licence and its URL live where they can be read — the About page, which also now names
|
||||
**OpenMapTiles**, as OpenFreeMap asks and neither the page nor the README did.
|
||||
|
||||
- **Every Python file names its licence with an SPDX identifier** on its first line (second, after a shebang), and `tests/test_spdx.py` keeps a new file from landing without one. Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all.
|
||||
|
||||
|
||||
@@ -22,10 +22,17 @@ never `git push` unless explicitly asked in that moment.
|
||||
shebang). `tests/test_spdx.py` is the gate. Anything under another licence lives beside
|
||||
its own text and says so in its header — the GPL-2.0-only console-font script, the
|
||||
MeshCore MIT notice beside the XIAO patch — and never enters the distributed package.
|
||||
- **The map credits OpenStreetMap on the map** (`ui/attribution.py`): the full OpenFreeMap
|
||||
line in the drawing's bottom-right corner on arrival, `© OpenStreetMap` there after the
|
||||
first keystroke, `muted`, overprinting the cells it sits in. Never a title atom, never a
|
||||
footer character, never a key. The licence and URL live on the About page.
|
||||
- **The map credits OpenStreetMap in the map's own bottom-right corner**
|
||||
(`ui/attribution.py`): the full OpenFreeMap line on arrival, `© OpenStreetMap` after the
|
||||
first keystroke, `muted`, on both platforms and in the same words. *Which* corner is the
|
||||
frame's, not the drawing's, wherever the frame has one — **regular** sets the credit into
|
||||
the panel's **bottom border rule**, right-justified, one rule cell before the corner, the
|
||||
way a title sits in the top rule (`Screen.bottom_caption` → `frame._panel_box`), so the
|
||||
map's cells are untouched; **picocalc**, whose frame has no bottom rule at all, stamps it
|
||||
over the right end of the drawing's last row instead, overprinting what it covers. The
|
||||
minimap always stamps. The split is bound once via `platforms.on_platform`, never asked
|
||||
per frame. Never a title atom, never a footer character, never a key. The licence and URL
|
||||
live on the About page.
|
||||
- The name and the wordmark are trademarks reserved in `NOTICE`, which Apache §4(d) makes
|
||||
every fork carry; `NOTICE` holds only what must travel with a redistribution.
|
||||
|
||||
|
||||
@@ -487,9 +487,11 @@ under the [Open Database License (ODbL)](https://www.openstreetmap.org/copyright
|
||||
are decoded with a small built-in reader for the
|
||||
[Mapbox Vector Tile](https://github.com/mapbox/vector-tile-spec) format.
|
||||
|
||||
Both map surfaces carry the credit themselves: the full-screen map opens with
|
||||
Both map surfaces carry the credit themselves. The full-screen map opens with
|
||||
`© OpenMapTiles · Data from OpenStreetMap` in its bottom-right corner and collapses it to
|
||||
`© OpenStreetMap` once you pan, zoom or type; the node page's location preview shows the
|
||||
`© OpenStreetMap` once you pan, zoom or type — set into the panel's bottom border rule on
|
||||
a desktop terminal, so the drawing itself keeps every cell, and on the map's own last row
|
||||
on the PicoCalc, whose frame has no bottom rule. The node page's location preview shows the
|
||||
short form throughout. The About page inside the app spells out the licence URL.
|
||||
|
||||
### Bundled font
|
||||
|
||||
+86
-11
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""The basemap's credit — OpenStreetMap's name, stamped in the corner of the drawing.
|
||||
"""The basemap's credit — OpenStreetMap's name, in the corner of the map's own frame.
|
||||
|
||||
The street map is rendered from OpenStreetMap data, served as vector tiles by
|
||||
OpenFreeMap on the unmodified OpenMapTiles schema. All three want naming, and the
|
||||
@@ -30,11 +30,21 @@ nice if you do."* — so :data:`CREDIT_FULL` is that line less its optional half
|
||||
|
||||
That is the whole design, and it is built to cost the map almost nothing:
|
||||
|
||||
* **The credit rides the map, not the chrome.** It is stamped over the right end of the
|
||||
drawing's own bottom row, which is the corner the guideline calls traditional. No
|
||||
title atom, no footer character, no body line: the title is a status line already
|
||||
chaining four atoms and the footer hint spends its whole 72-cell budget, and a credit
|
||||
bolted to either would be paid for on every screen of every visit.
|
||||
* **The credit rides the map's own frame, never a line of its own.** No title atom, no
|
||||
footer character, no body row: the title is a status line already chaining four atoms
|
||||
and the footer hint spends its whole 72-cell budget, and a credit bolted to either
|
||||
would be paid for on every screen of every visit. *Where* it rides is the one thing
|
||||
that differs by platform, and only because the platforms' frames differ. A bordered
|
||||
frame has a **bottom border rule**, so the credit sits in it the way a title sits in
|
||||
the top rule — right-justified, muted, one rule cell before the corner — and the
|
||||
drawing itself is left alone (JP, 2026-09-13: *"that way it's not in the map"*). The
|
||||
PicoCalc's frame has no bottom rule at all — a borderless title bar above, the F-key
|
||||
lane below, and body rows in between — so there the credit is **stamped over the right
|
||||
end of the drawing's own bottom row**, which is still the corner the guideline calls
|
||||
traditional, and costs the map fifteen cells of ground a pan can move out from under.
|
||||
That form was read on the device's own panel and signed off as it stands (JP,
|
||||
2026-09-13), so its glyphs, its position and its collapse are settled: leave them alone.
|
||||
:func:`rule_caption` and :func:`stamp` are the two forms; :func:`map_body` picks.
|
||||
* **It arrives whole and shrinks once used.** A map the reader has not touched yet shows
|
||||
:data:`CREDIT_FULL`, so the attribution is never something you have to interact to
|
||||
see; the first pan, zoom, reframe or find keystroke collapses it to
|
||||
@@ -51,11 +61,12 @@ That is the whole design, and it is built to cost the map almost nothing:
|
||||
unadvertised key is an undiscoverable one, so a reveal would have had to buy a chip on
|
||||
a five-slot F-key lane that is already full.
|
||||
|
||||
The mark **wins the cells it sits in**: it is spliced over the rendered row, so a street,
|
||||
a braille dot or a node label under it is overprinted rather than shifted. The guideline
|
||||
requires the attribution to be "legible and understandable", which a credit a passing
|
||||
label could shred is not — and the map is pannable, so ground hidden under fifteen cells
|
||||
of one row is a keypress away, while the credit is not allowed to be.
|
||||
Where it *is* stamped on the drawing — the PicoCalc's map, and the location preview on
|
||||
both platforms — the mark **wins the cells it sits in**: it is spliced over the rendered
|
||||
row, so a street, a braille dot or a node label under it is overprinted rather than
|
||||
shifted. The guideline requires the attribution to be "legible and understandable", which
|
||||
a credit a passing label could shred is not — and the map is pannable, so ground hidden
|
||||
under fifteen cells of one row is a keypress away, while the credit is not allowed to be.
|
||||
|
||||
Its style is ``muted``: this is chrome drawn on the map, not content on it. It must not
|
||||
be mistaken for a node, so it takes neither the ``you`` white nor any key-derived node
|
||||
@@ -67,6 +78,7 @@ from __future__ import annotations
|
||||
from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from ..platforms import Platform, on_platform
|
||||
from .tui.render import crop_cells, render_to_ansi
|
||||
|
||||
#: The whole credit, shown until the reader first touches the map. OpenFreeMap's required
|
||||
@@ -84,6 +96,18 @@ CREDIT_SHORT = "© OpenStreetMap"
|
||||
#: never reads as the tail of a street name it happens to land beside.
|
||||
_GAP = 1
|
||||
|
||||
#: Whether this platform's frame has a bottom border rule for the credit to sit in, rather
|
||||
#: than the map having to give up cells of its own bottom row. Bound once per platform
|
||||
#: switch (:func:`_bind`) so no paint ever asks the platform this question itself.
|
||||
_HAS_RULE = True
|
||||
|
||||
|
||||
@on_platform
|
||||
def _bind(platform: Platform) -> None:
|
||||
"""Settle where the credit goes when the platform is chosen, not when a frame is drawn."""
|
||||
global _HAS_RULE
|
||||
_HAS_RULE = platform.frame_border
|
||||
|
||||
|
||||
def credit(*, full: bool) -> Text:
|
||||
"""The credit as a styled run — :data:`CREDIT_FULL` or :data:`CREDIT_SHORT`.
|
||||
@@ -147,3 +171,54 @@ def stamp(lines: list[str], width: int, *, full: bool, left: Text | None = None)
|
||||
row.pad_right(max(0, keep - cell_len(row.plain)))
|
||||
row.append_text(mark)
|
||||
return [*lines[:-1], render_to_ansi(row, width, no_wrap=True)]
|
||||
|
||||
|
||||
def rule_caption(*, full: bool) -> str:
|
||||
"""The credit for the frame's bottom border rule — ``""`` where the frame draws none.
|
||||
|
||||
A bordered frame already has a rule under the body doing nothing but closing the box,
|
||||
and a caption in it is the cheapest place the credit can possibly go: it takes no cell
|
||||
of the drawing and no cell of any line the app was otherwise using. The frame renders
|
||||
it right-justified through Rich's own subtitle machinery
|
||||
(:func:`~meshterm.ui.tui.frame._panel_box`), so it lands as ``──── © OpenStreetMap ─╯``
|
||||
— one rule cell before the corner, exactly as a title sits in the top rule.
|
||||
|
||||
Empty on the PicoCalc, whose frame has no bottom rule to sit in: there the credit is
|
||||
stamped on the drawing instead (:func:`stamp`). Both callers ask unconditionally and
|
||||
one of them gets nothing, so neither has to know which platform it is on.
|
||||
|
||||
Args:
|
||||
full: Whether the credit is still in its whole form (see :func:`credit`).
|
||||
|
||||
Returns:
|
||||
The caption, or ``""`` where this platform's frame has no rule for it.
|
||||
"""
|
||||
return (CREDIT_FULL if full else CREDIT_SHORT) if _HAS_RULE else ""
|
||||
|
||||
|
||||
def map_body(lines: list[str], width: int, *, full: bool, left: Text | None = None) -> list[str]:
|
||||
"""The full-screen map's rows, credited wherever this platform's frame cannot do it.
|
||||
|
||||
The counterpart to :func:`rule_caption`, and the reason the map's ``render_body`` needs
|
||||
no platform test of its own: exactly one of the two marks the map, and this is the one
|
||||
that draws nothing where the frame's bottom rule is carrying the credit instead.
|
||||
|
||||
``left`` (the find query echo) is drawn either way, because it belongs to the map
|
||||
rather than to the credit — though in practice only the rule-less platform asks for it,
|
||||
that being the same platform whose footer is the F-key lane and so has nowhere else to
|
||||
put the query.
|
||||
|
||||
Args:
|
||||
lines: The rendered map rows.
|
||||
width: The row width in cells.
|
||||
full: Whether the credit is still in its whole form (see :func:`credit`).
|
||||
left: The find query echo for the bottom row, or ``None``.
|
||||
|
||||
Returns:
|
||||
A new list of rows (the input's own list where there is nothing to draw).
|
||||
"""
|
||||
if not _HAS_RULE:
|
||||
return stamp(lines, width, full=full, left=left)
|
||||
if left is None or not lines or width <= 0:
|
||||
return lines
|
||||
return [*lines[:-1], render_to_ansi(crop_cells(left, 0, width), width, no_wrap=True)]
|
||||
@@ -364,7 +364,24 @@ class MapScreen(Screen):
|
||||
self._ensure_prefetch(vp) # last: it reads what this paint just asked for
|
||||
self.title = self._title(vp)
|
||||
echo = query_text(self._filter) if self._query_echo() else None
|
||||
return attribution.stamp(lines, width, full=self._untouched, left=echo)
|
||||
return attribution.map_body(lines, width, full=self._untouched, left=echo)
|
||||
|
||||
@property
|
||||
def bottom_caption(self) -> str: # type: ignore[override]
|
||||
"""The basemap credit, for a frame whose bottom rule can carry it.
|
||||
|
||||
The other half of :func:`~meshterm.ui.attribution.map_body`, and the half that
|
||||
costs the drawing nothing: where the panel has a bottom border, the credit is set
|
||||
into it right-justified rather than stamped over the map's own last row (JP,
|
||||
2026-09-13 — *"that way it's not in the map"*). Empty where the frame has no rule
|
||||
to set it into, which is exactly where ``map_body`` stamps instead, so between
|
||||
them the credit is drawn once on every platform and twice on none.
|
||||
|
||||
Read per paint, like the title, so the collapse to
|
||||
:data:`~meshterm.ui.attribution.CREDIT_SHORT` reaches the rule on the very
|
||||
keystroke that causes it.
|
||||
"""
|
||||
return attribution.rule_caption(full=self._untouched)
|
||||
|
||||
def _ensure_viewport(self, width: int) -> Viewport:
|
||||
"""The viewport for a body ``width`` cells wide — built on first paint, else resized.
|
||||
|
||||
@@ -143,29 +143,49 @@ def _breathing_room(body_len: int, budget: int) -> int:
|
||||
|
||||
|
||||
def _panel_box(
|
||||
title: str, visible: list[str], more_above: bool, more_below: bool, border: str
|
||||
title: str,
|
||||
visible: list[str],
|
||||
more_above: bool,
|
||||
more_below: bool,
|
||||
border: str,
|
||||
caption: str = "",
|
||||
) -> Panel:
|
||||
"""Wrap already-sliced body lines in a titled, scroll-aware panel.
|
||||
|
||||
The bottom rule carries up to two things, and they share one subtitle rather than
|
||||
fighting over the line. The clip arrows sit centred as they always have. A
|
||||
``caption`` — a screen's :attr:`~meshterm.ui.tui.screen.Screen.bottom_caption`, which
|
||||
today means the map's OpenStreetMap credit — is **right-justified**, so it lands as
|
||||
``──── © OpenStreetMap ─╯``: one rule cell before the corner, the mirror of the way
|
||||
the title sits in the top rule, and not one cell of the body given up for it. Where
|
||||
both are wanted the caption joins the arrows in the same run and the run moves right,
|
||||
because the arrows are the half a reader is looking *for* and the caption is the half
|
||||
that must simply be present — dropping either would be the frame deciding something
|
||||
neither screen asked it to.
|
||||
|
||||
Args:
|
||||
title: The screen's heading (empty for none).
|
||||
visible: The viewport's ANSI lines, already sliced and padded to height.
|
||||
more_above: Whether content continues above the slice.
|
||||
more_below: Whether content continues below the slice.
|
||||
border: Border style name (``"accent"`` for the focused screen).
|
||||
caption: A short muted credit for the bottom rule's right end (empty for none).
|
||||
|
||||
Returns:
|
||||
A Rich :class:`Panel` of exactly ``len(visible) + 2`` rows.
|
||||
"""
|
||||
body = Text.from_ansi("\n".join(visible))
|
||||
subtitle = None
|
||||
atoms = []
|
||||
if more_above or more_below:
|
||||
arrow = ("↑" if more_above else " ") + ("↓" if more_below else " ")
|
||||
subtitle = f"[{hint_style(border)}]{arrow} more[/]"
|
||||
atoms.append(("↑" if more_above else " ") + ("↓" if more_below else " ") + " more")
|
||||
if caption:
|
||||
atoms.append(caption)
|
||||
subtitle = f"[{hint_style(border)}]{' · '.join(atoms)}[/]" if atoms else None
|
||||
return Panel(
|
||||
body,
|
||||
title=f"[{title_style(border)}]{title}[/]" if title else None,
|
||||
subtitle=subtitle,
|
||||
subtitle_align="right" if caption else "center",
|
||||
border_style=border,
|
||||
padding=(0, 1),
|
||||
)
|
||||
@@ -314,11 +334,15 @@ def compose_base(
|
||||
# flip with the physical Shift key alone) has no place in the key, so it renders
|
||||
# uncached — that combination doesn't arise: the lane belongs to the borderless
|
||||
# platform below.
|
||||
key = (cols, rows, base.title, footer_hint, more_above, more_below, *visible)
|
||||
# The caption belongs in the key like the title does: the map's credit collapses on
|
||||
# a keystroke that need not change a single body line, and a memo without it would
|
||||
# keep serving the rule the reader has already grown out of.
|
||||
caption = base.bottom_caption
|
||||
key = (cols, rows, base.title, caption, footer_hint, more_above, more_below, *visible)
|
||||
if footer_lane is None and _BASE_BOX_CACHE is not None and _BASE_BOX_CACHE[0] == key:
|
||||
body = _BASE_BOX_CACHE[1]
|
||||
else:
|
||||
panel = _panel_box(base.title, visible, more_above, more_below, "accent")
|
||||
panel = _panel_box(base.title, visible, more_above, more_below, "accent", caption)
|
||||
body = render_lines(Group(panel, footer_row()), cols)
|
||||
if footer_lane is None:
|
||||
_BASE_BOX_CACHE = (key, body)
|
||||
|
||||
@@ -92,10 +92,20 @@ class Screen:
|
||||
base screen. Ignored while ``chrome`` is ``True``.
|
||||
footnote: A short line drawn muted and centered *below* a chromeless base screen's
|
||||
box (e.g. a copyright notice). Ignored while ``chrome`` is ``True``.
|
||||
bottom_caption: A short credit or label set into the **bottom border rule**,
|
||||
right-justified, the way :attr:`title` sits in the top one — for something
|
||||
that belongs to the framed picture rather than to any row inside it. The map's
|
||||
OpenStreetMap credit is the reason it exists (see
|
||||
:mod:`meshterm.ui.attribution`): a bordered frame already draws that rule, so
|
||||
a caption in it costs the body nothing at all. Read fresh every paint, so a
|
||||
screen may change it as its state changes. Drawn only where the platform's
|
||||
frame *has* a bottom rule — the borderless one has none, and a screen wanting
|
||||
a mark there puts it in its own body instead.
|
||||
"""
|
||||
|
||||
title: str = ""
|
||||
footer_hint: str = "Esc back"
|
||||
bottom_caption: str = ""
|
||||
floating: bool = True
|
||||
modal: bool = False
|
||||
grow_only: bool = False
|
||||
|
||||
+52
-3
@@ -68,6 +68,7 @@ from meshterm.ui.about import (
|
||||
join_discord,
|
||||
support_project,
|
||||
)
|
||||
from meshterm.ui.attribution import CREDIT_FULL, CREDIT_SHORT
|
||||
from meshterm.ui.chat import ChatScreen
|
||||
from meshterm.ui.config_editor import _ConfigMenu, _menu_items, config_table, has_pin
|
||||
from meshterm.ui.contactlist import SORT_COLUMNS, SORT_OPENS_ASCENDING
|
||||
@@ -340,9 +341,10 @@ def _map_body_rows(rows: int) -> int:
|
||||
The map is the one screen that sizes its own canvas from
|
||||
:meth:`~meshterm.ui.tui.session.TuiSession.base_body_size`, so the stub session has to
|
||||
answer with the *body* height rather than the terminal's — otherwise the specimen draws
|
||||
a canvas taller than the slice it is shown in, which cuts the bottom row (and with it
|
||||
the basemap credit that rides there) and hangs a phantom "↓ more" off a map that never
|
||||
scrolls. Mirrors ``base_body_size`` with no header — these specimens compose against an
|
||||
a canvas taller than the slice it is shown in, which cuts the bottom row (and on the
|
||||
borderless platform the basemap credit that rides there) and hangs a phantom "↓ more"
|
||||
off a map that never scrolls. Mirrors ``base_body_size`` with no header — these
|
||||
specimens compose against an
|
||||
empty one — so it is the footer row plus either the panel's two borders or the
|
||||
borderless platform's single title bar.
|
||||
"""
|
||||
@@ -1036,6 +1038,10 @@ _KNOWN_WIDE: set[str] = set()
|
||||
_PREEXISTING_REGULAR_OVERFLOW: set[str] = set()
|
||||
|
||||
|
||||
#: The specimens by name, for a test that wants one entry rather than the whole sweep.
|
||||
_ENTRY_BY_NAME = {entry.name: entry for entry in _ENTRIES}
|
||||
|
||||
|
||||
def _cases():
|
||||
for entry in _ENTRIES:
|
||||
for platform, cols, rows in _COMBOS:
|
||||
@@ -1136,3 +1142,46 @@ def test_gallery_screen_fits_its_platform(
|
||||
f"{entry.name} {where} line {i} has characters outside the console "
|
||||
f"font: {sorted(strays)!r} in {line!r}"
|
||||
)
|
||||
|
||||
|
||||
#: The map specimens and the credit each should be carrying — the whole OpenFreeMap line
|
||||
#: on the map nobody has touched, the remnant on the two that have been used.
|
||||
_CREDITED: list[tuple[str, bool]] = [
|
||||
("map", True),
|
||||
("map_panned", False),
|
||||
("map_find", False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,full", _CREDITED)
|
||||
@pytest.mark.parametrize("platform,cols,rows", _COMBOS)
|
||||
def test_gallery_map_shows_the_basemap_credit_where_its_frame_puts_it(
|
||||
name: str,
|
||||
full: bool,
|
||||
platform: Platform,
|
||||
cols: int,
|
||||
rows: int,
|
||||
) -> None:
|
||||
"""Every map specimen credits OpenStreetMap, on the surface its own frame affords.
|
||||
|
||||
The width gate above would be perfectly happy with a map that had quietly stopped
|
||||
crediting anyone, so the specimens assert the mark itself as well: on a bordered frame
|
||||
it is set into the bottom border rule, right-justified with one rule cell before the
|
||||
corner, and the drawing is left alone; on the borderless one, which has no rule, it is
|
||||
stamped on the drawing's own last row. See :mod:`meshterm.ui.attribution`.
|
||||
"""
|
||||
set_platform(platform)
|
||||
screen = _ENTRY_BY_NAME[name].factory(cols, rows)
|
||||
screen.note_viewport(max(1, rows - 4))
|
||||
expected = CREDIT_FULL if full else CREDIT_SHORT
|
||||
body = _plain(screen.render_body(cols))
|
||||
composed = frame.compose_base(Text(""), screen, screen.footer_hint, cols, rows)
|
||||
lines = [_plain(ln) for ln in composed.split("\n")]
|
||||
|
||||
if platform.frame_border:
|
||||
assert "OpenStreetMap" not in body, "the credit reached the drawing on a bordered frame"
|
||||
rule = next(ln for ln in reversed(lines) if "└" in ln)
|
||||
assert rule.endswith(f" {expected} ─┘"), rule
|
||||
else:
|
||||
assert body.splitlines()[-1].endswith(expected)
|
||||
assert any(expected in ln for ln in lines)
|
||||
+125
-36
@@ -2568,6 +2568,12 @@ def test_the_map_registers_no_cli_command() -> None:
|
||||
# meshterm/ui/attribution.py): the credit sits in a corner of the map, names
|
||||
# "OpenStreetMap" rather than an abbreviation, is visible without interacting, and may
|
||||
# collapse once the map is used — to a form that is still a complete OSM attribution.
|
||||
#
|
||||
# Which corner is the *frame's*, and the two platforms' frames differ: a bordered one sets
|
||||
# the credit into its bottom border rule and leaves the drawing alone, while the
|
||||
# borderless one has no rule and stamps the drawing's own last row instead. So most of
|
||||
# these run twice, once per platform, and assert on the surface that platform actually
|
||||
# marks.
|
||||
|
||||
|
||||
def _credited_map(cols: int = 80) -> MapScreen:
|
||||
@@ -2583,6 +2589,36 @@ def _last_row(screen: MapScreen, cols: int = 80) -> str:
|
||||
return _plain(screen.render_body(cols)).splitlines()[-1]
|
||||
|
||||
|
||||
def _bottom_rule(screen: MapScreen, cols: int = 80, rows: int = 24) -> str:
|
||||
"""The composed frame's bottom border rule — the line the caption is set into."""
|
||||
from rich.text import Text
|
||||
|
||||
from meshterm.ui.tui import frame
|
||||
|
||||
screen.note_viewport(max(1, rows - 4))
|
||||
composed = frame.compose_base(Text(""), screen, screen.footer_hint, cols, rows)
|
||||
return next(
|
||||
ln for ln in reversed([Text.from_ansi(x).plain for x in composed.split("\n")]) if "└" in ln
|
||||
)
|
||||
|
||||
|
||||
def _mark(screen: MapScreen, cols: int = 80) -> str:
|
||||
"""Wherever this platform puts the credit: the bottom rule, else the drawing's last row."""
|
||||
return _bottom_rule(screen, cols) if screen.bottom_caption else _last_row(screen, cols)
|
||||
|
||||
|
||||
def _both_platforms():
|
||||
"""The two platforms, restoring REGULAR afterwards — the credit differs across them."""
|
||||
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
||||
|
||||
try:
|
||||
for platform in (REGULAR, PICOCALC):
|
||||
set_platform(platform)
|
||||
yield platform
|
||||
finally:
|
||||
set_platform(REGULAR)
|
||||
|
||||
|
||||
def test_credit_names_openstreetmap_in_full_never_an_abbreviation() -> None:
|
||||
"""OSMF: "Attribution must be to 'OpenStreetMap'" — so neither form may abbreviate it."""
|
||||
from meshterm.ui import attribution
|
||||
@@ -2598,28 +2634,62 @@ def test_credit_names_openstreetmap_in_full_never_an_abbreviation() -> None:
|
||||
assert "Data from OpenStreetMap" in attribution.CREDIT_FULL
|
||||
|
||||
|
||||
def test_an_untouched_map_shows_the_whole_credit_in_its_bottom_right_corner() -> None:
|
||||
def test_an_untouched_map_shows_the_whole_credit_on_every_platform() -> None:
|
||||
"""The attribution may not be something you have to interact with the map to see."""
|
||||
from meshterm.ui import attribution
|
||||
|
||||
row = _last_row(_credited_map())
|
||||
assert row.endswith(attribution.CREDIT_FULL)
|
||||
assert cell_len(row) <= 80
|
||||
for platform in _both_platforms():
|
||||
mark = _mark(_credited_map(platform.readable_cols), platform.readable_cols)
|
||||
assert attribution.CREDIT_FULL in mark, platform.name
|
||||
|
||||
|
||||
def test_a_bordered_frame_sets_the_credit_in_its_bottom_rule_and_spares_the_drawing() -> None:
|
||||
"""JP, 2026-09-13: "that way it's not in the map" — the rule carries it, the map doesn't."""
|
||||
from meshterm.platforms import REGULAR, set_platform
|
||||
from meshterm.ui import attribution
|
||||
|
||||
set_platform(REGULAR)
|
||||
screen = _credited_map(72)
|
||||
# Right-justified with exactly one rule cell before the corner, as a title sits in the
|
||||
# top rule: "└──… © OpenMapTiles · Data from OpenStreetMap ─┘".
|
||||
rule = _bottom_rule(screen, 72)
|
||||
assert rule.startswith("└") and rule.endswith(f" {attribution.CREDIT_FULL} ─┘")
|
||||
assert cell_len(rule) == 72
|
||||
# And nothing of it reached the drawing: the map's own rows are pure picture.
|
||||
assert "OpenStreetMap" not in _plain(screen.render_body(72))
|
||||
|
||||
|
||||
def test_a_frame_with_no_bottom_rule_stamps_the_drawing_instead() -> None:
|
||||
"""The PicoCalc has a title bar and an F-key lane, and no rule to set a caption into."""
|
||||
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
||||
from meshterm.ui import attribution
|
||||
|
||||
try:
|
||||
set_platform(PICOCALC)
|
||||
screen = _credited_map(53)
|
||||
assert screen.bottom_caption == "" # nowhere to put it but the map
|
||||
assert _last_row(screen, 53).endswith(attribution.CREDIT_FULL)
|
||||
finally:
|
||||
set_platform(REGULAR)
|
||||
|
||||
|
||||
def test_the_credit_collapses_once_the_map_is_used_and_stays_collapsed() -> None:
|
||||
"""OSMF allows a collapse "automatically on map interaction such as panning … zooming"."""
|
||||
from meshterm.ui import attribution
|
||||
|
||||
for action in ("right", "pageup", "home", "text"):
|
||||
screen = _credited_map()
|
||||
assert _last_row(screen).endswith(attribution.CREDIT_FULL)
|
||||
screen.handle(action, "a" if action == "text" else "")
|
||||
row = _last_row(screen)
|
||||
assert row.endswith(attribution.CREDIT_SHORT), action
|
||||
assert attribution.CREDIT_FULL not in row, action
|
||||
screen.render_body(80) # a repaint never brings the whole line back
|
||||
assert _last_row(screen).endswith(attribution.CREDIT_SHORT), action
|
||||
for platform in _both_platforms():
|
||||
cols = platform.readable_cols
|
||||
for action in ("right", "pageup", "home", "text"):
|
||||
screen = _credited_map(cols)
|
||||
screen.render_body(cols) # the arrival paint the reader is answering
|
||||
assert attribution.CREDIT_FULL in _mark(screen, cols), (platform.name, action)
|
||||
screen.handle(action, "a" if action == "text" else "")
|
||||
mark = _mark(screen, cols)
|
||||
assert attribution.CREDIT_SHORT in mark, (platform.name, action)
|
||||
assert attribution.CREDIT_FULL not in mark, (platform.name, action)
|
||||
# A repaint never brings the whole line back — including through the frame's
|
||||
# own composition memo, which has to key on the caption to notice.
|
||||
assert attribution.CREDIT_FULL not in _mark(screen, cols), (platform.name, action)
|
||||
|
||||
|
||||
def test_a_reopened_map_arrives_with_the_whole_credit_again() -> None:
|
||||
@@ -2629,17 +2699,30 @@ def test_a_reopened_map_arrives_with_the_whole_credit_again() -> None:
|
||||
used = _credited_map()
|
||||
used.render_body(80) # keys do nothing until the map has a viewport to move
|
||||
used.handle("right")
|
||||
assert _last_row(used).endswith(attribution.CREDIT_SHORT)
|
||||
assert _last_row(_credited_map()).endswith(attribution.CREDIT_FULL)
|
||||
assert attribution.CREDIT_SHORT in _mark(used)
|
||||
assert attribution.CREDIT_FULL in _mark(_credited_map())
|
||||
|
||||
|
||||
def test_the_credit_costs_no_chrome_row_and_no_footer_character() -> None:
|
||||
"""It rides the drawing, so the body keeps its height and the hint keeps its wording."""
|
||||
screen = _credited_map()
|
||||
assert len(screen.render_body(80)) == len(_credited_map().render_body(80))
|
||||
assert "OpenStreetMap" not in screen.footer_hint
|
||||
screen.render_body(80)
|
||||
assert "OpenStreetMap" not in screen.title
|
||||
"""It rides the frame, so the body keeps its height and the hint keeps its wording."""
|
||||
for platform in _both_platforms():
|
||||
cols = platform.readable_cols
|
||||
screen = _credited_map(cols)
|
||||
assert len(screen.render_body(cols)) == len(_credited_map(cols).render_body(cols))
|
||||
assert "OpenStreetMap" not in screen.footer_hint
|
||||
screen.render_body(cols)
|
||||
assert "OpenStreetMap" not in screen.title
|
||||
|
||||
|
||||
def test_the_bottom_rule_keeps_its_clip_arrows_when_a_caption_shares_it() -> None:
|
||||
"""Neither half is dropped: the arrows lead, the caption follows, the run moves right."""
|
||||
from meshterm.ui.tui.frame import _panel_box
|
||||
|
||||
panel = _panel_box("Map", ["body"], True, True, "accent", "© OpenStreetMap")
|
||||
assert "↑↓ more · © OpenStreetMap" in (panel.subtitle or "")
|
||||
assert panel.subtitle_align == "right"
|
||||
# With no caption the arrows keep the centre they have always had.
|
||||
assert _panel_box("Map", ["body"], False, True, "accent", "").subtitle_align == "center"
|
||||
|
||||
|
||||
def test_the_find_query_and_the_credit_share_the_row_with_the_credit_kept_whole() -> None:
|
||||
@@ -2663,25 +2746,31 @@ def test_the_find_query_and_the_credit_share_the_row_with_the_credit_kept_whole(
|
||||
|
||||
|
||||
def test_the_location_preview_credits_the_basemap_in_its_short_form() -> None:
|
||||
"""A produced work too — and a non-interactive one, so its mark must stand still."""
|
||||
"""A produced work too — and a non-interactive one, so its mark must stand still.
|
||||
|
||||
The preview is embedded in somebody else's body, with no frame of its own to set a
|
||||
caption into, so it stamps on both platforms — unlike the full-screen map.
|
||||
"""
|
||||
from meshterm.ui import attribution
|
||||
from meshterm.ui.map_render import MapMarker
|
||||
from meshterm.ui.minimap import MiniMap
|
||||
|
||||
preview = MiniMap(
|
||||
_StubSession(72, 24),
|
||||
_StubSource(),
|
||||
14,
|
||||
center_lat=45.5,
|
||||
center_lon=-73.6,
|
||||
zoom=12,
|
||||
markers=[MapMarker("A", 45.5, -73.6)],
|
||||
)
|
||||
rows = preview.render(72, 6)
|
||||
assert len(rows) == 6 # the credit takes no row of its own
|
||||
last = _plain(rows).splitlines()[-1]
|
||||
assert last.endswith(attribution.CREDIT_SHORT)
|
||||
assert attribution.CREDIT_FULL not in last
|
||||
for platform in _both_platforms():
|
||||
cols = platform.readable_cols
|
||||
preview = MiniMap(
|
||||
_StubSession(cols, 24),
|
||||
_StubSource(),
|
||||
14,
|
||||
center_lat=45.5,
|
||||
center_lon=-73.6,
|
||||
zoom=12,
|
||||
markers=[MapMarker("A", 45.5, -73.6)],
|
||||
)
|
||||
rows = preview.render(cols, 6)
|
||||
assert len(rows) == 6, platform.name # the credit takes no row of its own
|
||||
last = _plain(rows).splitlines()[-1]
|
||||
assert last.endswith(attribution.CREDIT_SHORT), platform.name
|
||||
assert attribution.CREDIT_FULL not in last, platform.name
|
||||
|
||||
|
||||
def test_the_credit_glyphs_are_in_the_picocalc_console_font() -> None:
|
||||
|
||||
Reference in new issue
Block a user