less-pager-mini
Version:
A scrollable terminal pager for Node.js CLI apps (terminal only)
271 lines (270 loc) • 8.82 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isStyled = exports.isAscii = exports.maxSubRow = void 0;
exports.bufferToNum = bufferToNum;
exports.inputToFilePaths = inputToFilePaths;
exports.inputToString = inputToString;
exports.ringBell = ringBell;
exports.formatContent = formatContent;
exports.addBufferChar = addBufferChar;
exports.delBufferChar = delBufferChar;
exports.render = render;
exports.visualWidth = visualWidth;
const fs_1 = __importDefault(require("fs"));
const wcwidth_1 = __importDefault(require("wcwidth"));
const config_1 = require("./config");
const chopLongLines_1 = require("./chopLongLines");
const wrapLongLines_1 = require("./wrapLongLines");
const constants_1 = require("./constants");
/**
* Returns how many extra sub-rows a line will take if it overflows screen
* width.
*
* - Returns 0 if line-chopping is enabled.
* - Otherwise, calculates how many full rows the visual width spans.
*
* @param line - The string to measure.
* @returns Number of sub-rows needed to display the line.
*/
const maxSubRow = (line) => config_1.config.chopLongLines ? 0 : Math.floor(visualWidth(line) / config_1.config.screenWidth);
exports.maxSubRow = maxSubRow;
/**
* Converts a buffer string to a number.
*
* - Parses the string as a base-10 integer.
* - Returns 0 if the input is not a valid number or equals 0.
*
* @param buffer - The string array to convert.
* @returns Parsed numeric value, or 0 if invalid.
*/
function bufferToNum(buffer) {
const n = parseInt(buffer.join(''), 10);
return n ? n : 0;
}
/**
* Normalizes unknown input into an array of valid file paths.
*
* - Accepts a string, an array, or nested arrays containing strings.
* - Filters out non-string values and paths that do not exist on the
* filesystem.
*
* @param input - A potential file path, array of paths, or nested arrays.
* @returns An array of existing file paths.
*/
function inputToFilePaths(input) {
if (Array.isArray(input)) {
return input
.flat(Infinity)
.filter(path => typeof path === 'string' && fs_1.default.existsSync(path));
}
if (typeof input === 'string' && fs_1.default.existsSync(input)) {
return [input];
}
return [];
}
/**
* Converts any input to a string array.
*
* - Strings and primitives are split by newline.
* - Objects are stringified with optional formatting.
*
* @param input - Value to convert.
* @param preserveFormat - Whether to keep original formatting.
* @returns - Array of strings representing the input.
*/
function inputToString(input, preserveFormat) {
switch (typeof input) {
case 'string':
return input.split('\n');
case 'undefined':
return ['undefined'];
case 'number':
case 'bigint':
case 'boolean':
case 'function':
return input.toString().split('\n');
case 'object':
return JSON
.stringify(input, null, preserveFormat ? 0 : config_1.config.indentation)
.split('\n');
}
return [];
}
/**
* Triggers an audible bell sound in the terminal.
*
* Sends the ASCII bell character (`\x07`) to `stdout`.
*/
function ringBell() {
process.stdout.write('\x07');
}
/**
* Formats content for display based on line wrapping configuration.
*
* - Chooses between chopping or wrapping long lines.
* - Limits formatting to the current window range.
*
* @param content - The full array of content lines to format.
* @returns A formatted string array ready for rendering.
*/
function formatContent(content) {
const lines = [];
if (config_1.config.chopLongLines || config_1.config.col) {
(0, chopLongLines_1.chopLongLines)(content, lines);
}
else {
(0, wrapLongLines_1.wrapLongLines)(content, lines);
}
padToEOF(lines);
return lines;
}
/**
* Adds a character to the input buffer.
*
* - Increments buffer offset if visible width limit is reached.
*
* @param buffer - Current input buffer array.
* @param key - Character to append.
*/
function addBufferChar(buffer, key) {
if (visibleBufferLength(buffer.length) + 1 === config_1.config.screenWidth - 1) {
config_1.config.bufferOffset++;
}
buffer.push(key);
}
/**
* Removes the last character from the input buffer.
*
* - Decrements buffer offset if no visible characters remain.
*
* @param buffer - Current input buffer array.
*/
function delBufferChar(buffer) {
if (visibleBufferLength(buffer.length) === 0) {
config_1.config.bufferOffset--;
}
buffer.pop();
}
/**
* Renders the given content to the terminal.
*
* - Clears the screen before writing.
* - Outputs the content directly to `stdout`.
*
* @param rawContent - The string content to display in the terminal.
* @param buffer - Array of buffer characters.
*/
function render(rawContent, buffer) {
const content = formatContent(rawContent);
const prompt = getPrompt();
if (prompt)
content.push(prompt + getBuffer(buffer));
console.clear();
process.stdout.write(content.join('\n'));
}
/**
* Calculates the total visual width of a string based on terminal character
* widths.
*
* @param line - The input string to measure.
* @returns The total visual width of the string in terminal columns.
*/
function visualWidth(line) {
if ((0, exports.isStyled)(line))
line = line.replace(constants_1.STYLE_REGEX_G, '');
if ((0, exports.isAscii)(line))
return line.length;
const segments = Array.from(line);
let length = 0;
for (let i = 0; i < segments.length; i++) {
length += (0, wcwidth_1.default)(segments[i]);
}
return length;
}
/**
* Checks whether a given segment consists entirely of ASCII characters.
*
* - Matches characters in the range 0x00 to 0x7F.
* - Used to determine whether fast-path rendering can be applied.
*
* @param segment - A string segment to check.
* @returns Whether the segment is pure ASCII.
*/
const isAscii = (segment) => constants_1.ASCII_REGEX.test(segment);
exports.isAscii = isAscii;
/**
* Checks whether a given string contains ANSI style codes.
*
* @param line The input string to test.
* @returns `true` if ANSI style codes are present, otherwise `false`.
*/
const isStyled = (line) => constants_1.STYLE_REGEX.test(line);
exports.isStyled = isStyled;
/**
* Returns the prompt string to be shown at the bottom of the screen.
*
* - Typically returns `':'` to indicate the pager is awaiting user input.
* - Suppressed if an EOF marker like `(END)` is already displayed.
* - May adapt based on the current mode (e.g. buffering, EOF).
*
* @returns The prompt string, or an empty string if suppressed.
*/
function getPrompt() {
const helpPrompt = [
'HELP -- ',
config_1.mode.EOF ? 'END -- Press g to see it again' : 'Press RETURN for more',
', or q when done'
].join('');
if (config_1.mode.HELP && !config_1.mode.BUFFERING) {
return [
constants_1.INVERSE_ON,
helpPrompt.slice(Math.max(helpPrompt.length - config_1.config.screenWidth + 2, 0)),
constants_1.INVERSE_OFF
].join('');
}
if (!config_1.mode.EOF || config_1.mode.BUFFERING)
return ':';
return '';
}
/**
* Trims the buffer to fit within the screen width.
*
* - If too long, trims equally from the start to keep the tail visible.
*
* @param buffer - Array of buffer characters.
* @returns The buffer as a string, trimmed if necessary.
*/
function getBuffer(buffer) {
const width = config_1.config.screenWidth - 1;
const halfWidth = Math.floor(width / 2);
return buffer.slice(halfWidth * config_1.config.bufferOffset).join('');
}
/**
* Calculates visible characters in the buffer.
*
* @param bufferLength - Total buffer character count.
* @returns Number of visible characters based on offset.
*/
function visibleBufferLength(bufferLength) {
const width = config_1.config.screenWidth - 1;
const halfWidth = Math.floor(width / 2);
return bufferLength - halfWidth * config_1.config.bufferOffset;
}
/**
* Pads remaining window space with `~` lines or an `(END)` marker.
*
* @param lines - The array of formatted lines to pad.
*/
function padToEOF(lines) {
while (!config_1.mode.INIT && lines.length < config_1.config.window - 1) {
lines.push('\x1b[1m~\x1b[0m');
}
if (config_1.mode.INIT && lines.length === config_1.config.window - 1)
config_1.mode.INIT = false;
if (!config_1.mode.BUFFERING && !config_1.mode.HELP && config_1.mode.EOF) {
lines.push('\x1b[7m(END)\x1b[0m');
}
}