UNPKG

@syncify/ansi

Version:

ANSI Colors, Symbols and TUI related terminal enchancements for Syncify.

355 lines (274 loc) 7.06 kB
import { emitKeypressEvents } from 'node:readline'; import { type Options as WrapOptions } from 'wrap-ansi'; import wrap from 'wrap-ansi'; import { glue } from '@syncify/glue'; import { WSP } from './characters'; import { Tree } from './tree'; /** * Options for the Scroller class. */ interface ScrollerOptions { /** * The content to display in the scrollable area. * * @default undefined */ input?: string[] | string; /** * Whether or not each write to `stdout` should append `\n` * character and simulate native `console.log`. * * @default true */ newline?: boolean; /** * Whether or not Tree line prefixing applies * * @default false */ tree?: boolean; /** * Starting X position of scroller * * @default 0 */ xPos?: number; /** * Position Y position of scroller * * @default 0 */ yPos?: number /** * The width of the scrollable box * * @default process.stdout.columns - 15; */ width?: number; /** * The height of the scrollable box * * @default process.stdout.rows - 15; */ height?: number /** * Whether or not ansi word wrapping applies * * @default false */ wrap?: boolean; }; /** * A scrollable area that can be printed to the console. */ class Scroller { /** * The lines of content in the scrollable area wrapped * according to the specified {@link ScrollerOptions} and * split into an array of lines. */ lines: string[] = []; /** * The maximum height */ maxHeight: number; /** * The content to display in the scrollable area. * * @default undefined */ content: string; /** * Whether or not each write to `stdout` should append `\n` * character and simulate native `console.log`. */ newline: string; /** * The position of the first line to display in the scrollable area. */ position: number = 0; /** * The tree line prefix character */ private prefix: string; /** * The line suffix, when `newline` is `true` this is `\n` otherwise empty string */ private suffix: string; /** * An empty line */ private empty: string; /** * Word Wrap options */ private wrap: WrapOptions = { hard: false, trim: true, wordWrap: true }; /** * The options for the Scroller instance. */ options: ScrollerOptions = { input: undefined, newline: true, height: process.stdout.rows - 20, width: process.stdout.columns - 20, wrap: false, tree: false, xPos: 0, yPos: 0 }; /** * The height of the content */ get height (): number { return this.options.height; } /** * The width of the content */ get width (): number { return this.options.width; } /** * X position of scroller */ get x (): number { return this.options.xPos; } /** * Set X position of scroller */ set x (x: number) { this.options.xPos = x; } /** * Y position of scroller */ get y (): number { return this.options.yPos; } /** * Set Y position of scroller */ set y (y: number) { this.options.yPos = y; } /** * Creates a new Scroller instance. * @param options - The options for the Scroller instance. */ constructor (options?: ScrollerOptions) { Object.assign(this.options, options); this.prefix = this.options.tree ? Tree.line : ''; this.suffix = this.options.newline ? '\n' : ''; this.empty = glue(Array(this.width).fill(WSP)); if (typeof this.options.input === 'string') { this.content = this.options.input; } else { this.content = glue.nl(this.options.input); this.lines = this.options.input; } this.options.height = 'height' in options ? options.height : this.content.split('\n').length; this.maxHeight = this.content.split('\n').length - this.options.height - 1; } setKeypress (y: number, max: number) { process.stdin.setRawMode(true); emitKeypressEvents(process.stdin); return process.stdin.on('keypress', (str, key) => { if ( key.sequence === '\u0003' || key.sequence === '\u0004' || key.sequence === '\u001a') { process.exit(0); } else if (key.name === 'up') { if (this.position === 0) return; this.scroll(-2).print(); process.stdout.cursorTo(0, y + 2); } else if (key.name === 'down') { if (this.position >= max) return; this.scroll(2).print(); process.stdout.cursorTo(0, y + 2); } }); } /** * Sets the content to display in the scrollable area. */ setContent (content: string): Scroller { this.content = content; this.resetLines(); return this; } /** * Sets the `x` and/or `y`position of the scrollable area. */ setPosition (position: { x?: number, y?: number } = {}): Scroller { if ('x' in position) this.x = position.x; if ('y' in position) this.y = position.y; this.resetLines(); return this; } /** * Sets the size of the scroller area. */ setSize (size: { width?: number; height?: number }): Scroller { if ('width' in size) this.options.width = size.width; if ('height' in size) this.options.height = size.height; this.resetLines(); return this; } /** * Sets the options for wrapping the content in the scrollable area. */ setWrap (wrapOptions: boolean | WrapOptions): Scroller { if (typeof wrapOptions === 'boolean') { this.options.wrap = wrapOptions; } else { if (!this.options.wrap) this.options.wrap = true; Object.assign(this.wrap, wrapOptions); } if (this.options.wrap) this.resetLines(); return this; } /** * Prints the scrollable area to the console. * @returns The Scroller instance. */ print (): this { if (this.lines.length === 0) this.splitContentIntoLines(); this.clear(); // Clear the area. process.stdout.cursorTo(this.x, this.y); for (let i = 0; i < this.height; i++) { const line = this.lines[i + this.position]; process.stdout.write(this.prefix + (line ?? this.empty) + this.suffix); } return this; } /** * Scrolls by the specified number of lines. */ scroll (lines: number): this { this.position += lines; return this; } /** * Clears the scrollable area. */ clear (): this { process.stdout.cursorTo(this.x, this.y); for (let i = 0; i < this.height; i++) { process.stdout.cursorTo(this.x); process.stdout.write(this.empty + '\n'); } return this; } private resetLines (): void { this.lines = []; this.position = 0; } private splitContentIntoLines (): void { if (!this.content) return; if (this.options.wrap) { this.lines = wrap(this.content, this.width, this.wrap).split('\n'); } else { this.lines = this.content.split('\n'); } } } /** * Generates a scrollable area */ export function Scroll (options: ScrollerOptions): Scroller { return new Scroller(options); }