ink
Version:
React for CLI
87 lines (82 loc) • 4.33 kB
JavaScript
import ansiEscapes from 'ansi-escapes';
const showCursorEscape = '\u{1B}[?25h';
const hideCursorEscape = '\u{1B}[?25l';
export { showCursorEscape, hideCursorEscape };
/**
Compare two cursor positions. Returns true if they differ.
*/
// eslint-disable-next-line unicorn/consistent-boolean-name -- Exported helper that tests import by this name.
export const cursorPositionChanged = (a, b) => a?.x !== b?.x || a?.y !== b?.y;
/**
Build escape sequence to move cursor from the bottom of the output to the target position and show it.
`bottomLine` is the row the renderer left the cursor on, counted from the top of the output.
That is always `lines.length - 1` for `lines = str.split('\n')`, whether or not the output ends
with a newline:
- With a trailing newline, `split` yields one extra empty element and the renderer stops just
past the last visible line — which is `lines.length - 1`.
- Without one, there is no extra element and the renderer deliberately stops on the last visible
line instead of moving past it — which is also `lines.length - 1`.
This is the same row basis `buildReturnToBottom` measures from, so the two stay in step.
*/
export const buildCursorSuffix = (bottomLine, cursorPosition) => {
if (!cursorPosition) {
return '';
}
const moveUp = bottomLine - cursorPosition.y;
return ((moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : '') +
ansiEscapes.cursorTo(cursorPosition.x) +
showCursorEscape);
};
/**
Build escape sequence to move cursor from previousCursorPosition back to the bottom of output.
This must be done before eraseLines or any operation that assumes cursor is at the bottom.
*/
export const buildReturnToBottom = (previousLineCount, previousCursorPosition) => {
if (!previousCursorPosition) {
return '';
}
// PreviousLineCount is the raw `split('\n')` length, so `previousLineCount - 1`
// is the row the cursor was left on regardless of a trailing newline — the same
// basis `buildCursorSuffix` takes as its `bottomLine`.
const down = previousLineCount - 1 - previousCursorPosition.y;
return ((down > 0 ? ansiEscapes.cursorDown(down) : '') + ansiEscapes.cursorTo(0));
};
/**
Build the escape sequence for cursor-only updates (output unchanged, cursor moved).
Hides cursor if it was previously shown, returns to bottom, then repositions.
`buildReturnToBottom` has just placed the cursor on row `previousLineCount - 1`, so the
suffix measures from there rather than recomputing the row from the output.
*/
export const buildCursorOnlySequence = (input) => {
const hidePrefix = input.cursorWasShown ? hideCursorEscape : '';
const returnToBottom = buildReturnToBottom(input.previousLineCount, input.previousCursorPosition);
const cursorSuffix = buildCursorSuffix(input.previousLineCount - 1, input.cursorPosition);
return hidePrefix + returnToBottom + cursorSuffix;
};
/**
Build the prefix that hides cursor and returns to bottom before erasing or rewriting.
Returns empty string if cursor was not shown.
*/
export const buildReturnToBottomPrefix = (wasCursorShown, previousLineCount, previousCursorPosition) => {
if (!wasCursorShown) {
return '';
}
return (hideCursorEscape +
buildReturnToBottom(previousLineCount, previousCursorPosition));
};
/**
Build the sequence that erases the previous frame, as `clear` does before other output is written.
With a cursor shown, the frame's top row is found relative to the cursor instead of returning to the bottom first. A terminal shrinking its rows drops the rows below the cursor before scrolling anything, so the frame's bottom rows may be gone: moving down clamps at the last row and erasing upward from there reaches into the content above the frame.
*/
export const buildEraseFrame = (previousLineCount, previousCursorPosition) => {
if (!previousCursorPosition) {
return ansiEscapes.eraseLines(previousLineCount);
}
// `buildCursorSuffix` never moves below the bottom row, so a cursor set past the output sits on that row.
const rowsBelowTop = Math.min(previousCursorPosition.y, previousLineCount - 1);
return (hideCursorEscape +
ansiEscapes.cursorMove(0, -rowsBelowTop) +
ansiEscapes.cursorTo(0) +
ansiEscapes.eraseDown);
};
//# sourceMappingURL=cursor-helpers.js.map