Your terminal speaks a hidden language.
Watch npm install. Notice how progress bars update in place? How text overwrites itself? How the cursor jumps around?
That's not magic. It's escape sequences. And processing them correctly is surprisingly complex.
Terminal output isn't just text. It's a stream of:
\r, \n, \b, \t\x1b[32m, \x1b[2J, \x1b[1;5HYour terminal interprets all of this in real-time, maintaining cursor position, handling overwrites, managing screen regions.
Installing packages...
Progress: [ ] 0%
Progress: [#### ] 40%
Progress: [######## ] 80%
Progress: [##########] 100%
Done!
In raw form, this contains carriage returns that overwrite previous lines. You want:
Installing packages...
Progress: [##########] 100%
Done!
You're testing a CLI tool. It outputs ANSI colors, progress spinners, cursor movements. How do you assert the final visible state?
Feed terminal output to an LLM. It needs clean text, not escape sequences. \x1b[32m means nothing to GPT.
A TypeScript library that simulates terminal behavior:
import { TerminalTextRender } from "terminal-render";
const renderer = new TerminalTextRender();
renderer.write("Progress: 0%\rProgress: 50%\rProgress: 100%");
console.log(renderer.render()); // "Progress: 100%"
The carriage returns are processed. Only the final state remains.
The renderer maintains a virtual screen - a 2D grid of characters with a cursor position:
┌─────────────────────────────────┐
│ Line 0: Hello World │ ← cursor position tracked
│ Line 1: Progress: [##########] │
│ Line 2: │
└─────────────────────────────────┘
↑ cursor column
\x1b[s and \x1b[uEach character is processed in order:
Input: "Hello\x1b[5D" (Hello, then move cursor left 5)
State: cursor moves back to start
Result: Next characters overwrite "Hello"
| Sequence | Description | Example |
|---|---|---|
\x1b[nA |
Cursor up n lines | \x1b[2A - up 2 |
\x1b[nB |
Cursor down n lines | \x1b[3B - down 3 |
\x1b[nC |
Cursor forward n cols | \x1b[5C - right 5 |
\x1b[nD |
Cursor backward n cols | \x1b[5D - left 5 |
\x1b[n;mH |
Cursor to row n, col m | \x1b[1;1H - home |
\x1b[nG |
Cursor to column n | \x1b[10G - col 10 |
| Sequence | Description |
|---|---|
\x1b[nE |
Cursor to start of n lines down |
\x1b[nF |
Cursor to start of n lines up |
| Sequence | Description |
|---|---|
\x1b[0J |
Clear from cursor to end of screen |
\x1b[1J |
Clear from start of screen to cursor |
\x1b[2J |
Clear entire screen |
\x1b[0K |
Clear from cursor to end of line |
\x1b[1K |
Clear from start of line to cursor |
\x1b[2K |
Clear entire line |
| Sequence | Description |
|---|---|
\x1b[s |
Save cursor position |
\x1b[u |
Restore cursor position |
| Char | Description |
|---|---|
\r |
Carriage return - move to column 0 |
\n |
Line feed - move to next line |
\b |
Backspace - move cursor back one |
\t |
Tab - move to next 8-column stop |
const renderer = new TerminalTextRender();
renderer.write("Downloading...\n");
renderer.write("Progress: [ ] 0%\r");
renderer.write("Progress: [### ] 30%\r");
renderer.write("Progress: [###### ] 60%\r");
renderer.write("Progress: [##########] 100%\r");
renderer.write("\nComplete!");
console.log(renderer.render());
// Downloading...
// Progress: [##########] 100%
// Complete!
const renderer = new TerminalTextRender();
renderer.write("Line 1\nLine 2\nLine 3");
renderer.write("\x1b[2A"); // Move up 2 lines
renderer.write("\x1b[7G"); // Move to column 7
renderer.write("MODIFIED"); // Overwrite
console.log(renderer.render());
// Line 1 MODIFIED
// Line 2
// Line 3
const renderer = new TerminalTextRender();
renderer.write("First content\nMore content");
renderer.write("\x1b[2J"); // Clear screen
renderer.write("\x1b[1;1H"); // Home position
renderer.write("Fresh start");
console.log(renderer.render());
// Fresh start
const renderer = new TerminalTextRender();
renderer.write("Spinner: |\b/\b-\b\\\b|");
console.log(renderer.render());
// Spinner: |
Each backspace moves cursor back, next char overwrites.
import { readFile } from "fs/promises";
import { TerminalTextRender } from "terminal-render";
const rawLog = await readFile("npm-install.log", "utf8");
const renderer = new TerminalTextRender();
renderer.write(rawLog);
const cleanLog = renderer.render();
// Progress bars resolved to final state
// ANSI colors stripped
// Clean, readable output
import { execa } from "execa";
import { TerminalTextRender } from "terminal-render";
const { stdout } = await execa("my-cli", ["--verbose"]);
const renderer = new TerminalTextRender();
renderer.write(stdout);
const result = renderer.render();
expect(result).toContain("Success");
import { TerminalTextRender } from "terminal-render";
async function runCommandForAI(command: string) {
const { stdout, stderr } = await exec(command);
const renderer = new TerminalTextRender();
renderer.write(stdout);
renderer.write(stderr);
// Clean output suitable for LLM analysis
return renderer.render();
}
const output = await runCommandForAI("npm test");
const analysis = await askGPT(`Analyze this test output:\n${output}`);
const renderer = new TerminalTextRender();
// Stream chunks as they arrive
socket.on("data", (chunk) => {
renderer.write(chunk);
});
// Get clean snapshot anytime
setInterval(() => {
updateDashboard(renderer.render());
}, 1000);
class TerminalTextRender {
write(data: string): TerminalTextRender; // Process input
render(): string; // Get output
clear(): void; // Reset state
}
import { createTerminalLogManager } from "terminal-render";
const manager = createTerminalLogManager();
manager.write("...");
console.log(manager.render());
renderer.write("AAAAA");
renderer.write("\r");
renderer.write("BB");
// Result: "BBAAA" - partial overwrite
renderer.write("Line 1\nLine 2\nLine 3\nLine 4");
renderer.write("\x1b[3A"); // Up 3
renderer.write("X");
// X appears on Line 1
renderer.write("A\tB");
// "A B" - tabs expand to 8-column stops
renderer.write("\n\n\nText");
// Three empty lines, then "Text"
This library focuses on text state, not visual styling:
\x1b[32m) - Stripped, not renderedThe goal is extracting readable text, not full terminal emulation.
npm install terminal-render
import { TerminalTextRender } from "terminal-render";
const renderer = new TerminalTextRender();
renderer.write(yourTerminalOutput);
console.log(renderer.render());
Terminals evolved from physical teletypes. Their protocol is ancient but still everywhere.
Understanding escape sequences means understanding how half of developer tooling actually works. Build logs, test runners, package managers - they all speak this language.
terminal-render is the Rosetta Stone.
Install: npm install terminal-render
GitHub: github.com/snomiao/terminal-render
Related: claude-yes - where this library was born
Snowstar Miao builds parsers for the protocols we take for granted.