@remotex-labs/xansi
Version:
A lightweight ANSI utility library for styling terminal output
805 lines (800 loc) • 24.3 kB
JavaScript
// src/services/ast.service.ts
var P = {
// Text styles
1: "22",
2: "22",
3: "23",
4: "24",
5: "25",
7: "27",
8: "28",
9: "29",
// Foreground colors
30: "39",
31: "39",
32: "39",
33: "39",
34: "39",
35: "39",
36: "39",
37: "39",
38: "39",
90: "39",
91: "39",
92: "39",
93: "39",
94: "39",
95: "39",
96: "39",
97: "39",
// Background colors
40: "49",
41: "49",
42: "49",
43: "49",
44: "49",
45: "49",
46: "49",
47: "49",
48: "49",
100: "49",
101: "49",
102: "49",
103: "49",
104: "49",
105: "49",
106: "49",
107: "49",
// Resets
0: null,
22: null,
23: null,
24: null,
25: null,
27: null,
28: null,
29: null,
39: null,
49: null
};
function v(o) {
if (!o) return [""];
let e = [], n = [], i = [], t = "", r = "", s = "", u = /\x1b\[[0-9;]*m/g, c = 0, l;
for (; l = u.exec(o); ) {
let y = o.slice(c, l.index);
for (let _ of Array.from(y))
e.push(t + r + _ + s), t = "";
let C = l[0], f = C.slice(2, -1), w = f.indexOf(";"), R = w < 0 ? f : f.slice(0, w), B = P[R];
R === "0" ? (n.length = 0, i.length = 0, r = "", s = "", t += C) : B && n[n.length - 1] !== f ? (n.push(f), i.push(B), r = `\x1B[${n.join(";")}m`, s = `\x1B[${i.join(";")}m`) : i[i.length - 1] === f ? (n.pop(), i.pop(), r = n.length ? `\x1B[${n.join(";")}m` : "", s = i.length ? `\x1B[${i.join(";")}m` : "") : t += C, c = u.lastIndex;
}
let a = o.slice(c);
for (let y of Array.from(a))
e.push(t + r + y + s), t = "";
return e;
}
// src/components/ansi.component.ts
function b(o) {
process.stdout.write ? process.stdout.write(o) : console.log(o);
}
function A(o, e = 0) {
return `\x1B[${o};${e}H`;
}
function x(o) {
return o.replace(/\x1b\[[0-9;]*m/g, "");
}
var g = {
/**
* Clears from the cursor to the end of the line
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
CLEAR_LINE: "\x1B[K",
/**
* Moves the cursor to the "home" position (row 1, column 1).
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.2.0
*/
CURSOR_HOME: "\x1B[H",
/**
* Hides the cursor
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
HIDE_CURSOR: "\x1B[?25l",
/**
* Shows the cursor
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
SHOW_CURSOR: "\x1B[?25h",
/**
* Saves the current cursor position
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
SAVE_CURSOR: "\x1B[s",
/**
* Restores the cursor to the previously saved position
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
RESTORE_CURSOR: "\x1B[u",
/**
* Resets the terminal to its initial state (RIS - Reset to Initial State).
* Clears screen, scrollback buffer, and most settings.
* This is a "hard reset" escape code.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#Reset_(RIS)
* @since 1.0.0
*/
RESET_TERMINAL: "\x1Bc",
/**
* Clears the screen from the cursor position to the end of the screen.
*
* Equivalent to `ESC[J` or `ESC[0J`.
* Leaves the scrollback buffer intact.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
CLEAR_SCREEN_DOWN: "\x1B[0J",
/**
* Clears the screen from the cursor position to the beginning of the screen.
*
* Equivalent to `ESC[1J`.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
CLEAR_SCREEN_UP: "\x1B[1J",
/**
* Clears the entire screen and moves the cursor to the home position (top-left).
*
* Equivalent to `ESC[2JESC[H`.
* Does not clear the scrollback buffer.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
CLEAR_SCREEN: "\x1B[2J\x1B[H",
/**
* Clears the entire screen and deletes all lines saved in the scrollback buffer.
*
* Equivalent to `ESC[3JESC[H`.
* Supported in xterm and many modern terminal emulators.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.0.0
*/
CLEAR_SCREEN_FULL: "\x1B[3J\x1B[H",
/**
* Moves the cursor to the beginning of the current line (column 1).
*
* @remarks
* Unlike `\r` (carriage return), this is an ANSI escape sequence that
* explicitly positions the cursor at column 1, regardless of terminal state.
* It does not affect the row — only the horizontal position is changed.
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
* @since 1.3.0
*/
CURSOR_LINE_START: "\x1B[1G"
};
// src/services/shadow.service.ts
var E = class {
/**
* Creates a new ShadowRenderer instance for terminal-based UI rendering
*
* @param terminalHeight - The height of the terminal viewport in rows
* @param terminalWidth - The width of the terminal viewport in columns
* @param topPosition - The top-offset position within the terminal
* @param leftPosition - The left offset position within the terminal
*
* @since 1.0.0
*/
constructor(e, n, i, t) {
this.terminalHeight = e;
this.terminalWidth = n;
this.topPosition = i;
this.leftPosition = t;
}
/**
* Current scroll position in the content
*
* @remarks
* Tracks the index of the first row visible at the top of the viewport.
* A value of 0 indicates the content is not scrolled, while higher values
* indicate the content has been scrolled down by that many rows.
*
* This value is used during rendering to determine which portion of the
* contentBuffer to display in the visible area of the terminal.
*
* @see scroll - The getter for accessing this value
*
* @private
*/
scrollPosition = 0;
/**
* Internal buffer representing the current visible state of the terminal
*
* @remarks
* This two-dimensional array stores the actual rendered content as it appears in the terminal.
* It's organized in a row-major format, with each row containing an array of strings.
*
* Unlike contentBuffer which stores full cell information, viewBuffer only contains
* the string representation of each cell. This buffer is used for diffing against
* new content to determine what needs to be redrawn during render operations,
* allowing for efficient partial updates to the terminal display.
*
* @private
*/
viewBuffer = [];
/**
* Internal buffer containing the content to be rendered
*
* @remarks
* This two-dimensional array stores all content cells in a row-major format,
* where each row is an array of CellInterface objects representing individual
* characters with their associated style information.
*
* The outer array represents rows, while the inner arrays represent columns within each row.
* This structure allows for efficient access and manipulation of cell data during
* the rendering process.
*
* @private
* @see CellInterface
*/
contentBuffer = [];
/**
* Gets the top position offset of the renderer in the terminal
*
* @returns The row offset from the top edge of the terminal
*
* @since 1.0.0
*/
get top() {
return this.topPosition;
}
/**
* Gets the left position offset of the renderer in the terminal
*
* @returns The column offset from the left edge of the terminal
*
* @since 1.0.0
*/
get left() {
return this.leftPosition;
}
/**
* Gets the width of the renderer's viewport
*
* @returns The number of columns visible in the renderer's viewport
*
* @since 1.0.0
*/
get width() {
return this.terminalWidth;
}
/**
* Gets the height of the renderer's viewport
*
* @returns The number of rows visible in the renderer's viewport
*
* @since 1.0.0
*/
get height() {
return this.terminalHeight;
}
/**
* Gets the current scroll position
*
* @returns The current row index at the top of the viewport
*
* @since 1.0.0
*/
get scroll() {
return this.scrollPosition;
}
/**
* Sets the top position offset of the renderer in the terminal
*
* @param top - The row offset from the top edge of the terminal
*
* @remarks
* This setter updates the vertical positioning of the renderer's viewport
* within the terminal. The top position is used as an offset when calculating
* absolute cursor positions during rendering operations.
*
* Note that changing the top position does not automatically trigger a re-render.
* You should call the render() method separately after changing the position.
*
* @since 1.0.0
*/
set top(e) {
this.topPosition = e;
}
/**
* Sets the left position offset of the renderer in the terminal
*
* @param left - The column offset from the left edge of the terminal
*
* @remarks
* This setter updates the horizontal positioning of the renderer's viewport
* within the terminal. The left position is used as an offset when calculating
* absolute cursor positions during rendering operations.
*
* Note that changing the left position does not automatically trigger a re-render.
* You should call the render() method separately after changing the position.
*
* @since 1.0.0
*/
set left(e) {
this.leftPosition = e;
}
/**
* Sets the width of the renderer's viewport
*
* @param terminalWidth - The number of columns visible in the renderer's viewport
*
* @remarks
* This setter updates the internal width property which controls how many
* columns are rendered during the rendering process. This should be updated
* whenever the terminal or display area is resized.
*
* Note that changing the width does not automatically trigger a re-render.
* You should call the render() method separately after changing dimensions.
*
* @since 1.0.0
*/
set width(e) {
this.terminalWidth = e;
}
/**
* Sets the height of the renderer's viewport
*
* @param terminalHeight - The number of rows visible in the renderer's viewport
*
* @remarks
* This setter updates the internal height property which controls how many
* rows are rendered during the rendering process. This should be updated
* whenever the terminal or display area is resized.
*
* Note that changing the height does not automatically trigger a re-render.
* You should call the render() method separately after changing dimensions.
*
* @since 1.0.0
*/
set height(e) {
this.terminalHeight = e;
}
/**
* Sets the scroll position and renders the updated view
*
* @param position - New scroll position or relative movement (if negative)
*
* @remarks
* This setter handles both absolute and relative scrolling:
* - Positive values set the absolute scroll position
* - Negative values move relative to the current position
*
* If the requested position scrolls beyond the end of the content,
* the operation is ignored. After setting a valid scroll position,
* the view is automatically re-rendered.
*
* @example
* ```ts
* // Set absolute scroll position to row 10
* renderer.scroll = 10;
*
* // Scroll up 3 rows (relative movement)
* renderer.scroll = -3;
* ```
*
* @since 1.0.0
*/
set scroll(e) {
let n = e < 0 ? this.scrollPosition + e : e, i = Math.min(Math.max(n, 0), this.contentBuffer.length - 1);
i !== this.scrollPosition && (this.scrollPosition = i, this.render());
}
/**
* Clears all content from the renderer and resets its internal state.
*
* @remarks
* This method removes all entries from both the `viewBuffer` and `contentBuffer`,
* effectively resetting the renderer to its initial empty state.
*
* @returns void — This method does not produce a return value.
*
* @example
* ```ts
* // Completely reset the renderer's buffers
* renderer.clear();
* ```
*
* @since 1.0.0
*/
clear() {
this.viewBuffer = [], this.contentBuffer = [];
}
/**
* Clears the visible content from the terminal screen
*
* @returns Nothing
*
* @remarks
* This method builds a string of ANSI escape sequences that move the cursor
* to the beginning of each line in the view buffer and then clears each line.
* It doesn't reset the internal buffers, only clears the visible output.
*
* @example
* ```ts
* // Clear just the screen output without resetting buffers
* renderer.clearScreen();
* ```
*
* @since 1.0.0
*/
clearScreen() {
let e = "";
for (let n = 0; n < this.viewBuffer.length; n++)
e += this.moveCursor(n, 0), e += g.CLEAR_LINE;
e += g.CURSOR_HOME, b(e);
}
/**
* Writes text to the specified position in the content buffer
*
* @param row - Row position (0-based)
* @param column - Column position (0-based)
* @param text - Text to write
* @param clean - Whether to clear existing content first
*
* @remarks
* This method only updates the internal content buffer and marks cells as dirty.
* It doesn't immediately render to the screen - call render() to display changes.
* Only the first line of multi-line text is processed, and a text is truncated
* if it exceeds terminal boundaries.
*
* @example
* ```ts
* // Write text in the top-left corner
* renderer.writeText(0, 0, "Hello world");
*
* // Write text at position (5,10) and clear any existing content
* renderer.writeText(5, 10, "Menu Options", true);
*
* // Display the changes
* renderer.render();
* ```
*
* @since 1.0.0
*/
writeText(e, n, i, t = !1) {
if (e < 0 || n >= this.terminalWidth) return;
t && (this.contentBuffer[e] = []), i = i.split(`
`)[0];
let r = this.contentBuffer[e] ??= [], s = x(i), u = v(i);
for (let c = 0; c < s.length; c++) {
let l = n + c, a = u[c];
r[l] || (r[l] = { char: "", dirty: !0 }), r[l].char !== a && (r[l].char = a, r[l].dirty = !0);
}
}
/**
* Writes multi-line text into the content buffer.
*
* @param row - Starting row position (0-based).
* @param column - Starting column position (0-based).
* @param text - Text to write, either as a string (may contain newlines) or an array of strings (one per line).
* @param clean - Whether to clear existing content before writing.
* Only applies to the first written line; subsequent lines always append.
*
* @remarks
* Unlike {@link writeText}, this method supports writing multiple lines
* at once. If the input is a single string, it is split by newline characters.
* If an array of strings is provided, each element represents a line.
*
* This method automatically allocates new rows in the content buffer
* as needed, making it suitable for rendering large blocks of text
* that can later be scrolled with the renderer.
*
* @example
* Writing a block of text from a single string:
* ```ts
* renderer.writeBlock(0, 0, "Line 1\nLine 2\nLine 3");
* renderer.render();
* ```
*
* @example
* Writing a block of text from an array:
* ```ts
* renderer.writeBlock(2, 4, ["Indented line 1", "Indented line 2"]);
* renderer.render();
* ```
*
* @since 1.2.0
*/
writeBlock(e, n, i, t = !1) {
let r = Array.isArray(i) ? i : i.split(`
`);
for (let s = 0; s < r.length; s++) {
let u = e + s;
this.writeText(u, n, r[s], t);
}
}
/**
* Renders the content buffer to the screen using optimized terminal output
*
* @param force - Forces all cells to be redrawn, even if they haven't changed
*
* @remarks
* This method performs an optimized render by:
* - Only rendering the visible portion of the content buffer based on scroll position
* - Only updating cells that have been marked as dirty (unless force=true)
* - Clearing trailing content from previous renders
* - Repositioning the cursor to the bottom right when finished
*
* If the content buffer is empty or all content is scrolled out of view,
* the method will return early without performing any operations.
*
* @example
* ```ts
* // Normal render - only updates what changed
* renderer.render();
*
* // Force redraw of everything, useful after terminal resize
* renderer.render(true);
* ```
*
* @since 1.0.0
*/
render(e = !1) {
let n = Math.min(this.scrollPosition, this.contentBuffer.length), i = Math.min(n + this.terminalHeight, this.contentBuffer.length);
if (n >= i) return;
let t = {
force: e,
output: "",
viewLine: [],
screenRow: 1,
contentLine: []
};
for (let r = n; r < i; r++, t.screenRow++)
t.contentLine = this.contentBuffer[r], t.contentLine && (t.viewLine = this.viewBuffer[t.screenRow] ??= [], this.renderLine(t), t.viewLine.length > t.contentLine.length && (t.output += g.CLEAR_LINE, t.viewLine.length = t.contentLine.length));
if (i >= this.contentBuffer.length) {
this.viewBuffer.length = t.screenRow;
for (let r = t.screenRow; r <= this.terminalHeight; r++)
t.output += this.moveCursor(r, 0), t.output += g.CLEAR_LINE;
}
t.output += this.moveCursor(this.viewBuffer.length, this.terminalWidth), b(t.output);
}
/**
* Flushes the current content buffer directly to the terminal.
*
* @remarks
* This method writes all rows from {@link contentBuffer} to `stdout`,
* appending a newline after each row. It also clears each line with
* {@link ANSI.CLEAR_LINE} before writing, ensuring no stale characters
* remain on screen.
*
* Once flushing is complete, both {@link viewBuffer} and {@link contentBuffer}
* are reset to empty arrays, preparing the renderer for the next cycle.
*
* Unlike {@link render}, this method bypasses diffing and incremental
* optimizations. It always emits the full buffer to the terminal, which
* guarantees a complete reset of the visible state but may be less
* efficient for large outputs.
*
* @returns void — This method does not produce a return value.
*
* @example
* ```ts
* // Write a block of text to the buffer
* renderer.writeBlock(0, 0, "Hello\nWorld");
* renderer.clearScreen();
*
* // Forcefully flush everything to the terminal
* renderer.flushToTerminal();
* ```
*
* @since 1.2.0
*/
flushToTerminal() {
let e = `
`;
for (let n = 0; n < this.contentBuffer.length; n++) {
let i = this.contentBuffer[n];
if (i) {
e += g.CLEAR_LINE;
for (let t = 0; t < i.length; t++) {
let r = i[t];
e += r?.char ?? " ";
}
e += `
`;
}
}
this.clear(), b(e);
}
/**
* Renders a single line of content to the output buffer
*
* @param context - The current rendering context
*
* @remarks
* This private method handles the optimization of terminal output by:
* - Skipping cells that haven't changed since the last render
* - Only moving the cursor when necessary
* - Updating the view buffer to reflect what's been rendered
* - Clearing the dirty flag on cells after they're processed
*
* The context object contains all states needed for the rendering process,
* including the accumulating output string and references to the current
* content and view lines.
*
* @private
* @since 1.0.0
*/
renderLine(e) {
let n = !0, i = Math.min(this.terminalWidth, e.contentLine.length);
for (let t = 0; t < i; t++) {
let r = e.contentLine[t];
if (r && !r.dirty && r.char === e.viewLine[t] && !e.force) {
n = !0;
continue;
}
if (n && (e.output += this.moveCursor(e.screenRow, t + 1), n = !1), !r) {
e.viewLine[t] = " ", e.output += " ";
continue;
}
e.viewLine[t] = r.char, e.output += r.char, r.dirty = !1;
}
}
/**
* Generates an ANSI escape sequence to move the cursor to a position
*
* @param row - The row position relative to renderer's viewport
* @param column - The column position relative to renderer's viewport (defaults to 0)
* @returns ANSI escape sequence string for cursor positioning
*
* @remarks
* This private helper method translates relative viewport coordinates to
* absolute terminal coordinates by adding the renderer's top and left
* position offsets. The returned value is a string containing the
* appropriate ANSI escape sequence.
*
* @private
* @since 1.0.0
*/
moveCursor(e, n = 0) {
return A(e + this.topPosition, n + this.leftPosition);
}
};
// src/providers/styles.provider.ts
var S = {
dim: [2, 22],
bold: [1, 22],
reset: [0, 0],
hidden: [8, 28],
inverse: [7, 27]
}, L = {
red: [31, 39],
gray: [90, 39],
blue: [34, 39],
cyan: [36, 39],
black: [30, 39],
white: [37, 39],
green: [32, 39],
yellow: [33, 39],
magenta: [35, 39],
redBright: [91, 39],
blueBright: [94, 39],
cyanBright: [96, 39],
whiteBright: [97, 39],
greenBright: [92, 39],
blackBright: [90, 39],
yellowBright: [93, 39],
magentaBright: [95, 39],
darkGray: ["38;5;238", 39],
lightGray: ["38;5;252", 39],
lightCyan: ["38;5;81", 39],
lightCoral: ["38;5;203", 39],
oliveGreen: ["38;5;149", 39],
deepOrange: ["38;5;166", 39],
brightPink: ["38;5;197", 39],
lightOrange: ["38;5;215", 39],
burntOrange: ["38;5;208", 39],
lightYellow: ["38;5;230", 39],
canaryYellow: ["38;5;227", 39],
lightGoldenrodYellow: ["38;5;221", 39]
}, T = {
bgRed: [41, 49],
bgBlue: [44, 49],
bgCyan: [46, 49],
bgGray: [100, 49],
bgBlack: [40, 49],
bgGreen: [42, 49],
bgWhite: [47, 49],
bgYellow: [43, 49],
bgMagenta: [45, 49],
bgRedBright: [101, 49],
bgBlueBright: [104, 49],
bgCyanBright: [106, 49],
bgBlackBright: [100, 49],
bgWhiteBright: [107, 49],
bgGreenBright: [102, 49],
bgYellowBright: [103, 49],
bgMagentaBright: [105, 49]
};
// src/components/xterm.component.ts
var m = "\x1B[", p = "m";
function $(o, e) {
if (globalThis.NO_COLOR) return e;
let n = o.length;
if (n === 0) return e;
if (n === 1)
return `${m}${o[0][0]}${p}${e}${m}${o[0][1]}${p}`;
let i = new Array(n), t = new Array(n);
for (let r = 0; r < n; r++)
t[r] = `${m}${o[r][0]}${p}`, i[n - r - 1] = `${m}${o[r][1]}${p}`;
return t.concat(e, i).join("");
}
function d(o, e, n, i) {
if (typeof e != "number" || typeof n != "number" || typeof i != "number")
throw new Error(`RGB values must be numbers, received: r=${typeof e}, g=${typeof n}, b=${typeof i}`);
let t = o === "fg" ? 38 : 48, r = o === "fg" ? 39 : 49;
return [`${t};2;${e};${n};${i}`, r];
}
function I(o) {
let e = o.replace(/^#/, "").toLowerCase();
if (!/^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(e))
throw new Error(`Invalid hex color format: "${o}". Expected 3 or 6 hex digits.`);
if (e.length === 3) {
let r = parseInt(e[0] + e[0], 16), s = parseInt(e[1] + e[1], 16), u = parseInt(e[2] + e[2], 16);
return [r, s, u];
}
let n = parseInt(e.slice(0, 2), 16), i = parseInt(e.slice(2, 4), 16), t = parseInt(e.slice(4, 6), 16);
return [n, i, t];
}
function h(o = []) {
let e = (...t) => {
if (Array.isArray(t[0]) && "raw" in t[0]) {
let [r, ...s] = t, u = r.reduce(
(c, l, a) => c + l + (a < s.length ? String(s[a] ?? "") : ""),
""
);
return $(o, u);
}
return $(o, t.join(" "));
}, n = {
// RGB foreground color
rgb: (t, r, s) => h([...o, d("fg", t, r, s)]),
bgRgb: (t, r, s) => h([...o, d("bg", t, r, s)]),
hex: (t) => h([...o, d("fg", ...I(t))]),
bgHex: (t) => h([...o, d("bg", ...I(t))])
}, i = {
...S,
...L,
...T
};
return new Proxy(e, {
get(t, r) {
if (typeof r != "string")
throw new Error(`Invalid property: ${String(r)}`);
return r in i ? h([...o, i[r]]) : r in n ? n[r] : Reflect.get(t, r);
}
});
}
var U = /* @__PURE__ */ h();
export {
g as ANSI,
E as ShadowRenderer,
A as moveCursor,
x as stripAnsi,
b as writeRaw,
U as xterm
};
//# sourceMappingURL=index.js.map