UNPKG

@remotex-labs/xansi

Version:

A lightweight ANSI utility library for styling terminal output

805 lines (800 loc) 24.3 kB
// 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