ink
Version:
React for CLI
242 lines • 10.5 kB
JavaScript
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