less-pager-mini
Version:
A scrollable terminal pager for Node.js CLI apps (terminal only)
191 lines (190 loc) • 6.95 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = pager;
const config_1 = require("./config");
const lessHelp_1 = require("./lessHelp");
const readKey_1 = require("./readKey");
const normalKeys_1 = require("./normalKeys");
const helpers_1 = require("./helpers");
const moving_1 = require("./features/moving");
const constants_1 = require("./constants");
const TITLE = constants_1.CONSOLE_TITLE_START + 'less-pager-mini' + constants_1.CONSOLE_TITLE_END;
/**
* Less-pager-mini
*
* - If `examineFile` is true, treats input as file path(s) and loads file
* content.
* - Otherwise, converts arbitrary input into displayable string content.
*
* @param input - The input to render, which can be a string, object, or array.
* @param preserveFormat - Whether to preserve original formatting
* (no indentation).
* @param examineFile - If true, treats input as file path(s) and reads from
* disk.
*/
async function pager(input, preserveFormat = false, examineFile = false) {
if (examineFile) {
await filePager((0, helpers_1.inputToFilePaths)(input), preserveFormat);
return;
}
const content = (0, helpers_1.inputToString)(input, preserveFormat);
if (!content.length)
return;
await contentPager(content);
}
/**
* Displays the contents of provided file paths using the pager.
*
* - Ignores empty file path arrays.
* - Converts file content to string arrays for rendering.
*
* @param filePaths - Array of file paths to display.
* @param preserveFormat - Whether to preserve the file’s original formatting.
*/
async function filePager(filePaths, preserveFormat) {
if (!filePaths.length)
return;
// remove line below in the future
if (preserveFormat)
console.log('TODO: preserveFormat not implemented yet');
}
/**
* Starts an interactive pager session to navigate through string content.
*
* - Handles terminal resizing (SIGWINCH) to repaint content.
* - Supports key-based navigation with buffered numeric input.
* - Responds to various paging actions like line/window movement and exit.
*
* @param content - The content to be displayed in the pager.
*/
async function contentPager(content) {
process.stdout.write(TITLE);
process.stdout.write(constants_1.ALTERNATE_CONSOLE_ON);
process.on('uncaughtException', (error) => {
process.stdout.write(constants_1.ALTERNATE_CONSOLE_OFF);
process.stdout.write(constants_1.CONSOLE_TITLE_RESET);
console.error(error);
process.exit(1);
});
process.on('SIGWINCH', () => {
config_1.mode.INIT = false;
config_1.config.window = process.stdout.rows;
config_1.config.screenWidth = process.stdout.columns;
config_1.config.halfWindow = Math.floor(config_1.config.window / 2);
config_1.config.halfScreenWidth = Math.floor(config_1.config.screenWidth / 2);
buffer = [];
config_1.config.bufferOffset = 0;
(0, helpers_1.render)(content, buffer);
});
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
let exit = false;
let repaint = true;
let buffer = [];
let prevContent = [];
let prevConfig = config_1.config;
let prevMode = config_1.mode;
while (!exit) {
config_1.mode.BUFFERING = Boolean(buffer.length);
if (repaint)
(0, helpers_1.render)(content, buffer);
repaint = true;
const key = await (0, readKey_1.readKey)();
if (key >= '0' && key <= '9') {
(0, helpers_1.addBufferChar)(buffer, key);
continue;
}
const action = (0, normalKeys_1.getAction)(key);
if (action === 'BACKSPACE' && buffer.length) {
(0, helpers_1.delBufferChar)(buffer);
continue;
}
switch (action) {
case 'FORCE_EXIT':
exit = true;
break;
case 'EXIT':
exit = shouldExit();
break;
case 'HELP':
prepareHelp();
break;
case 'LINE_FORWARD':
(0, moving_1.lineForward)(content, (0, helpers_1.bufferToNum)(buffer) || 1);
break;
case 'LINE_BACKWARD':
(0, moving_1.lineBackward)(content, (0, helpers_1.bufferToNum)(buffer) || 1);
break;
case 'WINDOW_FORWARD':
(0, moving_1.windowForward)(content, buffer);
break;
case 'WINDOW_BACKWARD':
(0, moving_1.windowBackward)(content, buffer);
break;
case 'SET_WINDOW_FORWARD':
(0, moving_1.setWindowForward)(content, buffer);
break;
case 'SET_WINDOW_BACKWARD':
(0, moving_1.setWindowBackward)(content, buffer);
break;
case 'NO_EOF_WINDOW_FORWARD':
(0, moving_1.windowForward)(content, buffer, true);
break;
case 'SET_HALF_WINDOW_FORWARD':
(0, moving_1.setHalfWindowForward)(content, buffer);
break;
case 'SET_HALF_WINDOW_BACKWARD':
(0, moving_1.setHalfWindowBackward)(content, buffer);
break;
case 'SET_HALF_SCREEN_RIGHT':
(0, moving_1.setHalfScreenRight)(buffer);
break;
case 'SET_HALF_SCREEN_LEFT':
(0, moving_1.setHalfScreenLeft)(buffer);
break;
case 'REPAINT':
break;
default:
(0, helpers_1.ringBell)();
repaint = false;
}
buffer = [];
config_1.config.bufferOffset = 0;
}
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write(constants_1.ALTERNATE_CONSOLE_OFF);
process.stdout.write(constants_1.CONSOLE_TITLE_RESET);
// helpers
/**
* Exits help mode if active, otherwise allows pager to exit.
*
* @returns `true` if should exit, `false` if returning from help.
*/
function shouldExit() {
if (!config_1.mode.HELP)
return true;
content = prevContent;
(0, config_1.applyConfig)(prevConfig);
(0, config_1.applyMode)(prevMode);
config_1.mode.HELP = false;
return false;
}
/**
* Enters help mode by saving current state and loading help content.
*/
function prepareHelp() {
if (config_1.mode.HELP)
return;
prevContent = content;
prevConfig = config_1.config;
prevMode = config_1.mode;
(0, config_1.resetConfig)();
(0, config_1.resetMode)();
config_1.mode.HELP = true;
content = lessHelp_1.help;
}
}
module.exports = pager;
module.exports.default = pager;