-
Notifications
You must be signed in to change notification settings - Fork 5
Open
Description
The same file, but with the clear lines.
const os = require('os');
const readline = require('readline');
const keypress = require('keypress');
const defaultHelpMessage =
'Type a hotkey or use Down/Up arrows then Enter to choose an item.';
/**
* Displays a menu of items in the console and asynchronously waits for the user to select an item.
*
* @param {any} items Array of menu items, where each item is an object that includes a title
* property and optional hotkey property. (Items may include additional user-defined properties.)
* @param {any} options Dictionary of options for the menu:
* - header {string}: Header text for the menu.
* - border {boolean}: True to draw a border around the menu.
* - pageSize {integer}: Max number of items to show at a time. Additional items cause the menu
* to be scrollable.
* - helpMessage {string}: Message text to show under the menu.
* @returns A promise that resolves to the chosen item, or to null if the menu was cancelled.
*/
function menu(items, options) {
if (!items || !Array.isArray(items) || items.length < 1) {
throw new TypeError('A nonempty items array is required.');
}
options = options || {};
var count = items.length;
var selectedIndex = items.findIndex(item => item.selected);
if (selectedIndex < 0) {
selectedIndex = 0;
while (selectedIndex < count && items[selectedIndex].separator) selectedIndex++;
}
var scrollOffset = 0;
printMenu(items, options, selectedIndex, scrollOffset);
return new Promise((resolve, reject) => {
process.stdin.setRawMode(true);
process.stdin.resume();
keypress(process.stdin);
var handleMenuKeypress = (ch, key) => {
var selection = null;
if (isEnter(key)) {
selection = items[selectedIndex];
} else if (ch) {
selection = items.find(item => item.hotkey && item.hotkey === ch) ||
items.find(item => item.hotkey &&
item.hotkey.toLowerCase() === ch.toLowerCase());
}
var newIndex = null;
if (selection || isCancelCommand(key)) {
process.stdin.removeListener('keypress', handleMenuKeypress);
process.stdin.setRawMode(false);
resetCursor(options, selectedIndex, scrollOffset);
readline.clearScreenDown(process.stdout);
process.stdin.pause();
resolve(selection);
} else if (isUpCommand(key) && selectedIndex > 0) {
newIndex = selectedIndex - 1;
while (newIndex >= 0 && items[newIndex].separator) newIndex--;
} else if (isDownCommand(key) && selectedIndex < count - 1) {
newIndex = selectedIndex + 1;
while (newIndex < count && items[newIndex].separator) newIndex++;
} else if (isPageUpCommand(key) && selectedIndex > 0) {
newIndex = (options.pageSize ? Math.max(0, selectedIndex - options.pageSize) : 0);
while (newIndex < count && items[newIndex].separator) newIndex++;
} else if (isPageDownCommand(key) && selectedIndex < count - 1) {
newIndex = (options.pageSize
? Math.min(count - 1, selectedIndex + options.pageSize) : count - 1);
while (newIndex >= 0 && items[newIndex].separator) newIndex--;
} else if (isGoToFirstCommand(key) && selectedIndex > 0) {
newIndex = 0;
while (newIndex < count && items[newIndex].separator) newIndex++;
} else if (isGoToLastCommand(key) && selectedIndex < count - 1) {
newIndex = count - 1;
while (newIndex >= 0 && items[newIndex].separator) newIndex--;
}
if (newIndex !== null && newIndex >= 0 && newIndex < count) {
resetCursor(options, selectedIndex, scrollOffset);
selectedIndex = newIndex;
// Adjust the scroll offset when the selection moves off the page.
if (selectedIndex < scrollOffset) {
scrollOffset = (isPageUpCommand(key)
? Math.max(0, scrollOffset - options.pageSize) : selectedIndex);
} else if (options.pageSize && selectedIndex >= scrollOffset + options.pageSize) {
scrollOffset = (isPageDownCommand(key)
? Math.min(count - options.pageSize, scrollOffset + options.pageSize)
: selectedIndex - options.pageSize + 1);
}
printMenu(items, options, selectedIndex, scrollOffset);
}
};
process.stdin.addListener('keypress', handleMenuKeypress);
});
}
function isEnter(key) { return key && (key.name === 'enter' || key.name === 'return'); }
function isUpCommand(key) { return key && key.name === 'up'; }
function isDownCommand(key) { return key && key.name === 'down'; }
function isPageUpCommand(key) { return key && key.name === 'pageup'; }
function isPageDownCommand(key) { return key && key.name === 'pagedown'; }
function isGoToFirstCommand(key) { return key && key.name === 'home'; }
function isGoToLastCommand(key) { return key && key.name === 'end'; }
function isCancelCommand(key) {
return key && ((key.ctrl && key.name == 'c') || key.name === 'escape');
}
function resetCursor(options, selectedIndex, scrollOffset) {
readline.moveCursor(process.stdout, -3,
- (options.header ? 1 : 0)
- (options.border ? (options.header ? 2 : 1) : 0)
- selectedIndex + scrollOffset);
}
function printMenu(items, options, selectedIndex, scrollOffset) {
var repeat = (s, n) => {
return Array(n + 1).join(s);
};
var width = 0;
for (var i = 0; i < items.length; i++) {
if (items[i].title && 4 + items[i].title.length > width) {
width = 4 + items[i].title.length;
}
}
var prefix = (options.border ? '│' : '');
var suffix = (options.border ? ' │' : '');
if (options.header && options.header.length > width) {
width = options.header.length;
}
if (options.border) {
if (!options.header && options.pageSize && scrollOffset > 0) {
process.stdout.write('.──/\\' + repeat('─', width - 2) + '?' + os.EOL);
} else {
process.stdout.write('┌' + repeat('─', width + 2) + '┐' + os.EOL);
}
}
if (options.header) {
process.stdout.write(prefix + (options.border ? ' ' : '') + options.header +
repeat(' ', width - options.header.length) + suffix + os.EOL);
if (options.border) {
if (options.pageSize && scrollOffset > 0) {
process.stdout.write('+──/\\' + repeat('─', width - 2) + '+' + os.EOL);
} else {
process.stdout.write('├' + repeat('─', width + 2) + '┤' + os.EOL);
}
}
}
var scrollEnd = options.pageSize
? Math.min(items.length, scrollOffset + options.pageSize)
: items.length;
for (var i = scrollOffset; i < scrollEnd; i++) {
if (items[i].separator) {
process.stdout.write(prefix + ' ' + repeat(' ', width) + suffix + os.EOL);
} else {
var hotkey = items[i].hotkey || '*';
var title = items[i].title || '';
var label = (i === selectedIndex
? '[' + hotkey + ']' : ' ' + hotkey + ')');
process.stdout.write(prefix + ' ' + label + ' ' + title +
repeat(' ', width - title.length - 4) + suffix + os.EOL);
}
}
if (options.border) {
if (options.pageSize && scrollEnd < items.length) {
process.stdout.write('└──\\/' + repeat('─', width - 2) + '┘' + os.EOL);
} else {
process.stdout.write('└' + repeat('─', width + 2) + '┘' + os.EOL);
}
}
process.stdout.write(options.helpMessage || defaultHelpMessage);
readline.moveCursor(process.stdout,
-(options.helpMessage || defaultHelpMessage).length + prefix.length + 2,
-(options.border ? 1 : 0) - (scrollEnd - scrollOffset) + selectedIndex - scrollOffset);
}
module.exports = menu;
ackvf
Metadata
Metadata
Assignees
Labels
No labels