diff --git a/meshterm/tools/devices.py b/meshterm/tools/devices.py index 62b1515..ace3ace 100644 --- a/meshterm/tools/devices.py +++ b/meshterm/tools/devices.py @@ -153,6 +153,10 @@ def _listing(devices: list, known: dict, remembered: object, active_target: str columns=( fields.word("target", "TARGET"), fields.word("transport", "TRANSPORT"), + # The two halves `target` is one of, kept apart for a caller that has to know + # which transport it is holding without parsing the target for a colon. + fields.hidden("port"), + fields.hidden("address"), fields.name("label", "NAME"), fields.name("hardware", "HARDWARE"), fields.word("meshcore", "MESHCORE"), diff --git a/meshterm/ui/renderers.py b/meshterm/ui/renderers.py index ceba83f..cbb5d52 100644 --- a/meshterm/ui/renderers.py +++ b/meshterm/ui/renderers.py @@ -100,14 +100,22 @@ class PlainRenderer(Renderer): self.console = console def render(self, report: Report | None) -> None: - """Draw each block in order, one blank line between them.""" - for index, block in enumerate(report or ()): + """Draw each block in order, one blank line between them. + + The separator is counted off what was *drawn* rather than off the block index: a + block can render to nothing (a listing with no rows, an acknowledgement whose plain + answer is its exit status), and a blank line above the first visible block would be + a line of output the command did not have. + """ + drawn_any = False + for block in report or (): drawn = self._block(block) if drawn is None: continue - if index: + if drawn_any: self.console.print(script.blank()) self.console.print(drawn) + drawn_any = True def _block(self, block: Block) -> Any: """The renderable for one block, or ``None`` where it prints nothing.""" diff --git a/tests/test_report.py b/tests/test_report.py index b911949..9906999 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -270,6 +270,24 @@ def test_a_block_drawn_only_for_a_person_never_reaches_the_document() -> None: assert _json(facts, summary) == {"packets": 2} +def test_a_block_that_prints_nothing_leaves_no_blank_line_where_it_stood() -> None: + """The separator is counted off what was drawn, not off the block index. + + ``config restore --dry-run`` states its outcome silently and its plan as a listing, and + counting positions instead opened the command with an empty line — output it did not + have, in whatever was reading it. + """ + silent = Facts( + key="restore", fields=(fields.flag("dry_run", "dry_run"),), values={}, shape=SILENT + ) + plan = Listing( + key="operations", + columns=(fields.word("operation", "OPERATION"),), + rows=[{"operation": "set"}], + ) + assert _plain(silent, plan).splitlines()[0] == "OPERATION" + + # -- the document's own rules ----------------------------------------------------------