diff --git a/docs/memories/gf_routing_and_carriers.md b/docs/memories/gf_routing_and_carriers.md index da5b78c1..35137885 100644 --- a/docs/memories/gf_routing_and_carriers.md +++ b/docs/memories/gf_routing_and_carriers.md @@ -324,4 +324,6 @@ The Matrix table prints the same line through `_print_matrix_carrier_legend`, naming each code by Matrix's `shortName`. Both go through `_print_carrier_legend_line`, which collapses the whitespace runs of an entry, a newline and a tab included, to one space, so a name cannot break the legend over -two lines. +two lines. When the legend outgrows the console it breaks only between entries, +never between a code and its name; Rich breaks at any space, a no-break space +included, so `_print_carrier_legend_line` picks the break itself. diff --git a/src/flight_cli/cli.py b/src/flight_cli/cli.py index 7bdafa2f..4d88a173 100644 --- a/src/flight_cli/cli.py +++ b/src/flight_cli/cli.py @@ -5069,12 +5069,28 @@ def _render_search( def _print_carrier_legend_line(named: Mapping[str, str | None]) -> None: - """The one `Carriers:` line both tables print, each named code once. An entry's + """The `Carriers:` legend both tables print, each named code once. An entry's whitespace runs, a newline and a tab included, collapse to one space so a name - cannot break the legend over two lines; a table with no named code prints none.""" - if any(named.values()): - entries = (" ".join(f"{code} {name}".split()) for code, name in named.items() if name) - console.print("[dim]Carriers: " + _safe_text(" · ".join(entries)) + "[/]") + cannot break the legend over two lines; a table with no named code prints none. + When it outgrows the console it breaks only between entries, never between a + code and its name: Rich breaks at any space, a no-break space included, so the + break is chosen here, and an entry wider than the console is left for Rich to fold.""" + entries = [ + " ".join(f"{code} {name}".translate(_CTRL).split()) for code, name in named.items() if name + ] + entries = [entry for entry in entries if entry] + if not entries: + return + # The prefix counts toward the first line, so the first entry is measured too. + lines = ["Carriers:"] + for i, entry in enumerate(entries): + piece = entry + (" ·" if i < len(entries) - 1 else "") + if cell_len(lines[-1] + " " + piece) <= console.width: + lines[-1] += " " + piece + else: + lines.append(piece) + for line in lines: + console.print("[dim]" + _safe_text(line) + "[/]") def _print_matrix_carrier_legend(shown: Sequence[Itinerary]) -> None: diff --git a/tests/test_carrier_legend_docs.py b/tests/test_carrier_legend_docs.py index 27c4f0e0..348790da 100644 --- a/tests/test_carrier_legend_docs.py +++ b/tests/test_carrier_legend_docs.py @@ -14,3 +14,13 @@ def test_memo_says_a_legend_name_cannot_break_the_line() -> None: "collapses the whitespace runs of an entry, a newline and a tab included, to one " "space, so a name cannot break the legend over two lines" ) in memo + + +def test_memo_says_a_wrapped_legend_breaks_only_between_entries() -> None: + text = (_ROOT / "docs/memories/gf_routing_and_carriers.md").read_text() + memo = " ".join(text.split()) + assert ( + "When the legend outgrows the console it breaks only between entries, never between a " + "code and its name; Rich breaks at any space, a no-break space included, so " + "`_print_carrier_legend_line` picks the break itself." + ) in memo diff --git a/tests/test_gf_carrier_legend.py b/tests/test_gf_carrier_legend.py index c55b598e..738a0af4 100644 --- a/tests/test_gf_carrier_legend.py +++ b/tests/test_gf_carrier_legend.py @@ -195,6 +195,31 @@ def test_a_narrow_console_wraps_the_legend_within_its_width( assert _LEGEND in " ".join(" ".join(lines).split()) +def test_a_wrapping_legend_keeps_each_code_with_its_name( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """At 40 columns Rich alone ends the first line on `F9`, apart from its name.""" + lines = _printed(monkeypatch, [_row(("MX", "100")), _row(("F9", "200"))], width=40) + start = next(i for i, ln in enumerate(lines) if ln.startswith("Carriers:")) + assert [ln.rstrip() for ln in lines[start:]] == [ + "Carriers: MX Breeze Airways ·", + "F9 Frontier Airlines", + ] + + +def test_a_legend_entry_that_fits_the_console_is_never_split_the_first_included( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """`LH Lufthansa ·` fits 14 columns; with `Carriers: ` before it, it needs 24.""" + split: list[int] = [] + for width in range(14, 61): + lines = _printed(monkeypatch, [_row(("LH", "1")), _row(("B6", "2"))], width=width) + legend = lines[next(i for i, ln in enumerate(lines) if ln.startswith("Carriers:")) :] + if not all(any(e in ln for ln in legend) for e in ("LH Lufthansa", "B6 JetBlue")): + split.append(width) + assert split == [] + + def test_a_search_prints_the_legend_under_its_table(gf_session: Callable[..., Any]) -> None: body = _answering( _ds1("ds1_jfk_lhr_tfu.json"), origin="JFK", destination="LHR", date=_DEP.isoformat()