The splash slides the whole row, because the whole row is what gets cut
The device rows pinned their fixed lanes and slid only the hardware model underneath them. The reasoning was borrowed from the Trophy case, where the rank, date and score always fit and only the walk overflows -- there the lanes really are the reader's place in a long list, and sliding them off would cost the row its identity to read something the columns already told you. This list inverts that. On a narrow terminal it is the device name and the address that get cut, so pinning them pinned the truncation in place: the one thing the arrow keys could not reach was the text somebody most wanted to finish reading. A name is not a landmark when it is the thing being elided. So no lane is pinned, and the row slides whole. That needed the scroll to be asked for outright, because what normally turns it on is a row pinning a head block, and none of these do any more -- `hscroll` travels from the picker through the splash surface to the list, the same path `live` took. The test that covered the old behaviour is rewritten rather than removed. It was a fair test of a decision that has changed, and the argument for the new one belongs where the old one was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
551a86f47a
commit
2dbae7af77
4 files changed
+41
-12
No files matched your search
@@ -442,6 +442,9 @@ async def prompt_device(
|
||||
keys=_SHORTCUTS,
|
||||
key_hint=_shortcut_hint(len(store.hidden_ids())),
|
||||
live=keep_looking,
|
||||
# Said outright because the rows no longer imply it: scrolling is switched on by
|
||||
# a row pinning a head block, and none of these do any more.
|
||||
hscroll=True,
|
||||
)
|
||||
# Esc (``None``) and the Quit row both mean "leave the picker" — surface that to the
|
||||
# caller as ``None`` so it can exit the program instead of continuing device-less.
|
||||
@@ -949,11 +952,12 @@ def _build_items(
|
||||
row.append_text(cell)
|
||||
row.append(" " * max(0, type_w - cell.cell_len))
|
||||
row.append(" ")
|
||||
# Everything up to here is the row's fixed lanes — the reader's place in the list —
|
||||
# so it is pinned and only what follows slides under ←→ (see Choice.hscroll_from).
|
||||
# What follows is the hardware model and its tag, which is the part that runs long:
|
||||
# a firmware model string is as long as its vendor felt like making it.
|
||||
head = row.cell_len
|
||||
# Nothing is pinned here, so ←→ slide the whole row. The Trophy case pins its
|
||||
# rank/date/score lanes because those always fit and only the walk overflows, which
|
||||
# makes them the reader's place in a long list. This list inverts that: on a narrow
|
||||
# terminal it is the device name and the address that get cut, so pinning them would
|
||||
# pin the truncation in place and leave the one thing ←→ could not reach being the
|
||||
# text somebody most wants to finish reading.
|
||||
row.append(_pad(_hardware_label(device, registry), hardware_w), style="muted")
|
||||
# Only devices we've actually confirmed are billed as MeshCore companions; a USB
|
||||
# vendor ID (or a BLE advert) is a sort hint, not a claim. A bare serial bridge earns
|
||||
@@ -973,7 +977,7 @@ def _build_items(
|
||||
# Only a network device opts into Delete-to-remove: it's listed solely from its
|
||||
# remembered endpoint, so forgetting it is the only way it leaves the picker. A scanned
|
||||
# serial/BLE device would just reappear, so Delete stays inert on those rows.
|
||||
items.append(Choice(title=row, value=device, deletable=device.is_tcp, hscroll_from=head))
|
||||
items.append(Choice(title=row, value=device, deletable=device.is_tcp))
|
||||
items.extend(_action_rows())
|
||||
return items
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ class Ui:
|
||||
keys: Mapping[str, Any] | None = None,
|
||||
key_hint: Callable[[Any], str] | None = None,
|
||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||
hscroll: bool | None = None,
|
||||
) -> Any:
|
||||
"""Choose one item on a chromeless startup splash; ``None`` if skipped.
|
||||
|
||||
@@ -522,6 +523,7 @@ class PlainUi(Ui):
|
||||
keys: Mapping[str, Any] | None = None,
|
||||
key_hint: Callable[[Any], str] | None = None,
|
||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||
hscroll: bool | None = None,
|
||||
) -> Any:
|
||||
"""Unsupported in scripted CLI mode."""
|
||||
raise self._no_prompt()
|
||||
@@ -786,6 +788,7 @@ class TuiUi(Ui):
|
||||
keys: Mapping[str, Any] | None = None,
|
||||
key_hint: Callable[[Any], str] | None = None,
|
||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||
hscroll: bool | None = None,
|
||||
) -> Any:
|
||||
"""Delegate to the session's chromeless startup select splash."""
|
||||
return await self.session.select_startup(
|
||||
@@ -797,6 +800,7 @@ class TuiUi(Ui):
|
||||
keys=keys,
|
||||
key_hint=key_hint,
|
||||
live=live,
|
||||
hscroll=hscroll,
|
||||
**({} if footer_hint is None else {"footer_hint": footer_hint}),
|
||||
)
|
||||
|
||||
|
||||
@@ -918,6 +918,7 @@ class TuiSession:
|
||||
keys: Mapping[str, Any] | None = None,
|
||||
key_hint: Callable[[Any], str] | None = None,
|
||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||
hscroll: bool | None = None,
|
||||
) -> Any:
|
||||
"""Show a chromeless select splash (banner above a content-sized box).
|
||||
|
||||
@@ -941,6 +942,10 @@ class TuiSession:
|
||||
nothing outlives the list it was redrawing and no repaint lands on a dialog that
|
||||
has since opened over it.
|
||||
|
||||
``hscroll`` overrules what the rows imply: a splash whose rows pin no head block
|
||||
still wants ←→ to read a long row to its end, and a row that pins nothing cannot
|
||||
ask for that on its own (see :class:`~meshterm.ui.tui.select.SelectScreen`).
|
||||
|
||||
``keys`` hands the list bare-key shortcuts — which a splash can afford precisely
|
||||
because it does not filter, so every letter is free (see :class:`SelectScreen`) —
|
||||
and ``key_hint`` says what they are called on the row the highlight is standing on,
|
||||
@@ -958,6 +963,7 @@ class TuiSession:
|
||||
filterable=False,
|
||||
keys=keys,
|
||||
key_hint=key_hint,
|
||||
hscroll=hscroll,
|
||||
)
|
||||
screen.chrome = False
|
||||
screen.banner = banner
|
||||
|
||||
+21
-6
@@ -3344,8 +3344,18 @@ def test_a_hint_too_long_for_its_box_drops_atoms_rather_than_its_tail() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_the_splash_scrolls_the_hardware_column_of_the_row_it_is_on() -> None:
|
||||
"""←→ read a long firmware model to its end; the lanes in front of it stay put."""
|
||||
def test_the_splash_scrolls_the_whole_row_it_is_on() -> None:
|
||||
"""←→ move the entire row, lanes included, rather than sliding the tail under them.
|
||||
|
||||
The Trophy case pins its rank/date/score lanes because those always fit and only the
|
||||
walk overflows, so they are the reader's place in a long list. This list inverts that:
|
||||
on a narrow terminal it is the device name and the address that get cut, so pinning
|
||||
them would pin the truncation in place and the one thing ←→ could not reach would be
|
||||
the text somebody most wants to finish reading.
|
||||
|
||||
Scrolling has to be asked for outright here, because it is a row pinning a head block
|
||||
that normally turns it on and no row does now.
|
||||
"""
|
||||
from meshterm.core.device_store import RememberedDevice
|
||||
from meshterm.core.discovery import serial_device
|
||||
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
||||
@@ -3367,17 +3377,22 @@ def test_the_splash_scrolls_the_hardware_column_of_the_row_it_is_on() -> None:
|
||||
for platform, width in ((REGULAR, 66), (PICOCALC, 47)):
|
||||
set_platform(platform)
|
||||
items = _build_items([radio], registry[radio.stable_id], registry)
|
||||
screen = SelectScreen("Select a companion device", items, filterable=False)
|
||||
screen = SelectScreen(
|
||||
"Select a companion device", items, filterable=False, hscroll=True
|
||||
)
|
||||
row = next(it for it in items if isinstance(it, Choice))
|
||||
assert row.hscroll_from > 0 # the fixed lanes are pinned...
|
||||
assert row.hscroll_from < width # ...and leave something to scroll into
|
||||
assert not row.hscroll_from, "no lane is pinned on the splash any more"
|
||||
|
||||
before = _plain(screen.render_body(width)[1])
|
||||
for _ in range(3):
|
||||
screen.handle("right")
|
||||
after = _plain(screen.render_body(width)[1])
|
||||
assert before != after, platform.name
|
||||
assert after.startswith(before[: row.hscroll_from - 4]), "the lanes moved"
|
||||
# The head moved with everything else: what was at the left edge is gone.
|
||||
assert not after.startswith(before[:8]), f"the lanes stayed put on {platform.name}"
|
||||
# And the name is what scrolled out of view, which is the point -- it is the
|
||||
# part a narrow terminal cuts, so it has to be reachable.
|
||||
assert "Wardriver" in before
|
||||
finally:
|
||||
set_platform(REGULAR)
|
||||
|
||||
|
||||
Reference in new issue
Block a user