UNPKG

@remotex-labs/xansi

Version:

A lightweight ANSI utility library for styling terminal output

706 lines (701 loc) 21.6 kB
"use strict"; var R = Object.defineProperty; var A = Object.getOwnPropertyDescriptor; var B = Object.getOwnPropertyNames; var S = Object.prototype.hasOwnProperty; var _ = (o, t) => { for (var n in t) R(o, n, { get: t[n], enumerable: !0 }); }, P = (o, t, n, r) => { if (t && typeof t == "object" || typeof t == "function") for (let e of B(t)) !S.call(o, e) && e !== n && R(o, e, { get: () => t[e], enumerable: !(r = A(t, e)) || r.enumerable }); return o; }; var I = (o) => P(R({}, "__esModule", { value: !0 }), o); // src/services/shadow.service.ts var N = {}; _(N, { ShadowRenderer: () => b }); module.exports = I(N); // src/services/ast.service.ts var y = { // 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 E(o) { if (!o) return [""]; let t = [], n = [], r = [], e = "", i = "", s = "", c = /\x1b\[[0-9;]*m/g, u = 0, l; for (; l = c.exec(o); ) { let g = o.slice(u, l.index); for (let L of Array.from(g)) t.push(e + i + L + s), e = ""; let p = l[0], h = p.slice(2, -1), v = h.indexOf(";"), d = v < 0 ? h : h.slice(0, v), C = y[d]; d === "0" ? (n.length = 0, r.length = 0, i = "", s = "", e += p) : C && n[n.length - 1] !== h ? (n.push(h), r.push(C), i = `\x1B[${n.join(";")}m`, s = `\x1B[${r.join(";")}m`) : r[r.length - 1] === h ? (n.pop(), r.pop(), i = n.length ? `\x1B[${n.join(";")}m` : "", s = r.length ? `\x1B[${r.join(";")}m` : "") : e += p, u = c.lastIndex; } let a = o.slice(u); for (let g of Array.from(a)) t.push(e + i + g + s), e = ""; return t; } // src/components/ansi.component.ts function m(o) { process.stdout.write ? process.stdout.write(o) : console.log(o); } function w(o, t = 0) { return `\x1B[${o};${t}H`; } function x(o) { return o.replace(/\x1b\[[0-9;]*m/g, ""); } var f = { /** * 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 b = 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(t, n, r, e) { this.terminalHeight = t; this.terminalWidth = n; this.topPosition = r; this.leftPosition = e; } /** * 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(t) { this.topPosition = t; } /** * 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(t) { this.leftPosition = t; } /** * 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(t) { this.terminalWidth = t; } /** * 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(t) { this.terminalHeight = t; } /** * 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(t) { let n = t < 0 ? this.scrollPosition + t : t, r = Math.min(Math.max(n, 0), this.contentBuffer.length - 1); r !== this.scrollPosition && (this.scrollPosition = r, 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 t = ""; for (let n = 0; n < this.viewBuffer.length; n++) t += this.moveCursor(n, 0), t += f.CLEAR_LINE; t += f.CURSOR_HOME, m(t); } /** * 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(t, n, r, e = !1) { if (t < 0 || n >= this.terminalWidth) return; e && (this.contentBuffer[t] = []), r = r.split(` `)[0]; let i = this.contentBuffer[t] ??= [], s = x(r), c = E(r); for (let u = 0; u < s.length; u++) { let l = n + u, a = c[u]; i[l] || (i[l] = { char: "", dirty: !0 }), i[l].char !== a && (i[l].char = a, i[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(t, n, r, e = !1) { let i = Array.isArray(r) ? r : r.split(` `); for (let s = 0; s < i.length; s++) { let c = t + s; this.writeText(c, n, i[s], e); } } /** * 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(t = !1) { let n = Math.min(this.scrollPosition, this.contentBuffer.length), r = Math.min(n + this.terminalHeight, this.contentBuffer.length); if (n >= r) return; let e = { force: t, output: "", viewLine: [], screenRow: 1, contentLine: [] }; for (let i = n; i < r; i++, e.screenRow++) e.contentLine = this.contentBuffer[i], e.contentLine && (e.viewLine = this.viewBuffer[e.screenRow] ??= [], this.renderLine(e), e.viewLine.length > e.contentLine.length && (e.output += f.CLEAR_LINE, e.viewLine.length = e.contentLine.length)); if (r >= this.contentBuffer.length) { this.viewBuffer.length = e.screenRow; for (let i = e.screenRow; i <= this.terminalHeight; i++) e.output += this.moveCursor(i, 0), e.output += f.CLEAR_LINE; } e.output += this.moveCursor(this.viewBuffer.length, this.terminalWidth), m(e.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 t = ` `; for (let n = 0; n < this.contentBuffer.length; n++) { let r = this.contentBuffer[n]; if (r) { t += f.CLEAR_LINE; for (let e = 0; e < r.length; e++) { let i = r[e]; t += i?.char ?? " "; } t += ` `; } } this.clear(), m(t); } /** * 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(t) { let n = !0, r = Math.min(this.terminalWidth, t.contentLine.length); for (let e = 0; e < r; e++) { let i = t.contentLine[e]; if (i && !i.dirty && i.char === t.viewLine[e] && !t.force) { n = !0; continue; } if (n && (t.output += this.moveCursor(t.screenRow, e + 1), n = !1), !i) { t.viewLine[e] = " ", t.output += " "; continue; } t.viewLine[e] = i.char, t.output += i.char, i.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(t, n = 0) { return w(t + this.topPosition, n + this.leftPosition); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ShadowRenderer }); //# sourceMappingURL=shadow.service.js.map