rich-terminal-render
A deterministic terminal-layout engine, built by bundling the parts of
Textualize/rich that contain no
third-party dependencies. It runs with no pip install at all.
When to use this skill
Use it whenever text has to line up in a monospace context, or whenever a
terminal-style artifact has to be produced:
- Producing an aligned ASCII/box-drawing table, panel or report
- Any layout containing CJK, Hangul, emoji, or combining marks
- Checking whether an existing block of text is display-aligned (before sending
it, committing it, or pasting it into a document) - Stripping ANSI escape sequences from captured command output
- Turning structured data into a terminal-style standalone SVG or HTML
artifact - Advising on, or writing, code that uses rich itself →
references/api-cheatsheet.md
The core reason this skill exists: len() is the wrong measure for terminal
width. len("中文") == 2 but it occupies 4 cells, so str.ljust() on CJK text
silently produces columns that drift. An LLM cannot compute this reliably by
inspection; the Unicode table can. See references/cjk-width.md.
Quick start
Everything goes through one CLI. PY below is any Python ≥ 3.9.
PY=python
CLI="<skill-root>/scripts/rich_terminal_render.py"
# 1. how wide is this, really?
$PY $CLI width "贵州茅台 MSFT 👍" --chars
$PY $CLI width --file titles.txt # one line per line, in cells
# 2. is this block aligned?
$PY $CLI align --file report.txt # exit 1 if any line differs
$PY $CLI align --width 40 --json < report.txt
# 3. render an aligned table from JSON
$PY $CLI table --title "板块资金流" '{"columns":[{"header":"板块"},{"header":"净流入","justify":"right"}],"rows":[["半导体","+18.42 亿"],["白酒","-6.10 亿"]]}'
# 4. ...or straight from CSV on stdin
$PY $CLI table --csv < data.csv --title 板块 --check-alignment
# 5. strip ANSI (normalises CRLF as a side effect)
printf '\033[31mred\033[0m\r\n' | $PY $CLI strip-ansi
# 6. export a terminal-style artifact
$PY $CLI export --csv < data.csv --title 板块 --out svg --write table.svg
$PY $CLI export --csv < data.csv --title 板块 --out html --write table.html
# 7. what is in this engine / does it work
$PY $CLI box --list
$PY $CLI info
$PY $CLI selftest
$PY $CLI demo --check-alignment
Useful flags: --json on width / align / info for machine-readable
output, --check-alignment on table / demo to fail loudly if a rendered
block is not rectangular, --box <STYLE> (19 styles), --width <N> to override
the terminal width.
Using the engine as a library
The bundled package keeps rich's own import paths, so existing rich code works
unchanged:
import sys; sys.path.insert(0, "<skill-root>")
from rich.cells import cell_len, set_cell_size
from rich.console import Console
from rich.table import Table
cell_len("贵州茅台") # 8, not 4
set_cell_size("中文", 8) # padded to exactly 8 cells
What is in scope, and what is not
| Capability | Status |
|---|---|
| Cell width for CJK / Hangul / emoji / ZWJ / combining marks | ✅ |
Console, Text, Table, Panel, Tree, Columns, Align, Padding, Rule, Layout, Live, Progress, Status, Spinner, Logging, Pretty, Inspect, markup, styles, themes, box drawing |
✅ |
ANSI decoding (Text.from_ansi, rich.ansi) |
✅ |
export_text / export_html / export_svg |
✅ |
| Unicode width tables, 21 versions (4.1.0 → 17.0.0), selectable | ✅ |
| Win32 legacy console backend | ✅ (covered by tests on Windows) |
Syntax highlighting (rich.syntax) |
❌ needs pygments |
Rich tracebacks (rich.traceback) |
❌ needs pygments |
Markdown rendering (rich.markdown) |
❌ needs markdown-it-py |
The three missing modules are present as stubs that raise a descriptive
ImportError telling the caller to pip install rich. Nothing else in the
package reaches for a third-party import — that claim is enforced by an executable
test, not by convention.
Verification
python scripts/run_tests.py # full suite
python scripts/run_tests.py --suite test_cells -v
python scripts/run_tests.py --rich-path <dir> # baseline against pristine upstream
node tests/pay-flow.test.mjs # 402 end-to-end suite
python scripts/rich_terminal_render.py selftest
Measured on Python 3.13.14 with pygments / markdown-it-py / attrs / pytest all
absent:
| vendored upstream suites | 57 (+1 local suite) |
| passed | 910 |
| failed | 0 |
| blocked (out-of-scope module) | 2 |
skipped (upstream's own skipif) |
16 |
node tests/pay-flow.test.mjs (Node 22.22.2, local mock gateway, no real
account or keys): 113 assertions passing — config validation, the judging
engine, the full 402 → pay → 200 flow, idempotent replay, byte-identical reports
across two separate payments, and the negative cases (tampered amount,
active=false, resource mismatch, forged proof, reused trade_no, broken
gateway signature, payload changed after payment, a single modified cell,
changed render width, raw control characters, 12 structural rejection paths,
report escaping and zero external references).
The upstream test files are byte-identical to Textualize/rich@main; only
the harness around them is local (scripts/_mini_pytest.py implements the four
pytest features upstream actually uses: mark, raises, fixture, fail).
Argument binding reproduces pytest's own rule for unittest.mock.patch
decorators, taken from _pytest/compat.py.
Same suites against unmodified upstream rich produce an identical per-suite
result, which is the evidence that bundling changed no behaviour.
Optional hosted service: Alipay AI pay-per-use (HTTP 402)
The local capabilities above are free, offline, and dependency-free and need
nothing else. service/ additionally ships an optional hosted service that
meters one resource: POST /render-table — submit a table spec, receive a
self-contained HTML alignment report (rendered table + per-check verdicts +
column width breakdown + per-line display widths).
It implements the Alipay AI pay-per-use protocol end to end:
| Step | Behaviour |
|---|---|
No Payment-Proof |
402 Payment Required with a Payment-Needed header carrying a Base64URL bill, RSA2-signed locally by the merchant, plus a readable body (out_trade_no / amount / resource_id / goods_name) |
With Payment-Proof |
calls alipay.aipay.agent.payment.verify, then strictly checks active / trade_no / amount / out_trade_no / resource_id / order state |
| Generating the resource | renders the table in a Python subprocess, stores the report atomically (one report per order) |
| Confirming fulfilment | calls alipay.aipay.agent.fulfillment.confirm; only then marks the order fulfilled |
| Delivering | 200 with a Payment-Validation header and the report |
Any failed verification check returns 402 and never releases the resource.
Format and size validation happen before payment, so an oversized request
returns 400 and never creates an order.
- Agent-side behaviour guide:
references/pay-per-use.md - Server config and operations:
service/README.md - Environment variables:
service/env.example.md - Local end-to-end test (no real account or keys needed):
node tests/pay-flow.test.mjs
→ 113 assertions passing
Deployment is what makes it live: an Alipay enterprise account with a signed
service-market agreement, production keys, the production gateway, and the
hardening in service/README.md. Local integration testing proves the protocol
logic only; it does not mean the service is production-ready.
Node ≥ 22 is required only for this optional service (the order store uses
node:sqlite). The local CLI needs nothing but a Python 3 interpreter.
Layout
SKILL.md this file
LICENSE.md MIT (upstream copyright preserved)
THIRD_PARTY_NOTICES.md rich, wcwidth, Unicode data
rich/ the engine -- 99 files, 95 byte-identical to upstream
scripts/rich_terminal_render.py the CLI (local, free, stdlib-only)
scripts/run_tests.py zero-dependency test runner
scripts/_mini_pytest.py the pytest stand-in
scripts/service_runner.py the paid resource: render + width verification
service/ OPTIONAL Alipay pay-per-use service (Node >= 22)
config.mjs env parsing (prefix RTR_), fails loudly
checker.mjs resource generation + 14-point subprocess sandbox
server.mjs HTTP endpoints + 402 orchestration
bill.mjs alipay.mjs orders.mjs verbatim protocol layer (verified in 8 packages)
README.md env.example.md operations + environment template
tools/mock-alipay-gateway.mjs local mock gateway (integration tests only, verbatim)
tests/ 60 vendored upstream test files (verbatim)
tests/test_local_platform_shapes.py local suite: CJK/emoji/CRLF/Windows shapes
tests/pay-flow.test.mjs local 402 end-to-end suite (113 assertions)
references/api-cheatsheet.md rich usage manual + recipes + pitfalls
references/cjk-width.md why len() is wrong and what to use instead
references/limitations.md measured limitations, with evidence
references/upstream-diff.md every difference from upstream, with reasons
references/pay-per-use.md agent-side guide to the 402 flow
Rules to follow when using this skill
- Never compute terminal padding with
len()on text that may contain CJK,
emoji or combining marks. Usecell_len/set_cell_size, or letTable
do the layout. - Run
align(ortable --check-alignment) before shipping an aligned
block. A silent width-table miss produces output that looks fine until
someone notices the borders. The check turns that into a non-zero exit. - Normalise CRLF before any ANSI parsing. A bare
\ris interpreted as
"overwrite the line" and discards the line's content — an upstream behaviour
verified against a pristine checkout. The bundled CLI does it for you;
references/limitations.md§2 explains it. - Do not promise syntax highlighting, rich tracebacks or Markdown. They are
out of scope by design; point the user atpip install richinstead. - Do not modify
rich/in place. Prefer a wrapper or an upstream upgrade,
so the vendored code stays diffable against upstream. - Never treat a local payment integration test as production readiness, and
never narrow a verification check to make a test pass. The 402 layer refuses
on any failed check and returns 402 rather than a resource; keep it that way.
Format and size validation must stay before the payment decision, so a
malformed request returns400and never creates an order.
Provenance
- Upstream:
Textualize/richv15.0.0, MIT, (c) 2020 Will McGugan - Cell-width data derived from wcwidth (MIT)
and the Unicode Character Database - 95 of 99 engine files byte-identical to upstream; 4 modified, 2 removed —
itemised inreferences/upstream-diff.md