CLI: strip the framing from scripted output, keep a legible splash

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 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Haiku 4.5 committed 2026-07-14 01:44:26 -04:00
1 parent d977aeb22a
commit 216a6549f5
2 files changed
+106 -5

No files matched your search

+34
View File
@@ -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",
)
+72 -5
View File
@@ -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."""