CLI primitives for hackers.
Built entirely on Bun's native APIs — every string measurement, color conversion, and text wrap runs through Zig/SIMD-optimized internals.
bun demo:frame # Claude Code-style interactive REPL (the crown jewel)
bun demo:repl # simpler REPL without frame
bun demo:all # see everything
bun demo:spinner # spinner showcase
bun demo # original demo
- CLI, not TUI — output stays inline with terminal history. Pipes, composes, scrolls back. No alternate screen.
- Terminal-themed by default — ANSI 16 codes respect your terminal color scheme. Exact RGB available when you need it via
.fg()/.bg(). - Pipe-aware everything — every module detects TTY vs pipe and degrades gracefully. Colors strip, animations become static text, links show URLs.
- Bun-native —
Bun.color(),Bun.stringWidth(),Bun.stripANSI(),Bun.wrapAnsi(),Bun.markdown.render(). Built on top of what Bun already optimized.
| Module | Category | What it does |
|---|---|---|
writer |
output | Pipe-aware stdout, terminal width, TTY detection |
style |
output | Composable ANSI styling with Proxy chains |
box |
layout | Framed sections, dividers, section headers |
table |
layout | Data tables with per-cell styling |
columns |
layout | Auto-sizing multi-column layout |
markdown |
rendering | Markdown to styled terminal output |
spinner |
animation | 45 animated inline loaders |
progress |
animation | 10 progress bar styles with ETA |
badge |
display | Inline status indicators |
list |
display | Bullets, numbered, key-value, trees |
log |
output | Structured logging with icons |
text |
utility | Truncate, indent, pad, link, wrap |
keypress |
input | Raw keyboard reading, key event parsing |
prompt |
input | Confirm, input, password, select, multiselect |
banner |
display | Large block-letter text (5 render styles) |
timer |
utility | Stopwatch, countdown, benchmark helper |
layout |
composition | Two-zone terminal manager (output + active) |
stream |
composition | Buffered streaming text with line flushing |
highlight |
rendering | Syntax highlighting for 7 languages |
args |
parsing | Declarative CLI args with auto-generated help |
repl |
interactive | Readline and REPL loop with slash commands |
live |
interactive | Activity spinners, multi-line sections |
statusbar |
interactive | Left/right aligned terminal status line |
diff |
display | Line-level diff with red/green coloring |
file-preview |
display | Syntax-highlighted code block with border |
import { s, writeln, box, spinner, log, badge } from "@rlabs-inc/prism"
// styled output
writeln(s.bold.green("mission ready"))
// framed content
writeln(box("Target acquired", { title: "HUNT", border: "rounded" }))
// structured logging
log.info("Scanning targets...")
log.success("Found 452 programs")
// status badges
writeln(badge("CRITICAL", { color: s.red }) + " XSS in login form")
// async operations with spinners
const spin = spinner("Syncing HackerOne...", { style: "dots", timer: true })
const data = await fetchPrograms()
spin.done(`Synced ${data.length} programs`)Pipe-aware output primitives. The foundation everything else builds on.
import { write, writeln, error, pipeAware, termWidth, isTTY } from "@rlabs-inc/prism"// Write raw text to stdout (no newline)
write("loading")
// Write with newline (default: empty line)
writeln("hello")
writeln()
// Write to stderr
error("something went wrong")
// Strip ANSI if piped, keep if TTY
const safe = pipeAware(styledText)
// Terminal width (defaults to 80 if not TTY)
const width = termWidth()
// Whether output is a real terminal
if (isTTY) { /* animate */ } else { /* static */ }Composable terminal styling via Proxy chains. Two color modes: terminal-themed (ANSI 16) and exact RGB.
import { s, color, RESET } from "@rlabs-inc/prism"s.bold("bold text")
s.dim("dim text")
s.italic("italic text")
s.underline("underlined")
s.inverse("inverted")
s.strikethrough("struck")These respect your terminal's color scheme. s.red shows terminal defined red color.
// foreground
s.red("red")
s.green("green")
s.yellow("yellow")
s.blue("blue")
s.magenta("magenta")
s.cyan("cyan")
s.white("white")
s.gray("gray")
s.black("black")
// bright variants
s.brightRed("bright red")
s.brightGreen("bright green")
s.brightYellow("bright yellow")
s.brightBlue("bright blue")
s.brightMagenta("bright magenta")
s.brightCyan("bright cyan")
s.brightWhite("bright white")
// background
s.bgRed("on red")
s.bgGreen("on green")
s.bgBlue("on blue")
s.bgYellow("on yellow")
s.bgMagenta("on magenta")
s.bgCyan("on cyan")
s.bgWhite("on white")
s.bgBlack("on black")Modifiers and colors compose in any order:
s.bold.red("bold red")
s.dim.cyan("dim cyan")
s.bold.underline.yellow("bold underline yellow")
s.italic.brightMagenta("italic bright magenta")When you need a specific color that ignores the terminal theme, use .fg() and .bg(). Accepts any CSS color format via Bun.color().
// hex
s.fg("#ff6b35")("exact orange")
// hsl
s.fg("hsl(280, 80%, 60%)")("exact purple")
// combine with modifiers
s.bold.fg("#00d4aa")("bold exact teal")
// background
s.bg("#8b5cf6").white("white on purple bg")
// shorthand for quick exact colors
color("text", "#ff6b35") // fg only
color("text", "white", "#8b5cf6") // fg + bgFramed content sections with Unicode box-drawing characters.
import { box, divider, header, borders, type BorderStyle } from "@rlabs-inc/prism"// simple box
writeln(box("Hello from prism"))
// with title and styling
writeln(box("Mission: Aggregate all bug bounty platforms\nStatus: Active", {
title: "HUNT",
border: "rounded", // "single" | "double" | "rounded" | "heavy"
titleColor: s.bold.green,
titleAlign: "left", // "left" | "center" | "right"
width: 60, // defaults to terminal width
padding: 1, // horizontal padding inside box
}))Output:
╭─ HUNT ───────────────────────────────────────────────╮
│ Mission: Aggregate all bug bounty platforms │
│ Status: Active │
╰──────────────────────────────────────────────────────╯
| Style | Characters | Look |
|---|---|---|
single |
┌─┐│└┘ |
Clean, standard |
double |
╔═╗║╚╝ |
Bold, formal |
rounded |
╭─╮│╰╯ |
Soft, modern |
heavy |
┏━┓┃┗┛ |
Thick, attention-grabbing |
// default (─ across full width)
writeln(divider())
// custom character and width
writeln(divider("━", 40))
// with color
writeln(divider("═", undefined, "gray"))// centered text with lines extending to terminal width
writeln(header("BUG BOUNTY PLATFORMS"))
// → ──────────── BUG BOUNTY PLATFORMS ────────────
// custom character and color
writeln(header("RESULTS", { char: "━", color: s.bold.green }))Data tables with per-cell colors, alignment, truncation, and formatting.
import { table } from "@rlabs-inc/prism"writeln(table([
{ name: "HackerOne", programs: 452, status: "Active" },
{ name: "Bugcrowd", programs: 128, status: "Planned" },
{ name: "Intigriti", programs: 89, status: "Planned" },
]))Output:
┌───────────┬──────────┬─────────╮
│ name │ programs │ status │
├───────────┼──────────┼─────────┤
│ HackerOne │ 452 │ Active │
│ Bugcrowd │ 128 │ Planned │
│ Intigriti │ 89 │ Planned │
└───────────┴──────────┴─────────╯
writeln(table(data, {
border: "rounded", // border style
borderColor: "gray", // border color (CSS string)
headerColor: s.bold, // header row styling
maxWidth: 80, // max table width
compact: true, // remove padding
index: true, // add row numbers
columns: [
{
key: "platform",
label: "Platform", // custom header label
color: s.bold.cyan, // cell color function
align: "left", // "left" | "center" | "right"
width: 20, // fixed width
minWidth: 10, // minimum width
maxWidth: 30, // maximum width
format: (v) => String(v).toUpperCase(), // value formatter
},
{
key: "status",
label: "Status",
color: (v) => v === "Active" ? s.green(v) : s.yellow(v),
},
],
}))Auto-sizing multi-column layout. Fits as many columns as the terminal allows.
import { columns } from "@rlabs-inc/prism"const items = [
"dots", "dots2", "dots3", "line", "pipe", "arc",
"circle", "triangles", "blocks", "pulse", "wave",
"arrows", "aesthetic", "binary", "matrix", "orbit",
]
writeln(columns(items, {
gap: 3, // space between columns (default: 2)
padding: 2, // left padding (default: 0)
minWidth: 10, // minimum column width (default: 10)
maxColumns: 4, // cap number of columns
}))Output (auto-sized to terminal width):
dots dots2 dots3 line pipe
arc circle triangles blocks pulse
wave arrows aesthetic binary matrix
orbit
Render markdown to styled terminal output using Bun.markdown.render() with hacker-themed ANSI callbacks.
import { md } from "@rlabs-inc/prism"writeln(md(`# Hunt Report
The **ultimate** bug bounty aggregator.
## Targets
- [x] HackerOne synced
- [x] Bugcrowd mapped
- [ ] Intigriti pending
> "Connections matter more than objects"
Use \`hunt search\` to find your next target.
\`\`\`bash
hunt sync --platform hackerone
hunt list --bounty-min 1000
\`\`\`
`))Renders with:
- Headings: bold/underline for h1, bold for h2, bold+dim for h3+
- Bold/italic: proper ANSI modifiers
- Code: cyan with horizontal rules
- Inline code: cyan with backticks
- Links: underlined blue with URL in parentheses
- Lists:
›bullets,✓checked,○unchecked - Blockquotes: dim
│prefix with italic text - Horizontal rules: full-width divider
45 animated inline loaders across 12 categories. Animates on the current line, then completes with an icon and final message that stays in terminal history.
import { spinner, spinners, type SpinnerStyle } from "@rlabs-inc/prism"const spin = spinner("Syncing HackerOne...")
// ... async work ...
spin.done("Synced 452 programs") // ✓ Synced 452 programs (green)Terminal shows:
⠋ Syncing HackerOne... ← animates in place
✓ Synced 452 programs ← final state, stays in history
spin.done("Success message") // ✓ green
spin.fail("Error message") // ✗ red
spin.warn("Warning message") // ⚠ yellow
spin.info("Info message") // ℹ blue
spin.stop("★", "Custom", s.magenta) // custom icon + colorconst spin = spinner("Loading...", {
style: "arc", // any of 45 spinner styles
color: s.yellow, // spinner frame color (default: s.cyan)
timer: true, // show elapsed time
frames: ["⠋","⠙","⠹"], // custom frames (overrides style)
interval: 100, // custom interval ms (overrides style default)
})const spin = spinner("Starting...")
spin.text("Phase 1: Fetching programs...")
spin.text("Phase 2: Processing results...")
spin.done("All phases complete")let spin = spinner("Syncing HackerOne...", { timer: true })
await syncHackerOne()
spin.done("HackerOne synced (452 programs)")
spin = spinner("Syncing Bugcrowd...", { timer: true })
await syncBugcrowd()
spin.done("Bugcrowd synced (128 programs)")Terminal history:
✓ HackerOne synced (452 programs) 1.2s
✓ Bugcrowd synced (128 programs) 0.8s
| Category | Styles | Preview |
|---|---|---|
| Classic | dots dots2 dots3 dots4 line pipe simpleDots star spark |
⠋ ⠙ ⠹ ⠸ ⠼ ⠴ |
| Geometric | arc circle squareSpin triangles sectors diamond |
◜ ◠ ◝ ◞ ◡ ◟ |
| Block & Shade | toggle toggle2 blocks blocks2 blocks3 |
░ ▒ ▓ █ ▓ ▒ |
| Pulse & Breathe | pulse pulse2 breathe heartbeat |
· • ● • |
| Bar & Bounce | growing bounce bouncingBar bouncingBall |
▏ ▎ ▍ ▌ ▋ ▊ ▉ █ |
| Arrow | arrows arrowPulse |
▹▹▹▹▹ ►▹▹▹▹ ▹►▹▹▹ |
| Wave | wave wave2 |
▁ ▂ ▃ ▄ ▅ ▆ ▇ █ |
| Aesthetic | aesthetic filling scanning |
▰▰▰▱▱ ▰▰▰▰▱ |
| Digital & Hacker | binary matrix hack |
010010 001101 |
| Braille Art | brailleSnake brailleWave |
⠏ ⠛ ⠹ ⢸ ⣰ ⣤ |
| Orbit | orbit |
◯ ◎ ● ◎ |
| Emoji | earth moon clock hourglass |
🌍 🌎 🌏 |
Browse and preview:
bun run demo-spinner.ts --list # see all 45 with frame previews
bun run demo-spinner.ts --all # animated showcase of every spinner
bun run demo-spinner.ts dots # preview a specific style for 3sDeterminate progress bars with 10 visual styles, smooth sub-character rendering, and ETA calculation.
import { progress, barStyles, type ProgressStyle } from "@rlabs-inc/prism"const bar = progress("Downloading", { total: 100 })
for (let i = 0; i <= 100; i++) {
bar.update(i)
await doWork()
}
bar.done("Download complete")Terminal shows:
Downloading ██████████████░░░░░░░░░░░░ 56% ← updates in place
✓ Download complete 3.2s ← final state
const bar = progress("Syncing programs", {
total: 452, // total value (default: 100)
style: "arrows", // bar style (default: "bar")
color: s.green, // bar color (default: s.cyan)
width: 30, // bar width in chars (auto-sized if omitted)
showPercent: true, // show percentage (default: true)
showCount: true, // show current/total (e.g., 225/452)
showETA: true, // show estimated time remaining
smooth: true, // sub-character precision (default: true)
})
bar.update(225) // update current value
bar.update(300, 500) // update current AND total
bar.done("All synced") // ✓ green
bar.fail("Network error") // ✗ red| Style | Look | Characters |
|---|---|---|
bar |
████████░░░░ |
█ filled, ░ empty |
blocks |
▓▓▓▓▓▓░░░░ |
▓ filled, ░ empty |
shades |
▐████████ ▌ |
With frame |
classic |
[======== ] |
ASCII brackets |
arrows |
▰▰▰▰▰▱▱▱▱ |
Filled/empty triangles |
smooth |
━━━━━━─── |
Horizontal lines |
dots |
⣿⣿⣿⣿⠀⠀⠀ |
Braille blocks |
square |
■■■■□□□□ |
Filled/empty squares |
circle |
●●●●○○○○ |
Filled/empty circles |
pipe |
┫┃┃┃╌╌╌┣ |
Pipe characters |
When smooth: true (default for bar/blocks/shades styles), the progress bar uses sub-character block elements (▏▎▍▌▋▊▉) for fractional progress — 8 substeps per character instead of jumping one full block at a time.
Inline status indicators. Three variants for different contexts.
import { badge } from "@rlabs-inc/prism"Colored text in dim brackets. Best for inline status tags.
badge("CRITICAL", { color: s.red }) // [CRITICAL]
badge("HIGH", { color: s.yellow }) // [HIGH]
badge("LOW", { color: s.green }) // [LOW]
badge("INFO", { color: s.blue }) // [INFO]Colored dot prefix. Best for state indicators in lists.
badge("Active", { color: s.green, variant: "dot" }) // ● Active
badge("Paused", { color: s.yellow, variant: "dot" }) // ● Paused
badge("Closed", { color: s.red, variant: "dot" }) // ● ClosedBackground-colored label. Use with s.bgColor for full effect.
badge("H1", { color: s.bgGreen, variant: "pill" }) // H1 (green bg)
badge("BC", { color: s.bgBlue, variant: "pill" }) // BC (blue bg)Formatted lists, key-value pairs, and file trees.
import { list, kv, tree } from "@rlabs-inc/prism"const platforms = ["HackerOne", "Bugcrowd", "Intigriti", "YesWeHack"]
// bullet (default)
writeln(list(platforms))
// • HackerOne
// • Bugcrowd
// • Intigriti
// • YesWeHack
// numbered
writeln(list(platforms, { style: "numbered" }))
// 1. HackerOne
// 2. Bugcrowd
// 3. Intigriti
// 4. YesWeHack
// all styles
list(items, { style: "bullet" }) // • item
list(items, { style: "dash" }) // - item
list(items, { style: "arrow" }) // → item
list(items, { style: "star" }) // ★ item
list(items, { style: "check" }) // ✓ item
list(items, { style: "numbered" }) // 1. item
list(items, { style: "alpha" }) // a. itemlist(items, {
style: "arrow", // marker style
color: s.cyan, // marker color (default: s.dim)
indent: 4, // left indentation
marker: "▸", // custom marker (overrides style)
})Aligned key-value pairs with automatic padding.
writeln(kv({
Name: "hunt",
Version: "0.1.0",
Runtime: "Bun 1.3.9",
Programs: "452 synced",
}))
// Name hunt
// Version 0.1.0
// Runtime Bun 1.3.9
// Programs 452 syncedWith options:
writeln(kv(data, {
separator: " → ", // between key and value (default: " ")
keyColor: s.cyan, // key styling (default: s.bold)
valueColor: s.dim, // value styling (default: none)
indent: 2, // left indentation
}))
// Name → hunt
// Version → 0.1.0Also accepts [key, value][] tuples for ordered entries:
writeln(kv([
["first", "HackerOne"],
["second", "Bugcrowd"],
]))File and data tree rendering with box-drawing connectors.
writeln(tree({
src: {
"writer.ts": null, // null = file (leaf node)
"style.ts": null,
lib: { // object = directory (branch)
"utils.ts": null,
"helpers.ts": null,
},
},
"package.json": null,
}))Output:
├── src/
│ ├── writer.ts
│ ├── style.ts
│ └── lib/
│ ├── utils.ts
│ └── helpers.ts
└── package.json
With options:
writeln(tree(data, {
fileColor: s.white, // file name color (default: none)
dirColor: s.bold.blue, // directory color (default: s.bold.blue)
}))Structured logging with consistent icons and colors. Same visual language across all prism-based tools.
import { log } from "@rlabs-inc/prism"log.info("Server listening on port 3000") // ℹ blue
log.success("Connected to database") // ✓ green
log.warn("Rate limit approaching (450/500)") // ⚠ yellow
log.error("Connection refused: API timeout") // ✗ red
log.debug("Query returned 452 rows in 1.2s") // ● dim
log.step("Processing next batch...") // → cyanOutput:
ℹ Server listening on port 3000
✓ Connected to database
⚠ Rate limit approaching (450/500)
✗ Connection refused: API timeout
● Query returned 452 rows in 1.2s
→ Processing next batch...
// per-call options
log.info("message", { timestamp: true, prefix: "hunt" })
// → 14:30:52 [hunt] ℹ message
// global defaults (apply to all calls)
log.configure({ timestamp: true, prefix: "hunt" })
log.info("now all calls have timestamp and prefix")
log.success("like this too")
// reset
log.configure({})Text manipulation utilities. All ANSI-aware — they handle escape codes correctly.
import { truncate, indent, pad, link, wrap } from "@rlabs-inc/prism"ANSI-aware text truncation. Properly handles escape sequences — truncates visible characters while preserving ANSI codes, adds reset before ellipsis to prevent color bleed.
truncate("The quick brown fox jumps over the lazy dog", 20)
// → "The quick brown fox…"
// works with styled text
truncate(s.red("Hello World"), 8)
// → "\x1b[31mHello W\x1b[0m…" (red "Hello W" + reset + ellipsis)
// custom ellipsis
truncate("Long text here", 10, "...")
// → "Long te..."Indent every line of text.
indent("line 1\nline 2", 4)
// → " line 1\n line 2"
// custom character
indent("nested", 2, "│ ")
// → "│ │ nested"
// composable nesting
indent("level 0\n" + indent("level 1\n" + indent("level 2", 2), 2))
// → "level 0\n level 1\n level 2"Pad text to a fixed width (ANSI-aware). Uses Bun.stringWidth() for correct measurement.
"|" + pad("left", 20) + "|" // |left |
"|" + pad("center", 20, "center") + "|" // | center |
"|" + pad("right", 20, "right") + "|" // | right|
// works with styled text (measures visible width, not byte length)
pad(s.red("hi"), 10) // "hi" in red + 8 spacesTerminal hyperlinks (OSC 8). Clickable in supported terminals (iTerm2, Warp, WezTerm, GNOME Terminal). Falls back to text (url) when piped.
link("HackerOne", "https://hackerone.com")
// In TTY: clickable "HackerOne" that opens the URL
// In pipe: "HackerOne (https://hackerone.com)"ANSI-preserving text wrapping using Bun.wrapAnsi().
wrap("Very long text that needs wrapping...", 40) // wrap to 40 chars
wrap("Auto-width wrapping") // wraps to terminal widthRaw keyboard input reading. Foundation for all interactive components.
import { keypress, keypressStream, rawMode, type KeyEvent } from "@rlabs-inc/prism"const key = await keypress()
// key.key → "a", "enter", "up", "tab", "escape", "f1", etc.
// key.char → "a" (empty for special keys)
// key.ctrl → true if Ctrl was held
// key.shift → true if Shift was held
// key.meta → true if Alt/Option was held
// key.sequence → raw escape sequence// read keys until "q" is pressed
const stop = keypressStream((key) => {
console.write(`You pressed: ${key.key}\n`)
if (key.key === "q") return "stop"
})All standard keys including arrows, home/end, page up/down, insert, delete, F1-F12, and Ctrl+A through Ctrl+Z. Unknown sequences are passed through as-is.
Interactive terminal input primitives. Built on keypress for raw keyboard handling.
import { confirm, input, password, select, multiselect } from "@rlabs-inc/prism"const yes = await confirm("Deploy to production?")
// ? Deploy to production? (y/N) _
// ✓ Deploy to production? yes
const withDefault = await confirm("Continue?", { default: true })
// ? Continue? (Y/n) _const name = await input("Project name:")
// ? Project name: _
// ✓ Project name: hunt
// with defaults and validation
const port = await input("Port:", {
default: "3000",
placeholder: "3000",
validate: (v) => /^\d+$/.test(v) || "Must be a number",
})Characters displayed as dots. Value never shown.
const key = await password("API key:")
// ? API key: ●●●●●●●●
// ✓ API key: ●●●●●●●●Arrow-key driven single selection with j/k vim-style navigation.
const platform = await select("Target platform:", [
"HackerOne",
"Bugcrowd",
"Intigriti",
"YesWeHack",
"Immunefi",
])
// ? Target platform: (↑/↓ to navigate, enter to select)
// › HackerOne
// Bugcrowd
// Intigriti
// YesWeHack
// Immunefi
// ✓ Target platform: HackerOneWith options:
const choice = await select("Choose:", longList, {
pageSize: 10, // visible items before scrolling (default: 7)
})Space to toggle, a to toggle all, enter to confirm.
const platforms = await multiselect("Sync platforms:", [
"HackerOne",
"Bugcrowd",
"Intigriti",
"YesWeHack",
], { min: 1, max: 3 })
// ? Sync platforms: (space to toggle, enter to confirm)
// › ◉ HackerOne
// ○ Bugcrowd
// ◉ Intigriti
// ○ YesWeHack
// ✓ Sync platforms: HackerOne, IntigritiLarge block-letter text using a 5x5 pixel bitmap font. Supports A-Z, 0-9, and common symbols.
import { banner } from "@rlabs-inc/prism"writeln(banner("PRISM"))Output:
████████ ████████ ██████████ ████████ ██ ██
██ ██ ██ ██ ██ ██ ████ ████
████████ ████████ ██ ██████ ██ ██ ██
██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██████████ ████████ ██ ██
banner("HI", { style: "block" }) // ██ full blocks (default)
banner("HI", { style: "shade" }) // ▓░ shade gradient
banner("HI", { style: "dots" }) // ⣿ braille blocks
banner("HI", { style: "ascii" }) // ## ASCII hash
banner("HI", { style: "outline" }) // ▐▌ outline blockswriteln(banner("HUNT", {
style: "shade", // render style
color: s.green, // color function
letterSpacing: 2, // pixels between letters (default: 1)
charWidth: 1, // 1 = compact, 2 = wide (default: 2)
}))Stopwatch, countdown, and benchmarking utilities.
import { stopwatch, countdown, bench, formatTime } from "@rlabs-inc/prism"const sw = stopwatch("Syncing data") // prints "⏱ Syncing data"
await syncHackerOne()
sw.lap("HackerOne") // prints " ⏱ HackerOne 1.2s"
await syncBugcrowd()
sw.lap("Bugcrowd") // prints " ⏱ Bugcrowd 2.1s"
sw.done("Sync complete") // prints "⏱ Sync complete 2.1s"
// or just measure without printing
const sw2 = stopwatch()
await doWork()
const { ms, formatted } = sw2.stop() // { ms: 1234, formatted: "1.2s" }const timer = countdown(30, "Rate limit cooldown")
// ⏳ Rate limit cooldown 30.0s ← updates every second
// ✓ Rate limit cooldown complete
// cancel early
timer.cancel()
// ⏹ Rate limit cooldown cancelledawait bench("string concat", () => {
let s = ""; for (let i = 0; i < 100; i++) s += "x"
}, 10000)
// ⚡ string concat: 0.003ms per op (333,333 ops/sec)Human-readable time formatting used by all timer functions:
formatTime(42) // "42ms"
formatTime(1234) // "1.2s"
formatTime(65000) // "1m 5s"
formatTime(3700000) // "1h 1m"Keyword-based syntax highlighting for terminal output. Not a full parser — just enough to make code snippets readable in CLI output.
import { highlight } from "@rlabs-inc/prism"writeln(highlight(`const data = await fetch("/api/programs")
const programs = data.filter(p => p.bounty > 1000)
console.log("Found", programs.length, "targets")`))Highlights with: keywords in magenta, strings in green, numbers in yellow, comments in dim, builtins in cyan.
| Language | Auto-detected by |
|---|---|
typescript |
import, interface, : string (default) |
javascript |
const, function |
json |
Starts with { or [ |
bash |
#!/bin/, echo |
sql |
SELECT, FROM |
graphql |
query, mutation |
rust |
fn, let mut |
highlight(code, {
language: "sql", // explicit language (default: "auto")
lineNumbers: true, // show line numbers with gutter
startLine: 10, // starting line number (default: 1)
})writeln(highlight(`SELECT name, bounty
FROM programs
WHERE platform = 'hackerone'
ORDER BY bounty DESC`, { language: "sql", lineNumbers: true }))Output:
1 │ SELECT name, bounty
2 │ FROM programs
3 │ WHERE platform = 'hackerone'
4 │ ORDER BY bounty DESC
Declarative CLI argument parsing.
import { args } from "@rlabs-inc/prism"const cli = args({
name: "hunt",
version: "0.1.0",
description: "Bug bounty aggregator",
commands: {
sync: { description: "Sync bug bounty programs" },
list: { description: "List synced programs" },
discover: { description: "Find your next adventure" },
},
flags: {
verbose: { type: "boolean", short: "v", description: "Verbose output" },
},
})
switch (cli.command) {
case "sync": await handleSync(cli.flags); break
case "list": await handleList(cli.flags); break
case "discover": await handleDiscover(cli.flags); break
}Running hunt --help auto-generates:
hunt v0.1.0 — Bug bounty aggregator
USAGE
hunt <command> [flags]
COMMANDS
sync Sync bug bounty programs
list List synced programs
discover Find your next adventure
FLAGS
-v, --verbose Verbose output
-h, --help Show help
--version Show version
Run 'hunt <command> --help' for command-specific flags.
Each command can define its own flags, shown alongside global flags in <command> --help:
const cli = args({
name: "hunt",
commands: {
sync: {
description: "Sync bug bounty programs",
flags: {
platform: { type: "string", short: "p", description: "Filter by platform", placeholder: "name" },
force: { type: "boolean", short: "f", description: "Force re-sync all data" },
},
},
lookup: {
description: "Look up a specific program",
usage: "<handle>", // shown in USAGE line
},
},
flags: {
verbose: { type: "boolean", short: "v", description: "Verbose output" },
},
})Running hunt sync --help:
hunt sync — Sync bug bounty programs
USAGE
hunt sync [flags]
FLAGS
-p, --platform <name> Filter by platform
-f, --force Force re-sync all data
GLOBAL FLAGS
-v, --verbose Verbose output
{
type: "string" | "boolean", // flag type
short: "p", // single-char alias (-p)
description: "Filter by...", // shown in help
default: "name", // default value (shown in help)
required: true, // exit with error if missing
placeholder: "name", // type hint in help (default: flag name)
}const cli = args(config)
cli.command // matched command name or undefined
cli.flags // { platform: "hackerone", force: true, verbose: true }
cli.args // positional arguments (excludes command name)
cli.showHelp() // manually print help
cli.showVersion() // manually print version--help/-h: auto-prints help and exits--version: auto-prints version and exits (whenversionis set)- No args: shows help when commands are defined but none given
- Unknown command: shows error with available commands list
- Missing required flag: shows error with usage hint
- Examples: shown in help when
examplesarray is provided
args({
name: "hunt",
// ...
examples: [
"hunt sync --platform hackerone",
"hunt list --sort bounty --limit 10",
"hunt discover",
],
}) EXAMPLES
$ hunt sync --platform hackerone
$ hunt list --sort bounty --limit 10
$ hunt discover
Interactive prompt system with full line editing, history, tab completion, and slash commands. Pure input primitives — compose with layout for framed UIs.
import { readline, repl, type ReadlineOptions, type ReplOptions, type CommandDef } from "@rlabs-inc/prism"Read a single line of input with full line editing.
const name = await readline({ prompt: "Name: " })
// with all options
const cmd = await readline({
prompt: "❯ ", // string or () => string for dynamic
default: "nmap", // pre-filled value
promptColor: s.cyan, // prompt styling (default: s.cyan)
history: sharedHistory, // shared array, mutated on submit
historySize: 500, // max entries (default: 500)
mask: "●", // mask chars (for passwords)
completion: (word, line) => { // tab completion
return tools.filter(t => t.startsWith(word))
},
})Built-in keybindings:
- Arrow keys, Home/End, Ctrl+A/E — cursor movement
- Ctrl+Left/Right, Alt+B/F — word jumping
- Up/Down — history navigation
- Tab — completion (single match auto-completes, multiple shows hints)
- Ctrl+W — delete word backward
- Ctrl+U/K — clear before/after cursor
- Ctrl+L — clear screen
- Ctrl+C — cancel (or clear line in REPL mode)
- Ctrl+D — EOF on empty, forward-delete otherwise
- Paste — multi-line paste flattened to single line
Wrapping: Handles input that wraps past terminal width. Properly tracks rows and repositions cursor across wrapped lines.
Run an interactive prompt loop with slash commands and abort support.
await repl({
prompt: "❯ ",
greeting: "Welcome to hunt interactive",
onInput: async (input, signal) => {
// called for non-command input
// return a string to auto-print it
return `You said: ${input}`
},
commands: {
scan: {
description: "Run a network scan",
aliases: ["s"],
handler: async (args, signal) => {
const sec = section("Scanning...")
// ... work ...
sec.done("Complete")
},
},
},
// auto-registered: /help (with /h and /? aliases)
// auto-handled: exit, quit, Ctrl+C (×2), Ctrl+D
})Abort support: Handlers receive an AbortSignal. First Ctrl+C during execution aborts the signal. Second Ctrl+C force-exits.
Tab completion: Auto-completes slash commands. Custom completion merges with command completion:
await repl({
commands: { scan: { ... }, search: { ... } },
completion: (word, line) => {
// called for non-command input
return tools.filter(t => t.startsWith(word))
},
})
// typing "/sc" + Tab → /scan
// typing "nm" + Tab → nmapLifecycle hooks:
await repl({
beforePrompt: () => { /* called before each prompt */ },
onExit: () => { /* called when repl exits */ },
exitCommands: ["exit", "quit"], // strings that exit (default)
commandPrefix: "/", // command prefix (default: "/")
history: true, // enable history (default: true)
})Pipe support: When not a TTY, reads piped stdin line-by-line, dispatches commands, calls onInput for regular lines.
The repl handles input. The layout handles output zones. Together they build framed UIs like Claude Code:
import { repl, layout, statusbar, section, s, termWidth } from "@rlabs-inc/prism"
const app = layout()
app.setActive(() => ({
lines: [
s.dim("─".repeat(termWidth())),
statusbar({
left: [{ text: "hunt", color: s.cyan }],
right: { text: `${tokenCount} tokens`, color: s.dim },
}),
],
}))
await repl({
prompt: "❯ ",
commands: {
scan: {
description: "Network scan",
handler: async (_args, signal) => {
const sec = section("Scanning...", { spinner: "hack", timer: true })
await new Promise(r => setTimeout(r, 600))
sec.add("22/tcp ssh")
sec.done("Scan complete")
},
},
},
onInput: async (input) => `You said: ${input}`,
onExit: () => app.close(),
})Terminal layout:
❯ /scan
✓ Scan complete: 1 open port (0.6s)
⎿ 22/tcp ssh
────────────────────────────────────────
❯ _
hunt 150 tokens
Live terminal components that animate in-place, then freeze into scrollback. Two types: single-line activity() and multi-line section().
import { activity, section, type Activity, type Section, type FooterConfig } from "@rlabs-inc/prism"Single-line live status with animated icon, timer, and dynamic metrics.
const act = activity("Searching programs...")
// ⠋ Searching programs... ← animates in place
// update text while running
act.text("Searching page 2...")
// finish with different states
act.done("Found 42 programs") // ✓ green
act.fail("Network error") // ✗ red
act.warn("Rate limited") // ⚠ yellow
act.info("Cache hit") // ℹ blue
act.stop("★", "Custom", s.magenta) // custom iconOptions:
const act = activity("Downloading data...", {
icon: "hack", // spinner style name or static string (default: "dots")
timer: true, // show elapsed time (default: false)
color: s.green, // spinner color (default: s.cyan)
metrics: () => `${found} found`, // live metrics, called every tick
})
// ⠋ Downloading data... (2.1s · 42 found)With footer (used by Stage internally):
// live components accept a footer that renders below their content
// this is how the Stage system keeps the frame pinned below animations
const act = activity("Working...", {
footer: {
render: () => ["─────", "❯ ", "─────"], // lines below content
onEnd: () => { /* redraw frame */ }, // called when done/fail/stop
},
})Multi-line live block: animated title + incrementally added items.
const sec = section("Reading files...")
// ⠋ Reading files...
sec.add("src/repl.ts")
// ⠋ Reading files...
// ⎿ src/repl.ts
sec.add("src/live.ts")
// ⠋ Reading files...
// ⎿ src/repl.ts
// ⎿ src/live.ts
sec.done("Read 2 files")
// ✓ Read 2 files
// ⎿ src/repl.ts
// ⎿ src/live.tsOptions:
const sec = section("Scanning ports...", {
spinner: "hack", // spinner animation (default: "dots")
color: s.green, // spinner color (default: s.cyan)
indent: 2, // left indentation (default: 2)
connector: "⎿", // item connector char (default: "⎿")
timer: true, // show elapsed time
collapseOnDone: true, // hide items when done (default: false)
footer: { ... }, // footer config (used by Stage)
})Replace all items at once:
sec.body("line1\nline2\nline3") // replaces all items
sec.title("Updated title") // change title while runningLifecycle: create → animate/update → done/fail/stop → frozen in scrollback
Both activity and section are pipe-aware: when not a TTY, they emit static text (no animations, no cursor manipulation).
Left/right aligned terminal status line. A single line with left segments joined by separator and right-aligned text, space-filled between sides.
import { statusbar } from "@rlabs-inc/prism"console.write(statusbar({
left: [
{ text: "hunt", color: s.cyan },
{ text: "3 messages" },
{ text: "42s", color: s.dim },
],
right: { text: "150 tokens", color: s.dim },
}))
// → " hunt │ 3 messages │ 42s 150 tokens"
// ^indent ^separator ^right-alignedstatusbar({
left: [ // left-aligned segments
"plain text", // string
{ text: "styled", color: s.cyan }, // styled
{ text: () => `${Date.now()}`, color: s.dim }, // dynamic (function)
],
right: "right side", // right-aligned content (string or segment)
separator: " │ ", // between left segments (default: " │ ")
indent: 2, // left padding (default: 2)
separatorColor: s.dim, // separator styling (default: s.dim)
})Returns a string (does not write to stdout). Use in layout active zone:
const app = layout()
app.setActive(() => ({
lines: [
statusbar({ left: [...], right: ... }), // dynamic, called per render
],
}))Two-zone terminal manager. The output zone holds content that freezes to scrollback. The active zone stays pinned at the bottom, always alive, never freezes.
import { layout, type Layout, type ActiveRender, type LayoutOptions } from "@rlabs-inc/prism"const app = layout()
// set the active zone (pinned at bottom)
app.setActive(() => ({
lines: [
s.dim("─".repeat(termWidth())),
statusbar({ left: [{ text: "hunt" }], right: "ready" }),
],
}))
// write to output zone (freezes to scrollback, active zone redraws below)
app.print("Some output text")
// stream data (buffers, flushes complete lines)
app.write("partial...")
app.write("more data\n") // flushes on newline
// update active zone (re-renders with current render function)
app.refresh()
// done — erases active zone, writes closing message
app.close("Session ended")The render function can return a cursor position for input fields:
app.setActive(() => ({
lines: [
`❯ ${inputBuffer}`,
s.dim("─".repeat(termWidth())),
],
cursor: [0, 2 + inputBuffer.length], // [row, col] within the lines
}))The layout coordinates live components (activity/section) so they render in the output zone while the active zone stays pinned below:
// activity with active zone as footer
const act = app.activity("Searching...", { timer: true })
// output zone: ⠋ Searching... (1.2s)
// active zone: ──────────── (stays pinned)
act.done("Found 5 results")
// section with active zone as footer
const sec = app.section("Reading files...", { spinner: "dots" })
sec.add("src/repl.ts")
sec.add("src/layout.ts")
sec.done("Read 2 files")
// stream connected to layout
const str = app.stream({ prefix: " ", style: s.dim })
str.write("chunk1...")
str.write("chunk2\n") // flushes " chunk1...chunk2" through layout.print
str.done()setActive(render)— stores the render function, draws the active zoneprint(text)— erases active zone, writes text to scrollback, redraws active zonewrite(data)— buffers data, flushes complete lines throughprintactivity()/section()— creates live component with active zone as footer- When live component ends,
footer.onEnd()redraws the active zone close()— erases active zone, cleans up, firesonClose
Pipe-aware: When not a TTY, the output zone works normally (direct stdout), the active zone is silent.
Buffered streaming text with two modes: standalone (direct stdout with inline partial line preview) and layout-aware (flushes through layout.print).
import { stream, type Stream, type StreamOptions } from "@rlabs-inc/prism"const str = stream()
str.write("Hello ") // shows inline: "Hello " (partial, updated via CR)
str.write("world\n") // flushes "Hello world" as complete line
str.write("next line\n") // flushes immediately
str.done("All done") // flushes remaining buffer + final text
// or
str.fail("Something broke") // flushes remaining buffer + red error textWhen connected to a layout, complete lines flush through layout.print() which coordinates with the active zone:
const app = layout()
app.setActive(() => ({ lines: ["status bar here"] }))
const str = app.stream({ prefix: " │ ", style: s.dim })
str.write("first chunk ")
str.write("second chunk\n") // → layout.print(" │ first chunk second chunk")
str.write("another line\n") // → layout.print(" │ another line")
str.done()const str = stream({
prefix: " ", // prepended to each output line
style: (text) => s.dim(text), // transform applied to each line
})const str = stream({ prefix: "downloading: " })
str.write("chunk1\n") // → "downloading: chunk1"
str.text("uploading: ") // change prefix mid-stream
str.write("chunk2\n") // → "uploading: chunk2"Line-level diff display for the terminal. Pure function — string in, string out.
import { diff } from "@rlabs-inc/prism"
// or: import { diff } from "@rlabs-inc/prism/diff"const old = `function hello() {
console.log("hello")
}`
const updated = `function hello() {
console.log("hello, world!")
return true
}`
console.log(diff(old, updated))
// Red: - console.log("hello")
// Green: + console.log("hello, world!")
// Green: + return truediff(old, updated, { filename: "src/greet.ts" })
// === src/greet.ts ===
// (diff lines follow)// Show 5 lines of context around changes (default: 3)
diff(old, updated, { context: 5 })diff(oldText: string, newText: string, options?: DiffOptions): string
interface DiffOptions {
filename?: string // header label
context?: number // context lines around changes (default: 3)
}- Uses LCS algorithm for accurate line matching
- Red (
-) for removed, green (+) for added, dim for context - Gap separators (
...) for skipped unchanged regions - Line numbers on both sides (old and new)
- Degrades to plain
+/-/markers in non-TTY (piped output) - Returns
"(no changes)"for identical inputs
Syntax-highlighted code block with filename header, line numbers, and bordered box. Composes highlight() + box().
import { filePreview } from "@rlabs-inc/prism"
// or: import { filePreview } from "@rlabs-inc/prism/file-preview"const code = `const x = 42
console.log(x)`
console.log(filePreview(code))
// ╭──────────────────╮
// │ 1 │ const x = 42 │
// │ 2 │ console.log(x)│
// ╰──────────────────╯filePreview(code, {
filename: "src/main.ts",
language: "typescript",
})
// ╭─ src/main.ts ────╮
// │ 1 │ const x = 42 │
// │ 2 │ console.log(x)│
// ╰──────────────────╯filePreview(code, { startLine: 42 })
// Line numbers start at 42 instead of 1filePreview(content: string, options?: FilePreviewOptions): string
interface FilePreviewOptions {
filename?: string // title in box header
language?: "typescript" | "javascript" | "json" | "bash" |
"sql" | "graphql" | "rust" | "auto" // default: "auto"
lineNumbers?: boolean // default: true
startLine?: number // default: 1
border?: BorderStyle // default: "rounded"
}- Delegates to
highlight()for syntax coloring (7 languages + auto-detect) - Delegates to
box()for bordered frame with title - Pure function — no I/O, no state
- Respects ANSI 16 theme colors
Every heavy operation delegates to Bun's Zig/SIMD-optimized internals:
| Bun API | What prism uses it for |
|---|---|
Bun.color() |
CSS color → ANSI conversion (.fg() / .bg() exact colors) |
Bun.stringWidth() |
Display width measurement (ANSI/emoji/CJK aware) |
Bun.stripANSI() |
Strip escape codes for pipe-safe output |
Bun.wrapAnsi() |
ANSI-preserving text wrapping |
Bun.markdown.render() |
Markdown → terminal with custom callbacks |
Bun.enableANSIColors |
TTY detection |
console.write() |
Raw stdout with no newline (used everywhere) |
process.stdin |
Raw mode keyboard input for readline/repl/prompt |
util.parseArgs |
CLI argument parsing (used by consuming tools, not prism itself) |
Every module respects the terminal environment:
| Context | Colors | Animations | Links | Badges |
|---|---|---|---|---|
| TTY (terminal) | Full ANSI | Animated | OSC 8 clickable | Styled |
Pipe (| less) |
Stripped | Static text | text (url) |
Plain [TEXT] |
This happens automatically. No configuration needed.
prism/
├── src/
│ ├── index.ts # barrel exports (50+ exports)
│ ├── writer.ts # pipe-aware output
│ ├── style.ts # composable ANSI styling
│ ├── box.ts # framed sections, dividers, headers
│ ├── table.ts # data tables
│ ├── columns.ts # multi-column layout
│ ├── markdown.ts # markdown rendering
│ ├── spinner.ts # 45 animated loaders
│ ├── progress.ts # 10 progress bar styles
│ ├── badge.ts # status indicators
│ ├── list.ts # lists, key-value, trees
│ ├── log.ts # structured logging
│ ├── text.ts # truncate, indent, pad, link, wrap
│ ├── keypress.ts # raw keyboard input
│ ├── prompt.ts # confirm, input, password, select, multiselect
│ ├── banner.ts # large block-letter text
│ ├── timer.ts # stopwatch, countdown, benchmark
│ ├── highlight.ts # syntax highlighting
│ ├── args.ts # declarative CLI argument parsing
│ ├── repl.ts # readline, REPL loop, frame system, Stage
│ ├── live.ts # activity spinners, multi-line sections
│ ├── statusbar.ts # left/right aligned status line
│ ├── layout.ts # two-zone terminal manager
│ ├── stream.ts # buffered streaming text
│ ├── exec.ts # command output viewer
│ ├── line-editor.ts # stateless line editing
│ ├── diff.ts # line-level diff display
│ └── file-preview.ts # syntax-highlighted code preview
├── demo.ts # original demo (style, box, table, markdown)
├── demo-spinner.ts # spinner catalog and showcase
├── demo-all.ts # full demo of every module
├── demo-repl.ts # simple REPL demo (no frame)
├── demo-frame.ts # Claude Code-style REPL with frame + Stage
├── package.json
└── tsconfig.json
// grab everything
import * as prism from "@rlabs-inc/prism"
// cherry-pick what you need
import { s, writeln, box, spinner, log } from "@rlabs-inc/prism"
// individual modules (for tree-shaking or clarity)
import { s } from "@rlabs-inc/prism/style"
import { spinner } from "@rlabs-inc/prism/spinner"
import { log } from "@rlabs-inc/prism/log"Light through a prism, data through the terminal.