UNPKG

ink

Version:
242 lines 10.5 kB
import ansiEscapes from 'ansi-escapes'; import cliCursor from 'cli-cursor'; import { cursorPositionChanged, buildCursorSuffix, buildCursorOnlySequence, buildReturnToBottomPrefix, hideCursorEscape, } from './cursor-helpers.js'; // Count visible lines in a string, ignoring the trailing empty element // that `split('\n')` produces when the string ends with '\n'. const visibleLineCount = (lines, str) => str.endsWith('\n') ? lines.length - 1 : lines.length; const createStandard = (stream, { showCursor = false } = {}) => { let previousLineCount = 0; let previousOutput = ''; let hasHiddenCursor = false; let cursorPosition; let cursorDirty = false; let previousCursorPosition; let cursorWasShown = false; const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined); const hasChanges = (str, activeCursor) => { const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition); return str !== previousOutput || cursorChanged; }; const render = (str) => { if (!showCursor && !hasHiddenCursor) { cliCursor.hide(stream); hasHiddenCursor = true; } // Only use cursor if setCursorPosition was called since last render. // This ensures stale positions don't persist after component unmount. const activeCursor = getActiveCursor(); cursorDirty = false; const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition); if (!hasChanges(str, activeCursor)) { return false; } const lines = str.split('\n'); const visibleCount = visibleLineCount(lines, str); const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor); if (str === previousOutput && cursorChanged) { stream.write(buildCursorOnlySequence({ cursorWasShown, previousLineCount, previousCursorPosition, visibleLineCount: visibleCount, cursorPosition: activeCursor, })); } else { previousOutput = str; const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition); stream.write(returnPrefix + ansiEscapes.eraseLines(previousLineCount) + str + cursorSuffix); previousLineCount = lines.length; } previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; cursorWasShown = activeCursor !== undefined; return true; }; render.clear = () => { const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition); stream.write(prefix + ansiEscapes.eraseLines(previousLineCount)); previousOutput = ''; previousLineCount = 0; previousCursorPosition = undefined; cursorWasShown = false; }; render.done = () => { previousOutput = ''; previousLineCount = 0; previousCursorPosition = undefined; cursorWasShown = false; if (!showCursor) { cliCursor.show(stream); hasHiddenCursor = false; } }; render.sync = (str) => { const activeCursor = cursorDirty ? cursorPosition : undefined; cursorDirty = false; const lines = str.split('\n'); previousOutput = str; previousLineCount = lines.length; if (!activeCursor && cursorWasShown) { stream.write(hideCursorEscape); } if (activeCursor) { stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor)); } previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; cursorWasShown = activeCursor !== undefined; }; render.setCursorPosition = (position) => { cursorPosition = position; cursorDirty = true; }; render.isCursorDirty = () => cursorDirty; render.willRender = (str) => hasChanges(str, getActiveCursor()); return render; }; const createIncremental = (stream, { showCursor = false } = {}) => { let previousLines = []; let previousOutput = ''; let hasHiddenCursor = false; let cursorPosition; let cursorDirty = false; let previousCursorPosition; let cursorWasShown = false; const getActiveCursor = () => (cursorDirty ? cursorPosition : undefined); const hasChanges = (str, activeCursor) => { const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition); return str !== previousOutput || cursorChanged; }; const render = (str) => { if (!showCursor && !hasHiddenCursor) { cliCursor.hide(stream); hasHiddenCursor = true; } // Only use cursor if setCursorPosition was called since last render. // This ensures stale positions don't persist after component unmount. const activeCursor = getActiveCursor(); cursorDirty = false; const cursorChanged = cursorPositionChanged(activeCursor, previousCursorPosition); if (!hasChanges(str, activeCursor)) { return false; } const nextLines = str.split('\n'); const visibleCount = visibleLineCount(nextLines, str); const previousVisible = visibleLineCount(previousLines, previousOutput); if (str === previousOutput && cursorChanged) { stream.write(buildCursorOnlySequence({ cursorWasShown, previousLineCount: previousLines.length, previousCursorPosition, visibleLineCount: visibleCount, cursorPosition: activeCursor, })); previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; cursorWasShown = activeCursor !== undefined; return true; } const returnPrefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition); if (str === '\n' || previousOutput.length === 0) { const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor); stream.write(returnPrefix + ansiEscapes.eraseLines(previousLines.length) + str + cursorSuffix); cursorWasShown = activeCursor !== undefined; previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; previousOutput = str; previousLines = nextLines; return true; } const hasTrailingNewline = str.endsWith('\n'); // We aggregate all chunks for incremental rendering into a buffer, and then write them to stdout at the end. const buffer = []; buffer.push(returnPrefix); // Clear extra lines if the current content's line count is lower than the previous. if (visibleCount < previousVisible) { const previousHadTrailingNewline = previousOutput.endsWith('\n'); const extraSlot = previousHadTrailingNewline ? 1 : 0; buffer.push(ansiEscapes.eraseLines(previousVisible - visibleCount + extraSlot), ansiEscapes.cursorUp(visibleCount)); } else { buffer.push(ansiEscapes.cursorUp(previousVisible - 1)); } for (let i = 0; i < visibleCount; i++) { const isLastLine = i === visibleCount - 1; // We do not write lines if the contents are the same. This prevents flickering during renders. if (nextLines[i] === previousLines[i]) { // Don't move past the last line when there's no trailing newline, // otherwise the cursor overshoots the rendered block. if (!isLastLine || hasTrailingNewline) { buffer.push(ansiEscapes.cursorNextLine); } continue; } buffer.push(ansiEscapes.cursorTo(0) + nextLines[i] + ansiEscapes.eraseEndLine + // Don't append newline after the last line when the input // has no trailing newline (fullscreen mode). (isLastLine && !hasTrailingNewline ? '' : '\n')); } const cursorSuffix = buildCursorSuffix(visibleCount, activeCursor); buffer.push(cursorSuffix); stream.write(buffer.join('')); cursorWasShown = activeCursor !== undefined; previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; previousOutput = str; previousLines = nextLines; return true; }; render.clear = () => { const prefix = buildReturnToBottomPrefix(cursorWasShown, previousLines.length, previousCursorPosition); stream.write(prefix + ansiEscapes.eraseLines(previousLines.length)); previousOutput = ''; previousLines = []; previousCursorPosition = undefined; cursorWasShown = false; }; render.done = () => { previousOutput = ''; previousLines = []; previousCursorPosition = undefined; cursorWasShown = false; if (!showCursor) { cliCursor.show(stream); hasHiddenCursor = false; } }; render.sync = (str) => { const activeCursor = cursorDirty ? cursorPosition : undefined; cursorDirty = false; const lines = str.split('\n'); previousOutput = str; previousLines = lines; if (!activeCursor && cursorWasShown) { stream.write(hideCursorEscape); } if (activeCursor) { stream.write(buildCursorSuffix(visibleLineCount(lines, str), activeCursor)); } previousCursorPosition = activeCursor ? { ...activeCursor } : undefined; cursorWasShown = activeCursor !== undefined; }; render.setCursorPosition = (position) => { cursorPosition = position; cursorDirty = true; }; render.isCursorDirty = () => cursorDirty; render.willRender = (str) => hasChanges(str, getActiveCursor()); return render; }; const create = (stream, { showCursor = false, incremental = false } = {}) => { if (incremental) { return createIncremental(stream, { showCursor }); } return createStandard(stream, { showCursor }); }; const logUpdate = { create }; export default logUpdate; //# sourceMappingURL=log-update.js.map