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:
1 parent
d977aeb22a
commit
216a6549f5
2 files changed
+106
-5
No files matched your search
@@ -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
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user