UNPKG

inquirer-file-path

Version:

a file (relative to given path) selector for Inquirer.js

360 lines (299 loc) 9.07 kB
/** * `directory` type prompt */ var rx = require('rx-lite'); var _ = require("lodash"); var util = require("util"); var chalk = require("chalk"); var figures = require("figures"); var cliCursor = require("cli-cursor"); var Base = require("inquirer/lib/prompts/base"); var observe = require("inquirer/lib/utils/events"); var Paginator = require("inquirer/lib/utils/paginator"); var Choices = require('inquirer/lib/objects/choices'); var Separator = require('inquirer/lib/objects/separator'); var path = require('path'); var fs = require('fs'); /** * Module exports */ module.exports = Prompt; /** * Constants */ var BACK = "go back a directory"; /** * Constructor */ function Prompt() { Base.apply( this, arguments ); if (!this.opt.basePath) { this.throwParamError("basePath"); } this.basePath = path.isAbsolute(this.opt.basePath) ? path.resolve(this.opt.basePath) : path.resolve(process.cwd(), this.opt.basePath); this.currentPath = this.basePath; this.opt.choices = new Choices(this.createChoices(this.basePath, 0), this.answers); this.selected = 0; this.firstRender = true; // Make sure no default is set (so it won't be printed) this.opt.default = null; this.searchTerm = ''; this.paginator = new Paginator(); } util.inherits( Prompt, Base ); /** * Start the Inquiry session * @param {Function} cb Callback when prompt is done * @return {this} */ Prompt.prototype._run = function( cb ) { var self = this; self.searchMode = false; self.done = cb; var alphaNumericRegex = /\w|\.|\-/i; var events = observe(self.rl); var keyUps = events.keypress.filter(function (e) { return e.key.name === 'up' || (!self.searchMode && e.key.name === 'k'); }).share(); var keyDowns = events.keypress.filter(function (e) { return e.key.name === 'down' || (!self.searchMode && e.key.name === 'j'); }).share(); var keySlash = events.keypress.filter(function (e) { return e.value === '/'; }).share(); var alphaNumeric = events.keypress.filter(function (e) { return e.key.name === 'backspace' || alphaNumericRegex.test(e.value); }).share(); var searchTerm = keySlash.flatMap(function (md) { self.searchMode = true; self.searchTerm = ''; self.render(); var end$ = new rx.Subject(); var done$ = rx.Observable.merge(events.line, end$); return alphaNumeric.map(function (e) { if (e.key.name === 'backspace' && self.searchTerm.length) { self.searchTerm = self.searchTerm.slice(0, -1); } else if (e.value) { self.searchTerm += e.value; } if (self.searchTerm === '') { end$.onNext(true); } return self.searchTerm; }) .takeUntil(done$) .doOnCompleted(function() { self.searchMode = false; self.render(); return false; }); }).share(); var outcome = this.handleSubmit(events.line); outcome.done.forEach( this.onSubmit.bind(this) ); outcome.traversal.forEach( this.handleTraversal.bind(this) ); keyUps.takeUntil( outcome.done ).forEach( this.onUpKey.bind(this) ); keyDowns.takeUntil( outcome.done ).forEach( this.onDownKey.bind(this) ); events.keypress.takeUntil( outcome.done ).forEach( this.hideKeyPress.bind(this) ); searchTerm.takeUntil( outcome.done ).forEach( this.onKeyPress.bind(this) ); // Init the prompt cliCursor.hide(); self.render(); return this; }; /** * Render the prompt to screen * @return {Prompt} self */ Prompt.prototype.render = function() { // Render question var message = this.getQuestion(); if ( this.firstRender ) { message += chalk.dim( "(Use arrow keys)" ); } // Render choices or answer depending on the state var relativePath = path.relative(this.opt.basePath, this.currentPath); if ( this.status === "answered" ) { message += chalk.cyan(relativePath); } else { message += chalk.bold("\n Current directory: ") + this.opt.basePath + "/" + chalk.cyan(relativePath); var choicesStr = listRender(this.opt.choices, this.selected ); message += "\n" + this.paginator.paginate(choicesStr, this.selected, this.opt.pageSize); // append search mode info if (this.searchMode) { message += ("\nSearch: " + this.searchTerm); } else { message += "\n(Use \"/\" key to search this directory)"; } } this.firstRender = false; this.screen.render(message); }; /** * When user press `enter` key */ Prompt.prototype.handleSubmit = function (e) { var self = this; var obx = e.map(function () { return self.opt.choices.getChoice( self.selected ).value; }) .scan(function(stack, curr) { var isBack = curr === BACK; var depth = stack.length; if (isBack && depth > 0) { stack.pop(); } else if (isBack && depth === 0) { stack = []; } else { stack.push(curr); } return stack; }, []) .share(); var done = obx.filter(function(stack) { return isFile(getAbsolutePath(self.basePath, stack)); }); var traversal = obx.filter(function(stack) { return !isFile(getAbsolutePath(self.basePath, stack)); }).takeUntil(done); return { traversal: traversal, done: done }; }; /** * when user selects to drill into a folder (by selecting folder name) */ Prompt.prototype.handleTraversal = function (value) { this.currentPath = getAbsolutePath(this.basePath, value); this.opt.choices = new Choices(this.createChoices(this.currentPath, value.length), this.answers); this.selected = 0; this.render(); }; /** * when user selects a file */ Prompt.prototype.onSubmit = function(value) { this.currentPath = getAbsolutePath(this.basePath, value); this.status = "answered"; // Rerender prompt this.render(); this.screen.done(); cliCursor.show(); this.done( path.relative(this.opt.basePath, this.currentPath) ); }; /** * When user press a key */ Prompt.prototype.hideKeyPress = function() { if (!this.searchMode) { this.render(); } }; Prompt.prototype.onUpKey = function() { var len = this.opt.choices.realLength; this.selected = (this.selected > 0) ? this.selected - 1 : len - 1; this.render(); }; Prompt.prototype.onDownKey = function() { var len = this.opt.choices.realLength; this.selected = (this.selected < len - 1) ? this.selected + 1 : 0; this.render(); }; Prompt.prototype.onSlashKey = function(e) { this.render(); }; Prompt.prototype.onKeyPress = function(e) { var index = findIndex.call(this, this.searchTerm); if (index >= 0) { this.selected = index; } this.render(); }; function findIndex (term) { var item; for (var i=0; i < this.opt.choices.realLength; i++) { item = this.opt.choices.realChoices[i].name.toLowerCase(); if (item.indexOf(term) === 0) { return i; } } return -1; } /** * Helper to create new choices based on previous selection. */ Prompt.prototype.createChoices = function (basePath, depth) { var choices = getOptions(basePath); if (choices.length > 0) { choices.push(new Separator()); } if (depth > 0) { choices.push(new Separator()); choices.push(BACK); choices.push(new Separator()); } return choices; }; /** * Function for rendering list choices * @param {Number} pointer Position of the pointer * @return {String} Rendered content */ function listRender(choices, pointer) { var output = ''; var separatorOffset = 0; choices.forEach(function (choice, i) { if (choice.type === 'separator') { separatorOffset++; output += ' ' + choice + '\n'; return; } var isSelected = (i - separatorOffset === pointer); var line = (isSelected ? figures.pointer + ' ' : ' ') + choice.name; if (isSelected) { line = chalk.cyan(line); } output += line + ' \n'; }); return output.replace(/\n$/, ''); } /** * Helper function to get the absolute path for a choice * @param {String} basePath the containing path of the choice * @param {String} choices An array of choices that the user has made * @returns {String} the absolute path of the choice */ function getAbsolutePath(basePath, choices) { var paths = [basePath].concat(choices); return path.join.apply(null, paths); } function getRelativePath(basePathOption, baseAbsolutePath, choices) { return path.relative(basePathOption, getAbsolutePath(baseAbsolutePath, choices)); } function isFile(filePath) { var stats; try { stats = fs.lstatSync(filePath); } catch (e) { return false; } return stats.isFile(); } /** * Function for getting list of folders and files in directory * @param {String} basePath the path the folder to get a list of containing folders and files * @return {Array} array of folder names inside of basePath */ function getOptions(basePath) { return fs .readdirSync(basePath) .filter(function(file) { var stats = fs.lstatSync(path.join(basePath, file)); if (stats.isSymbolicLink()) { return false; } var isNotDotFile = path.basename(file).indexOf('.') !== 0; return isNotDotFile; }) .sort(); }