@promptctl/rich-js
v0.6.0
Published
Rich text and beautiful formatting in the terminal — a TypeScript port of Python's Rich
Maintainers
Readme
rich-js
A TypeScript port of Python's wonderful Rich library by @willmcgugan.
Rich text and beautiful formatting in the terminal
The rich-js API makes it easy to add color and style to terminal output. It can also render pretty tables, progress bars, markdown, syntax highlighted source code, tracebacks, and more — out of the box.
Compatibility
Works on Linux, macOS, and Windows. Requires Node.js >= 20. ESM-only.
Installing
npm install @promptctl/rich-jsUsing the Console
Import and construct a Console object:
import { Console } from "@promptctl/rich-js";
const console = new Console();The Console object has a print method similar to the built-in console.log. Rich will word-wrap your text to fit within the terminal width.
console.print("Hello", "World!");Add color and style with a style argument:
console.print("Hello, World!", { style: "bold red" });For finer-grained styling, Rich renders markup using a syntax similar to bbcode:
console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i]way[/i].");Rich Library
Rich includes a number of built-in renderables for creating elegant terminal output.
The Console object has a log() method similar to print(), but adds a timestamp column on the left. Rich will syntax-highlight data structures automatically.
import { Console } from "@promptctl/rich-js";
const console = new Console();
console.log("Server started");
console.log({ status: 200, method: "GET", path: "/api/users" });Insert an emoji in console output by placing the name between two colons:
console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:");
// 😃 🧛 💩 👍 🦝Rich can render flexible tables with unicode box characters. There is a large variety of formatting options for borders, styles, and cell alignment.
import { Console, Table } from "@promptctl/rich-js";
const console = new Console();
const table = new Table({ title: "Star Wars Box Office" });
table.addColumn("Date", { style: "dim", width: 12 });
table.addColumn("Title");
table.addColumn("Production Budget", { justify: "right" });
table.addColumn("Box Office", { justify: "right" });
table.addRow("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$275,000,000", "$375,126,118");
table.addRow("May 25, 2018", "[red]Solo[/red]: A Star Wars Story", "$275,000,000", "$393,151,347");
table.addRow("Dec 15, 2017", "Star Wars Ep. VIII: The Last Jedi", "$262,000,000", "[bold]$1,332,539,889[/bold]");
console.print(table);The Table class resizes columns to fit the available terminal width, wrapping text as required. Console markup is rendered inside cells, and any Renderable can be used as a cell value — including other tables.
Rich can render multiple flicker-free progress bars to track long-running tasks.
For basic usage, wrap any iterable with track:
import { track } from "@promptctl/rich-js";
for (const step of track(Array.from({ length: 100 }), { description: "Processing..." })) {
await doStep(step);
}For multiple progress bars and custom columns, use Progress directly:
import { Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn } from "@promptctl/rich-js";
const progress = new Progress(
new TextColumn("{task.description}"),
new BarColumn(),
new TaskProgressColumn(),
new TimeRemainingColumn(),
);
await progress.run(async () => {
const task1 = progress.addTask("Downloading...", { total: 100 });
const task2 = progress.addTask("Processing...", { total: 200 });
// ... update tasks
});For situations where it is hard to calculate progress, use Status to display a spinner animation with a message:
import { Console } from "@promptctl/rich-js";
const console = new Console();
const status = new Status("[bold green]Working on tasks...", { console });
status.start();
for (const task of tasks) {
await processTask(task);
console.log(`${task} complete`);
}
status.stop();Rich can render a tree with guide lines — ideal for displaying file structures or any other hierarchical data:
import { Console, Tree } from "@promptctl/rich-js";
const console = new Console();
const tree = new Tree(":open_file_folder: root");
const branch = tree.add(":file_folder: src");
branch.add(":page_facing_up: index.ts");
branch.add(":page_facing_up: utils.ts");
tree.add(":page_facing_up: package.json");
console.print(tree);Tree labels can be plain text, markup strings, or any Renderable.
Rich can render content in neat columns with equal or optimal width:
import { Console, Columns } from "@promptctl/rich-js";
const console = new Console();
const items = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape"];
console.print(new Columns(items));Rich can render Markdown and translates the formatting to the terminal:
import { Console, Markdown } from "@promptctl/rich-js";
import { readFileSync } from "fs";
const console = new Console();
const md = new Markdown(readFileSync("README.md", "utf-8"));
console.print(md);Rich can render syntax-highlighted source code:
import { Console, Syntax } from "@promptctl/rich-js";
const console = new Console();
const code = `
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
`;
const syntax = new Syntax(code, "typescript", { theme: "monokai", lineNumbers: true });
console.print(syntax);Rich can render beautiful tracebacks that are easier to read and show more context than standard Node.js errors:
import { Console, Traceback } from "@promptctl/rich-js";
const console = new Console();
try {
riskyOperation();
} catch (error) {
console.print(new Traceback(error));
}Custom Renderables
All Rich renderables use the Renderable protocol. You can implement your own:
import type { Renderable, RenderOptions } from "@promptctl/rich-js";
import { Segment } from "@promptctl/rich-js";
class Greeting implements Renderable {
render(options: RenderOptions): Iterable<Segment> {
return [new Segment("Hello, World!\n")];
}
}
console.print(new Greeting());Console Options
const console = new Console({
colorSystem: "truecolor", // null | "auto" | "standard" | "256" | "truecolor" | "windows"
width: 120, // override terminal width
stderr: true, // write to stderr
record: true, // record output for export
highlight: false, // disable auto-highlighting
markup: false, // disable markup processing
});When record: true, export output after the fact:
import { saveHtml } from "@promptctl/rich-js/node/save";
const text = console.exportText();
const html = console.exportHtml();
saveHtml(console, "output.html");saveText / saveHtml live on the node/save subpath because they import node:fs; the main barrel stays browser-safe.
Demos
A handful of demos under examples/ exercise the library against realistic use cases. Two are explained in detail below (rich-explore and claude-sessions); the rest are listed here with one-line summaries. The full set of available npm scripts is the authoritative list — cat package.json | jq .scripts to see them all.
# Detailed below
npm run demo # rich-explore — TUI file browser + markdown/code reader (interactive)
npm run sessions # claude-sessions — Claude Code session browser (interactive)
# Other interactive demos
npm run demo-inputs # rich-config — TextInput / palette search
npm run demo:dropdown # dropdown-demo — Dropdown widget showcase
npm run dash # rich-dash — Live dashboard
npm run template-bindings # rich-template-bindings — go-template / reactive bindings playground
# Non-interactive transcripts
npm run themes-and-color-studio # color / palette / theme / contrast tour (eight sections)
npm run strip # rich-strip — side-by-side joiner showcase
npm run markup-plugins # rich-markup-plugins — plugin-tag examplesrich-explore — TUI file browser + markdown/code reader
A two-pane file browser with a directory tree on the left and a file preview on the right. Navigate with vim-style keys, Tab to switch focus, Enter/arrow keys to expand/collapse directories.
npm run demo # browse current directory
npm run demo -- /some/path # browse a specific pathFeatures exercised:
| Module | How it's used |
|---|---|
| Console | Render orchestrator, style application, color-system detection |
| Layout | Row split (tree / preview), column split (header / body / footer), ratio and size allocation |
| Panel | Bordered panes with dynamic titles (▸ Tree (20)), borderStyle, padding, focus-aware styling |
| Tree | Recursive directory tree with guide lines, guide_style, mixed RichText labels |
| Table + Column | Directory listing (name, kind, size, mtime), styled headers, right-justified columns |
| Markdown | Renders .md files in the preview pane |
| Syntax | Syntax-highlighted source code with lineNumbers and per-extension language detection |
| JSONRenderable | Pretty-printed + highlighted JSON file preview |
| RichText + Span | Labels, headers, status bar; .stylize(), .append(), end control |
| Style | Parsed inline everywhere ("bold cyan", "reverse bold", "bold white on blue") |
| Segment | Used directly in the Window renderable for line splitting, padding, and clipping |
| Renderable protocol | Custom Window class implements Renderable for viewport clipping |
| Box | ROUNDED (Panel default), HEAVY_HEAD (Table default) |
claude-sessions — Claude Code session browser
Browses ~/.claude/projects/ JSONL session files. Two-level sidebar (projects → sessions) on top, conversation viewer below. Pretty-prints every block type (human turns, assistant responses, tool calls, subagents, system events, errors) with per-block raw-JSON toggle. Includes local search, global cross-file search, subagent drill-down with session stack, and hidden-block reveal.
npm run sessionsKey bindings: ↑↓/jk navigate, →/Enter open/drill, ← back, Tab focus, \ toggle browser, v raw view, e expand, H hidden blocks, / local search, S global search, n/N next/prev match, p parent, u pop subagent, q quit.
Features exercised (incremental to rich-explore):
| Module | How it's used |
|---|---|
| Rule | Turn-duration system blocks rendered as horizontal dividers; input/output separators in tool-call blocks |
| Group | Composes multi-section tool-call blocks (input + Rule + result) into a single renderable |
| Pretty | Per-block raw view (toggled with v) — exercises ReprHighlighter, indent guides, maxString, expandAll |
| Traceback | Error blocks with stack traces render via Traceback for styled frame display |
| Markdown | Assistant text rendering (Claude output is often markdown) |
| Syntax | Bash command highlighting in tool-call input summaries |
| Panel | Six distinct border-color schemes by block kind (cyan/blue/yellow/red/magenta/green) |
| Layout | Column split (browser-on-top / viewer-on-bottom), dynamic height budgeting |
themes-and-color-studio — color, palette, theme, and contrast tour
A one-shot non-interactive demo that walks every public surface of the color subsystem in eight sections: ColorRgba values and parsing, ColorSpec and downgrade tables, color-system detection, the theme registry, PaletteResolver spec forms, every bundled TerminalTheme constant, OKLCH transposition (hue circle / chroma sweep / lightness invert / themeKeyForRoot), and the WCAG contrast toolkit.
npm run themes-and-color-studio # terminal output
EXPORT_HTML=out.html npm run themes-and-color-studio # also write a styled HTML transcriptFeatures exercised (incremental to above):
| Module | How it's used |
|---|---|
| ColorRgba / parseRgbHex / parseRgbaHex / blendRgb | Pixel-level values, two hex parsers, linear blend, alpha compositing |
| ColorSpec / ColorDepth | Every factory; downgrade across STANDARD_TABLE / EIGHT_BIT_TABLE / WINDOWS_TABLE; ANSI_COLOR_NAMES lookups; ColorParseError |
| detectColorSystem / resolveColorSystem | Env-driven color-system detection with DetectColorOptions fixtures; spec-string resolution |
| Palette / PaletteResolver / buildPalette | Bare / modifier / alpha / auto spec forms against gruvbox; BaseColors → derived text-* / on-* / *-muted vars |
| Theme registry | getThemePalette / listThemePalettes / getThemeBaseColors walking every bundled theme; raw THEMES / ThemePaletteData via subpath |
| TerminalTheme constants | All bundled constants (DEFAULT, SVG_EXPORT, MONOKAI, NORD, GRUVBOX, DRACULA, TOKYO_NIGHT, FLEXOKI, CYBERPUNK, CATPPUCCIN_*, SOLARIZED_*, ROSE_PINE*, ATOM_ONE_*, TEXTUAL_*) |
| Oklch / transposePalette / themeKeyForRoot | Round-trip + IDENTITY / INVERT_LIGHTNESS; hue circle; chroma sweep; light↔dark invert; ANCHORED_ROOTS / isAnchored |
| relativeLuminance / contrastRatio / contrastFor / ensureContrast | WCAG contrast matrix, hue-preserving lightness adjustment to clear the AA threshold |
| Console record / saveHtml | Optional HTML export via EXPORT_HTML=path; same render pipeline drives both terminal and file output |
Coverage summary
Exercised across demos:
Core: Console, Style/StyleStack, RichText/Span, Segment, Box (multiple variants), Color/blendRgb/palettes, Renderable/Measurable protocol, Measurement, cells (transitively), ReprHighlighter, JSONHighlighter, Spinner data, TerminalTheme
Renderables: Layout, Panel, Tree, Table/Column, Markdown, Syntax, JSONRenderable, Rule, Group, Spinner, Pretty, Traceback
Bugs found and fixed via demo integration:
| Bug | Location | Impact | Fix |
|---|---|---|---|
| Live.refresh() strips all ANSI styles | src/renderables/live.ts:106 | Every renderable flowing through Live (including Status, Progress, Spinner) appeared unstyled | Apply style.render(text, colorSystem) instead of bare s.text |
| Progress.render() drops column styles | src/renderables/progress.ts:273 | Progress percentage, timing, and spinner styles were stripped when building table cells | Use RichText.append(text, style) to preserve segment styles |
| Tree emits double blank lines | src/renderables/tree.ts:98,122 | Label rendering and the explicit Segment.line() both contributed a newline, producing blank lines between tree entries | Make RichText stop emitting a trailing newline so Tree's explicit yield Segment.line() remains the only line break |
| Spinner constructor rejects undefined name | src/renderables/spinner.ts:36 | SpinnerColumn (used by Progress) passed optional string \| undefined to required string parameter | Make name optional, default to DEFAULT_SPINNER |
Not yet exercised — candidates for new demos or demo additions:
| Module | Notes | Suggested coverage |
|---|---|---|
| Status | Spinner + message display | Loading indicator for large sessions in claude-sessions |
| Prompt / IntPrompt / FloatPrompt / Confirm | Interactive input via readline | Add a go-to-path prompt in rich-explore; incompatible with raw-mode loops so needs a modal switch |
| emoji | Shortcode substitution (:smiley: → 😃) | Enable in markup-rendered block text in claude-sessions |
| NullHighlighter / RegexHighlighter | Specialized highlighters not yet exercised | RegexHighlighter for search-term highlighting in claude-sessions |
| StyleStack / Theme / DEFAULT_STYLES | Theme customization | Add a Console style-theme switcher demo |
| Console recording | record, exportText, exportHtml, saveHtml | Add an export-to-HTML feature to claude-sessions |
| Most Box variants | ASCII, SQUARE, MINIMAL, HEAVY, DOUBLE, MARKDOWN, etc. | Add a box-style picker to rich-explore's Panel borders |
| Measurement.get() / measureRenderables | Explicit width measurement | Used internally; could add a measurement debug overlay |
Environment Variables
| Variable | Effect |
|---|---|
| NO_COLOR | Disable all color |
| FORCE_COLOR | Enable color regardless of TERM |
| TERM=dumb | Disable color and style |
| COLUMNS / LINES | Override terminal dimensions |
