The name is what the reader came to read, so the address is what gives

Capping the address lane stopped it eating the row, but left the old priority
in place underneath: DEVICE was still sized from whatever the address had
finished taking. Turn it around. The name is sized first, in full, and the
address takes what is left.

That is the right way round because of what each lane is for. A name is what
the reader recognises their own radio by; an address is a disambiguator, and a
disambiguator does its whole job from its last ten cells -- which is already
why the cut takes the front and keeps the tail. So the address shrinks to that
floor before the name gives up a single cell, and never reserves more than it
actually wants: a lone COM3 hands its slack to the name rather than sitting in
a ten-cell lane holding four.

The name still has to stop somewhere. A USB adapter's product string runs to
forty-odd characters, and a lane sized to that pushed everything after it off
the edge -- the row ended mid-port with HARDWARE not on screen at all. So
HARDWARE keeps its readable stub out of the budget, and past that the name
ellipsizes. Where it must, the address is handed back its full width instead
of being ellipsized alongside: the name is being cut whatever happens, and
spending two more of its cells leaves one cut in the row rather than two.

On a Mac the name now shows whole at 28 cells where it had six, and the 36-cell
UUID behind it draws as its last nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-19 00:56:34 -04:00
1 parent 1933d725b0
commit b1f21664ff
2 files changed
+97 -54

No files matched your search

+51 -27
View File
@@ -167,7 +167,7 @@ def _fit_target(text: str, width: int) -> str:
Args:
text: The target to fit.
width: The cells available. Text already inside it is returned unchanged, so the
platforms that never reach :data:`_ADDRESS_MAX` are untouched.
platforms whose targets already fit the lane are untouched.
Returns:
The target, or its tail behind a leading ``…``, measuring at most ``width`` cells.
@@ -700,43 +700,66 @@ async def _remove_network_device(
#: Below this a model string says nothing at all, and the lane is better spent on the name.
_HARDWARE_MIN = 10
#: The widest the PORT / ADDRESS lane may grow before it ellipsizes (see :func:`_fit_target`).
#: A MAC is 17 cells and ``/dev/ttyUSB0`` is 12, so Windows and Linux never reach this and
#: are untouched by it. macOS is why it exists: CoreBluetooth reports no MAC at all but a
#: per-machine 36-cell UUID, and even its serial ports run long
#: (``/dev/cu.Bluetooth-Incoming-Port`` is 31). Uncapped, one of those ate the row —
#: :func:`_name_width` sizes DEVICE from what this lane leaves, so a 36-cell address
#: squeezed the name down to the width of its own heading and the interesting columns with it.
_ADDRESS_MAX = 20
#: The narrowest the PORT / ADDRESS lane is worth shrinking to before DEVICE has to give.
#: This is the lane that adapts: a target is how you tell two similar rows apart, and ten
#: cells still does that (the tail of a UUID, the end of a port path), while a *name* is
#: what the reader actually came to read. macOS is why any of this is needed —
#: CoreBluetooth reports no MAC at all but a per-machine 36-cell UUID, and even its ports
#: run long (every Mac carries a 31-cell ``/dev/cu.Bluetooth-Incoming-Port``) — where a MAC
#: is 17 and ``/dev/ttyUSB0`` is 12, so no other platform ever presses on the row at all.
_ADDRESS_MIN = 10
def _name_width(
devices: list[DiscoveredDevice], registry: dict[str, RememberedDevice], other_lanes: int
) -> int:
"""The DEVICE lane's width: the longest name, capped so HARDWARE keeps a readable column.
def _lane_widths(
devices: list[DiscoveredDevice], registry: dict[str, RememberedDevice]
) -> tuple[int, int]:
"""The DEVICE and PORT / ADDRESS widths: the name in full, the address taking the rest.
A USB adapter's own product string runs to forty-odd characters ("CP2102 USB to UART
Bridge Controller"), and the lane used to size itself to whichever of those was longest.
On a 53-column console that pushed every other lane off the right-hand edge — the row
ended mid-port, and the HARDWARE column that says what the thing *is* was not on screen
at all, nor reachable by scrolling, because the pinned head was already wider than the
box. So the name is the lane that gives: it ellipsizes, and the columns after it survive.
The row has a fixed budget, and something has to give when the two lanes together
outrun it. **The address gives first.** A name is what the reader came to read and what
they recognise their own radio by; an address is a disambiguator, and a disambiguator
does its whole job from its last ten cells (:func:`_fit_target` keeps the tail for
exactly this reason). So DEVICE is sized to the longest name it has, and ADDRESS takes
whatever is left over, down to :data:`_ADDRESS_MIN`.
Only once the address is down to that floor does the name start ellipsizing, and it
still must: a USB adapter's own product string runs to forty-odd characters ("CP2102
USB to UART Bridge Controller"), and a lane sized to *that* pushed everything after it
off the right-hand edge — the row ended mid-port and the HARDWARE column that says what
the thing actually *is* was not on screen, nor reachable by scrolling, because the
pinned head was already wider than the box. HARDWARE keeps :data:`_HARDWARE_MIN` out of
the budget so it always has a readable stub; it is the row's scrolling tail, so beyond
that stub it is free to run long.
Args:
devices: The devices being listed.
registry: Confirmed companions, for their remembered names.
other_lanes: Cells the PORT and TYPE lanes take between them.
Returns:
The name lane's width in cells.
``(name_w, port_w)`` in cells.
"""
platform = get_platform()
# What the splash's box gives a row: the terminal, less the gutter it floats over, less
# its own border and padding, less the pointer and star columns the row leads with.
content = platform.readable_cols - platform.dialog_margin - 4 - 4
fixed = other_lanes + 6 # the three two-cell gaps between the four lanes
longest = max(cell_len(_display_name(d, registry)) for d in devices)
return max(len("DEVICE"), min(longest, content - fixed - _HARDWARE_MIN))
# ...less the three two-cell gaps, the TYPE badge, and HARDWARE's readable stub.
budget = content - 6 - len("TYPE") - _HARDWARE_MIN
name_natural = max(cell_len(_display_name(d, registry)) for d in devices)
port_natural = max(cell_len(_where(d)) for d in devices)
# Never reserve more room for the address than it actually wants: a lone "COM3" should
# hand its slack to the name rather than sit in a ten-cell lane holding four cells.
floor = min(port_natural, _ADDRESS_MIN)
name_w = max(len("DEVICE"), min(name_natural, budget - floor))
port_w = max(floor, min(port_natural, budget - name_w))
if name_w < name_natural and port_w < port_natural:
# The name is being ellipsized whatever happens, so spending two more of its cells
# to keep the address whole trades one cut for none rather than adding a second.
port_w = min(port_natural, budget - len("DEVICE"))
name_w = max(len("DEVICE"), budget - port_w)
return name_w, port_w
def _order(
@@ -804,12 +827,13 @@ def _build_items(
port_label = (
"PORT / ADDRESS" if has_serial and has_address else "ADDRESS" if has_address else "PORT"
)
port_w = min(max(cell_len(_where(d)) for d in devices), _ADDRESS_MAX)
port_w = max(port_w, len(port_label))
# The TYPE column holds a small transport badge (at most 3 cells); its heading is wider,
# so the four-cell "TYPE" label sets the column width and every badge pads out to it.
type_w = len("TYPE")
name_w = _name_width(devices, registry, port_w + type_w)
# DEVICE in full, ADDRESS taking what is left (see _lane_widths). The heading does not
# set a floor here the way it used to: the lane may end up narrower than "PORT / ADDRESS"
# spells, and column_header already carries the short form to fall back on.
name_w, port_w = _lane_widths(devices, registry)
hardware_w = max(cell_len(_hardware_label(d, registry)) for d in devices)
hardware_w = max(hardware_w, len("HARDWARE"))
+46 -27
View File
@@ -1323,42 +1323,61 @@ def test_device_picker_builds_aligned_columns(tmp_path) -> None:
assert device_rows[0].index("COM5") == device_rows[1].index("/dev/ttyUSB0")
def test_device_picker_address_lane_gives_way_to_the_interesting_columns() -> None:
"""A long target ellipsizes from the left instead of eating the DEVICE lane.
def test_device_picker_address_lane_is_the_one_that_adapts() -> None:
"""DEVICE shows its name in full; PORT / ADDRESS takes whatever is left.
macOS is why: CoreBluetooth reports no MAC but a per-machine 36-cell UUID, and even
its serial ports run long. Uncapped, one address sized the lane and ``_name_width``
sized DEVICE from what was left — so the name shrank to the width of its own heading
and took HARDWARE down with it. Cutting the *front* is what keeps the lane useful:
what tells two targets apart is their tail.
A name is what the reader came to read and recognises their own radio by; an address
is a disambiguator, and does that job from its last ten cells. macOS is what forced
the question — CoreBluetooth reports no MAC but a per-machine 36-cell UUID, and even
its ports run long — which sized the lane to 36 and left DEVICE the width of its own
heading.
"""
from rich.cells import cell_len
from meshterm.core.discovery import DiscoveredDevice
from meshterm.ui.device_picker import _ADDRESS_MAX, _fit_target, _name_width
from meshterm.ui.device_picker import _ADDRESS_MIN, _fit_target, _lane_widths
# Short targets are returned untouched, so Windows and Linux never notice the cap.
for short in ("COM3", "/dev/ttyUSB0", "C6:B6:26:BC:7F:09", "/dev/cu.usbmodem1101"):
assert _fit_target(short, _ADDRESS_MAX) == short
# A long one is cut at the head and fits the lane it was given.
name = "MeshCore-Johnputer Wardriver"
uuid = "12345678-1234-1234-1234-123456789ABC"
fitted = _fit_target(uuid, _ADDRESS_MAX)
assert cell_len(fitted) <= _ADDRESS_MAX
assert fitted.startswith("…") and fitted.endswith("123456789ABC")
# THE property the cut direction exists for: sibling ports stay tellable apart.
assert _fit_target("/dev/cu.usbmodem1101", 14) != _fit_target("/dev/cu.usbmodem1102", 14)
# And the lane the address was stealing comes back.
devices = [
DiscoveredDevice(transport="ble", address=uuid, name="MeshCore-Johnputer Wardriver"),
mac_devices = [
DiscoveredDevice(transport="ble", address="C6:B6:26:BC:7F:09", name=name, product=name),
DiscoveredDevice("COM3", product="Some Adapter", vid=0x1234),
]
uuid_devices = [
DiscoveredDevice(transport="ble", address=uuid, name=name, product=name),
DiscoveredDevice("/dev/cu.Bluetooth-Incoming-Port"),
]
uncapped = max(cell_len(d.target) for d in devices)
assert _name_width(devices, {}, min(uncapped, _ADDRESS_MAX) + 4) > _name_width(
devices, {}, uncapped + 4
)
# The name is shown whole on both, and a 36-cell address does not shrink it: the lane
# that gives is the address, and it never drops below the floor that keeps it useful.
for devices in (mac_devices, uuid_devices):
name_w, port_w = _lane_widths(devices, {})
assert name_w == cell_len(name)
assert port_w >= _ADDRESS_MIN
assert _lane_widths(mac_devices, {})[0] == _lane_widths(uuid_devices, {})[0]
# A target that fits is untouched, and a long one keeps its tail -- which is what tells
# two of them apart, and the whole reason the cut takes the front.
_, port_w = _lane_widths(uuid_devices, {})
assert _fit_target("COM3", port_w) == "COM3"
assert _fit_target(uuid, port_w).endswith("9ABC")
assert cell_len(_fit_target(uuid, port_w)) <= port_w
assert _fit_target("/dev/cu.usbmodem1101", 14) != _fit_target("/dev/cu.usbmodem1102", 14)
# Nothing is hoarded: a lone short port hands its slack back to the name.
short = [DiscoveredDevice("COM5", product="Wio SX1262", vid=0x2886)]
assert _lane_widths(short, {})[1] == cell_len("COM5")
# And where the name cannot fit whatever happens, the address is left whole rather
# than ellipsized alongside it -- one cut in the row instead of two.
sprawling = [
DiscoveredDevice(
"/dev/ttyUSB0", product="CP2102 USB to UART Bridge Controller", vid=0x10C4
)
]
name_w, port_w = _lane_widths(sprawling, {})
assert port_w == cell_len("/dev/ttyUSB0")
assert name_w < cell_len("CP2102 USB to UART Bridge Controller")
def test_device_picker_names_and_sorts_known_devices(tmp_path) -> None: