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,
|
keys=_SHORTCUTS,
|
||||||
key_hint=_shortcut_hint(len(store.hidden_ids())),
|
key_hint=_shortcut_hint(len(store.hidden_ids())),
|
||||||
live=keep_looking,
|
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
|
# 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.
|
# 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_text(cell)
|
||||||
row.append(" " * max(0, type_w - cell.cell_len))
|
row.append(" " * max(0, type_w - cell.cell_len))
|
||||||
row.append(" ")
|
row.append(" ")
|
||||||
# Everything up to here is the row's fixed lanes — the reader's place in the list —
|
# Nothing is pinned here, so ←→ slide the whole row. The Trophy case pins its
|
||||||
# so it is pinned and only what follows slides under ←→ (see Choice.hscroll_from).
|
# rank/date/score lanes because those always fit and only the walk overflows, which
|
||||||
# What follows is the hardware model and its tag, which is the part that runs long:
|
# makes them the reader's place in a long list. This list inverts that: on a narrow
|
||||||
# a firmware model string is as long as its vendor felt like making it.
|
# terminal it is the device name and the address that get cut, so pinning them would
|
||||||
head = row.cell_len
|
# 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")
|
row.append(_pad(_hardware_label(device, registry), hardware_w), style="muted")
|
||||||
# Only devices we've actually confirmed are billed as MeshCore companions; a USB
|
# 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
|
# 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
|
# 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
|
# 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.
|
# 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())
|
items.extend(_action_rows())
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ class Ui:
|
|||||||
keys: Mapping[str, Any] | None = None,
|
keys: Mapping[str, Any] | None = None,
|
||||||
key_hint: Callable[[Any], str] | None = None,
|
key_hint: Callable[[Any], str] | None = None,
|
||||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||||
|
hscroll: bool | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Choose one item on a chromeless startup splash; ``None`` if skipped.
|
"""Choose one item on a chromeless startup splash; ``None`` if skipped.
|
||||||
|
|
||||||
@@ -522,6 +523,7 @@ class PlainUi(Ui):
|
|||||||
keys: Mapping[str, Any] | None = None,
|
keys: Mapping[str, Any] | None = None,
|
||||||
key_hint: Callable[[Any], str] | None = None,
|
key_hint: Callable[[Any], str] | None = None,
|
||||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||||
|
hscroll: bool | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Unsupported in scripted CLI mode."""
|
"""Unsupported in scripted CLI mode."""
|
||||||
raise self._no_prompt()
|
raise self._no_prompt()
|
||||||
@@ -786,6 +788,7 @@ class TuiUi(Ui):
|
|||||||
keys: Mapping[str, Any] | None = None,
|
keys: Mapping[str, Any] | None = None,
|
||||||
key_hint: Callable[[Any], str] | None = None,
|
key_hint: Callable[[Any], str] | None = None,
|
||||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||||
|
hscroll: bool | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Delegate to the session's chromeless startup select splash."""
|
"""Delegate to the session's chromeless startup select splash."""
|
||||||
return await self.session.select_startup(
|
return await self.session.select_startup(
|
||||||
@@ -797,6 +800,7 @@ class TuiUi(Ui):
|
|||||||
keys=keys,
|
keys=keys,
|
||||||
key_hint=key_hint,
|
key_hint=key_hint,
|
||||||
live=live,
|
live=live,
|
||||||
|
hscroll=hscroll,
|
||||||
**({} if footer_hint is None else {"footer_hint": footer_hint}),
|
**({} if footer_hint is None else {"footer_hint": footer_hint}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -918,6 +918,7 @@ class TuiSession:
|
|||||||
keys: Mapping[str, Any] | None = None,
|
keys: Mapping[str, Any] | None = None,
|
||||||
key_hint: Callable[[Any], str] | None = None,
|
key_hint: Callable[[Any], str] | None = None,
|
||||||
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
live: Callable[[Callable[[list], None]], Awaitable[None]] | None = None,
|
||||||
|
hscroll: bool | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Show a chromeless select splash (banner above a content-sized box).
|
"""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
|
nothing outlives the list it was redrawing and no repaint lands on a dialog that
|
||||||
has since opened over it.
|
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
|
``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`) —
|
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,
|
and ``key_hint`` says what they are called on the row the highlight is standing on,
|
||||||
@@ -958,6 +963,7 @@ class TuiSession:
|
|||||||
filterable=False,
|
filterable=False,
|
||||||
keys=keys,
|
keys=keys,
|
||||||
key_hint=key_hint,
|
key_hint=key_hint,
|
||||||
|
hscroll=hscroll,
|
||||||
)
|
)
|
||||||
screen.chrome = False
|
screen.chrome = False
|
||||||
screen.banner = banner
|
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:
|
def test_the_splash_scrolls_the_whole_row_it_is_on() -> None:
|
||||||
"""←→ read a long firmware model to its end; the lanes in front of it stay put."""
|
"""←→ 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.device_store import RememberedDevice
|
||||||
from meshterm.core.discovery import serial_device
|
from meshterm.core.discovery import serial_device
|
||||||
from meshterm.platforms import PICOCALC, REGULAR, set_platform
|
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)):
|
for platform, width in ((REGULAR, 66), (PICOCALC, 47)):
|
||||||
set_platform(platform)
|
set_platform(platform)
|
||||||
items = _build_items([radio], registry[radio.stable_id], registry)
|
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))
|
row = next(it for it in items if isinstance(it, Choice))
|
||||||
assert row.hscroll_from > 0 # the fixed lanes are pinned...
|
assert not row.hscroll_from, "no lane is pinned on the splash any more"
|
||||||
assert row.hscroll_from < width # ...and leave something to scroll into
|
|
||||||
|
|
||||||
before = _plain(screen.render_body(width)[1])
|
before = _plain(screen.render_body(width)[1])
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
screen.handle("right")
|
screen.handle("right")
|
||||||
after = _plain(screen.render_body(width)[1])
|
after = _plain(screen.render_body(width)[1])
|
||||||
assert before != after, platform.name
|
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:
|
finally:
|
||||||
set_platform(REGULAR)
|
set_platform(REGULAR)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user