UNPKG

ink

Version:
87 lines (82 loc) • 4.33 kB
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