From 216a6549f59c6a1f059dc6b8c705f3089b90f0bf Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Tue, 14 Jul 2026 01:44:26 -0400 Subject: [PATCH] CLI: strip the framing from scripted output, keep a legible splash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scripted runs print straight to the terminal, so a tool's heavy Rich framing — a Panel's border, a Table's full box, Typer's boxed help panels — is chrome between the reader and the data. Lighten it to a middle ground: PlainUi de-frames tool output (Panel/Table titles become bold-accent headings, tables keep just a single rule under their header), and the Typer help drops its rounded Options/Commands/Error panels for the same heading-over-content shape while keeping rich mode's colour (yellow usage, cyan options, green switches). Functional, easy to scan, none of the interactive menu's weight. The full-screen TUI is untouched. Co-Authored-By: Claude Haiku 4.5 --- meshterm/cli.py | 34 +++++++++++++++++++ meshterm/ui/surface.py | 77 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/meshterm/cli.py b/meshterm/cli.py index 8ce6d57..50f7ba0 100644 --- a/meshterm/cli.py +++ b/meshterm/cli.py @@ -28,9 +28,43 @@ from .tools import all_tools from .tools.base import Tool, ToolResult from .ui.theme import make_console +def _unframe_help_panels() -> None: + """Render Typer's help/error sections as coloured headings instead of boxed panels. + + Typer's rich help gives us the colour we want — yellow usage, cyan options, green + switches — but wraps each Options/Commands/Error section in a rounded :class:`Panel`, + the boxed framing scripted output is meant to be free of. There's no constant to drop + that box, so we swap the ``Panel`` the help formatter calls for a thin stand-in that + keeps the colour and the section title (as a bold heading, indigo like the app accent, + red for errors) but no border — the same heading-over-content shape tool results use + (see :func:`~meshterm.ui.surface._deframe`). The inner tables are already box-less. + """ + import typer.rich_utils as rich_utils + from rich.console import Group + from rich.text import Text + + def _bare_panel( + renderable: object, + *, + title: object = None, + border_style: str = "", + **_: object, + ) -> Group: + style = "bold red" if border_style == "red" else "bold #818cf8" + heading = Text(str(title), style=style) if title else Text("") + return Group(Text(""), heading, renderable) # type: ignore[list-item] + + rich_utils.Panel = _bare_panel # type: ignore[assignment,misc] + + +_unframe_help_panels() + app = typer.Typer( add_completion=False, no_args_is_help=False, + # Rich help for its colour (usage, option flags, metavars), but with the boxed + # section panels flattened to plain headings by _unframe_help_panels() above — a + # splash of colour, none of the framing. rich_markup_mode="rich", help="MeshTerm — a modern toolkit for tuning and exploring your MeshCore mesh", ) diff --git a/meshterm/ui/surface.py b/meshterm/ui/surface.py index 5780048..b07c058 100644 --- a/meshterm/ui/surface.py +++ b/meshterm/ui/surface.py @@ -20,7 +20,10 @@ from __future__ import annotations from contextlib import asynccontextmanager from typing import Any, AsyncIterator, Callable, Optional +from rich import box from rich.console import Console, Group, RenderableType +from rich.panel import Panel +from rich.table import Table from rich.text import Text from .tui.session import TuiSession @@ -236,8 +239,70 @@ class Ui: raise NotImplementedError +def _heading(title: "str | Text") -> Text: + """Render a Panel's or Table's hoisted title as an accent heading line. + + The ``accent`` theme style (bold indigo) is the one bit of emphasis that earns its + keep here: it marks where each block starts so a scripted run stays scannable, without + reaching for the interactive menu's framing. It must stay a bare theme name — Rich + can't combine the ``bold`` keyword with a named style in one string — but ``accent`` + already carries bold, so nothing is lost. + """ + text = title if isinstance(title, Text) else Text.from_markup(str(title)) + text.style = "accent" + return text + + +def _deframe(renderable: RenderableType) -> list[RenderableType]: + """Strip a renderable's chrome for direct terminal output. + + The scripted CLI prints straight to the terminal, with no result window to hold the + output, so a tool's heavy Rich framing — a :class:`Panel`'s border, a :class:`Table`'s + full box and centered title — is chrome between the reader and the data. This lightens + it to the middle ground: enough styling to stay legible, none of the interactive menu's + weight. A Panel becomes its title as a plain heading above its (also de-framed) body; a + Table keeps just a single rule under its header (:data:`~rich.box.SIMPLE_HEAD`) so the + columns still read, and hoists its title to the same plain heading; a :class:`Group` is + de-framed member by member. Everything else passes through untouched. + + The tables and panels are built fresh for each scripted run and discarded after + printing, so mutating them in place (clearing ``box``/``title``) is safe here. + + Args: + renderable: A renderable a tool handed to :meth:`PlainUi.show` or :meth:`view`. + + Returns: + The frame-light renderables to print, in order. + """ + if isinstance(renderable, Panel): + out: list[RenderableType] = [] + if renderable.title: + out.append(_heading(renderable.title)) + out.extend(_deframe(renderable.renderable)) + return out + if isinstance(renderable, Table): + out = [] + if renderable.title: + out.append(_heading(renderable.title)) + renderable.title = None + renderable.box = box.SIMPLE_HEAD + out.append(renderable) + return out + if isinstance(renderable, Group): + out = [] + for child in renderable.renderables: + out.extend(_deframe(child)) + return out + return [renderable] + + class PlainUi(Ui): - """CLI surface: print directly to the console; interactive prompts are unsupported.""" + """CLI surface: print directly to the console; interactive prompts are unsupported. + + Output is de-framed on the way out (see :func:`_deframe`): a scripted run prints + to the bare terminal, so a tool's Panels and boxed Tables are lightened to plain + headings and header-ruled columns — legible and easy to scan, without the framing. + """ def __init__(self, console: Console) -> None: """Bind the surface to a Rich console. @@ -248,9 +313,10 @@ class PlainUi(Ui): self.console = console def show(self, *renderables: RenderableType) -> None: - """Print each renderable to the console immediately.""" + """Print each renderable to the console immediately, de-framed (see :func:`_deframe`).""" for renderable in renderables: - self.console.print(renderable) + for item in _deframe(renderable): + self.console.print(item) def note(self, markup: str) -> None: """Print a markup line to the console immediately.""" @@ -259,8 +325,9 @@ class PlainUi(Ui): async def view( self, renderable: RenderableType, *, title: str = "", footer_hint: str = "" ) -> None: - """Print the renderable immediately (there is no windowing in CLI mode).""" - self.console.print(renderable) + """Print the renderable immediately, de-framed (there is no windowing in CLI mode).""" + for item in _deframe(renderable): + self.console.print(item) def progress(self, title: str = "Working"): # noqa: ANN201 """Return the Rich progress bar used for scripted runs."""