diff --git a/.travis.yml b/.travis.yml index b4ca0ba..c4b1276 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: - "0.12" - - "4.2" - - "5.2" \ No newline at end of file + - "4" + - "5" + - "6" diff --git a/README.md b/README.md index 8400e19..05045a7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This project is a fork of [Inquirer-directory](https://github.com/nicksrandall/i ![](https://img.shields.io/badge/license-MIT-blue.svg) -![](https://img.shields.io/badge/release-v0.1.0-blue.svg) +[![](https://img.shields.io/badge/release-v1.0.0-blue.svg)](https://github.com/KamiKillertO/inquirer-select-directory/releases/tag/v1.0.0) [![Build Status](https://travis-ci.org/KamiKillertO/inquirer-select-directory.svg)](https://travis-ci.org/KamiKillertO/inquirer-select-directory) [![Build status](https://ci.appveyor.com/api/projects/status/fdyk5g3y56381742?svg=true)](https://ci.appveyor.com/project/KamiKillertO/inquirer-select-directory) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e6a963539c4440b69356649c0048ea30)](https://www.codacy.com/app/kamikillerto/inquirer-select-directory?utm_source=github.com&utm_medium=referral&utm_content=KamiKillertO/inquirer-select-directory&utm_campaign=Badge_Grade) @@ -19,14 +19,14 @@ npm install --save inquirer-select-directory ## Features -- Support for symlinked files -- Vim style navigation -- Search for file with "/" key +- Support for symlinked files +- Vim style navigation +- Search for file with "/" key ### Key Maps -- Press "/" key to enter search mode. -- Press "-" key to go up (back) a directory. +- Press "/" key to enter search mode. +- Press "-" key to go up (back) a directory. ## Usage diff --git a/appveyor.yml b/appveyor.yml index e7b61d7..eabccb9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,8 +2,9 @@ environment: matrix: - nodejs_version: "0.12" - - nodejs_version: "4.2" - - nodejs_version: "5.2" + - nodejs_version: "4" + - nodejs_version: "5" + - nodejs_version: "6" # Install scripts. (runs after repo cloning) install: # Get the latest stable version of Node.js or io.js @@ -24,4 +25,4 @@ test_script: - npm test # Don't actually build. -build: off \ No newline at end of file +build: off diff --git a/example/example.js b/example/example.js index 3a07193..4a7e378 100644 --- a/example/example.js +++ b/example/example.js @@ -4,7 +4,7 @@ "use strict"; var inquirer = require("inquirer"); -inquirer.registerPrompt('directory', require('../src/index')); +inquirer.registerPrompt("directory", require("../src/index")); inquirer.prompt([{ type: "directory", @@ -13,4 +13,4 @@ inquirer.prompt([{ basePath: "./node_modules" }], function(answers) { console.log(JSON.stringify(answers, null, " ")); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 6e6f2c3..02cfeff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inquirer-select-directory", - "version": "0.1.0", + "version": "1.0.0", "description": "A directory prompt for Inquirer.js", "main": "src/index.js", "scripts": { @@ -36,7 +36,7 @@ "chai": "^3.4.1", "events": "^1.1.0", "mocha": "^2.3.4", - "mock-fs": "^3.5.0", + "mock-fs": "^3.11.0", "sinon": "^1.17.2" } } diff --git a/src/index.js b/src/index.js index 50bde3a..11c80e3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ /** * `directory` type prompt */ -var rx = require('rx-lite'); +var rx = require("rx-lite"); var util = require("util"); var chalk = require("chalk"); var figures = require("figures"); @@ -10,11 +10,11 @@ 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 Choices = require("inquirer/lib/objects/choices"); +var Separator = require("inquirer/lib/objects/separator"); -var path = require('path'); -var fs = require('fs'); +var path = require("path"); +var fs = require("fs"); /** @@ -22,11 +22,64 @@ var fs = require('fs'); */ var CHOOSE = "choose this directory"; var BACK = ".."; +var CURRENT = "."; + + /** - * Constructor + * 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, index) { + if (choice.type === "separator") { + separatorOffset++; + output += " " + choice + "\n"; + return; + } + + var isSelected = (index - separatorOffset === pointer); + var line = (isSelected ? figures.pointer + " " : " ") + choice.name; + if (isSelected) { + line = chalk.cyan(line); + } + output += line + " \n"; + }); + return output.replace(/\n$/, ""); +} + +/** + * Function for getting list of folders in directory + * @param {String} basePath the path the folder to get a list of containing folders + * @return {Array} array of folder names inside of basePath + */ +function getDirectories(basePath) { + return fs + .readdirSync(basePath) + .filter(function(file) { + try { + var stats = fs.lstatSync(path.join(basePath, file)); + if (stats.isSymbolicLink()) { + return false; + } + var isDir = stats.isDirectory(); + var isNotDotFile = path.basename(file).indexOf(".") !== 0; + return isDir && isNotDotFile; + } catch (error) { + return false; + } + }) + .sort(); +} + +/** + * Constructor + */ function Prompt() { Base.apply(this, arguments); if (!this.opt.basePath) { @@ -37,12 +90,10 @@ function Prompt() { this.opt.choices = new Choices(this.createChoices(this.currentPath), this.answers); this.selected = 0; - this.firstRender = true; - - // Make sure no default is set (so it won't be printed) + // Make sure no default is set (so it won"t be printed) this.opt.default = null; - this.searchTerm = ''; + this.searchTerm = ""; this.paginator = new Paginator(); } @@ -50,50 +101,50 @@ util.inherits(Prompt, Base); /** * Start the Inquiry session - * @param {Function} cb Callback when prompt is done + * @param {Function} callback Callback when prompt is done * @return {this} */ -Prompt.prototype._run = function(cb) { +Prompt.prototype._run = function(callback) { var self = this; self.searchMode = false; - this.done = cb; + this.done = callback; var alphaNumericRegex = /\w|\.|\-/i; var events = observe(this.rl); - var keyUps = events.keypress.filter(function(e) { - return e.key.name === 'up' || (!self.searchMode && e.key.name === 'k'); + var keyUps = events.keypress.filter(function(evt) { + return evt.key.name === "up" || (!self.searchMode && evt.key.name === "k"); }).share(); - var keyDowns = events.keypress.filter(function(e) { - return e.key.name === 'down' || (!self.searchMode && e.key.name === 'j'); + var keyDowns = events.keypress.filter(function(evt) { + return evt.key.name === "down" || (!self.searchMode && evt.key.name === "j"); }).share(); - var keySlash = events.keypress.filter(function(e) { - return e.value === '/'; + var keySlash = events.keypress.filter(function(evt) { + return evt.value === "/"; }).share(); - var keyMinus = events.keypress.filter(function(e) { - return e.value === '-'; + var keyMinus = events.keypress.filter(function(evt) { + return evt.value === "-"; }).share(); - var alphaNumeric = events.keypress.filter(function(e) { - return e.key.name === 'backspace' || alphaNumericRegex.test(e.value); + var alphaNumeric = events.keypress.filter(function(evt) { + return evt.key.name === "backspace" || alphaNumericRegex.test(evt.value); }).share(); - var searchTerm = keySlash.flatMap(function(md) { + var searchTerm = keySlash.flatMap(function() { self.searchMode = true; - self.searchTerm = ''; + 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) { + return alphaNumeric.map(function(evt) { + if (evt.key.name === "backspace" && self.searchTerm.length) { self.searchTerm = self.searchTerm.slice(0, -1); - } else if (e.value) { - self.searchTerm += e.value; + } else if (evt.value) { + self.searchTerm += evt.value; } - if (self.searchTerm === '') { + if (self.searchTerm === "") { end$.onNext(true); } return self.searchTerm; @@ -133,32 +184,32 @@ 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 if (this.status === "answered") { message += chalk.cyan(this.currentPath); } else { // message += chalk.bold("\n Current directory: ") + path.resolve(this.opt.basePath) + path.sep + chalk.cyan(path.relative(this.opt.basePath, this.currentPath)); message += chalk.bold("\n Current directory: ") + chalk.cyan(path.resolve(this.opt.basePath, this.currentPath)); + message += chalk.bold("\n"); var choicesStr = listRender(this.opt.choices, this.selected); message += "\n" + this.paginator.paginate(choicesStr, this.selected, this.opt.pageSize); + if (this.searchMode) { + message += ("\nSearch: " + this.searchTerm); + } else { + message += chalk.dim("\n(Use "/" key to search this directory)"); + message += chalk.dim("\n(Use "-" key to navigate to the parent folder"); + } + message += chalk.dim("\n(Use arrow keys)"); } - 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 + * + * @param {any} e + * @returns */ Prompt.prototype.handleSubmit = function(e) { var self = this; @@ -167,15 +218,14 @@ Prompt.prototype.handleSubmit = function(e) { }).share(); var done = obx.filter(function(choice) { - return choice === CHOOSE; + return choice === CHOOSE || choice === CURRENT; }).take(1); - var back = obx.filter(function(choice) { return choice === BACK; }).takeUntil(done); var drill = obx.filter(function(choice) { - return choice !== BACK && choice !== CHOOSE; + return choice !== BACK && choice !== CHOOSE && choice !== CURRENT; }).takeUntil(done); return { @@ -200,7 +250,6 @@ Prompt.prototype.handleDrill = function() { * when user selects ".. back" */ Prompt.prototype.handleBack = function() { - var choice = this.opt.choices.getChoice(this.selected); this.currentPath = path.dirname(this.currentPath); this.opt.choices = new Choices(this.createChoices(this.currentPath), this.answers); this.selected = 0; @@ -243,28 +292,21 @@ Prompt.prototype.onDownKey = function() { this.render(); }; -Prompt.prototype.onSlashKey = function(e) { +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) { +Prompt.prototype.onKeyPress = function(/*e*/) { 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; + for (var index = 0; index < this.opt.choices.realLength; index++) { + item = this.opt.choices.realChoices[index].name.toLowerCase(); + if (item.indexOf(this.searchTerm) === 0) { + this.selected = index; + break; } } - return -1; -} + this.render(); +}; /** * Helper to create new choices based on previous selection. @@ -274,6 +316,7 @@ Prompt.prototype.createChoices = function(basePath) { if (basePath !== this.root) { choices.unshift(BACK); } + choices.unshift(CURRENT); if (choices.length > 0) { choices.push(new Separator()); } @@ -283,60 +326,8 @@ Prompt.prototype.createChoices = function(basePath) { }; -/** - * 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$/, ''); -} - -/** - * Function for getting list of folders in directory - * @param {String} basePath the path the folder to get a list of containing folders - * @return {Array} array of folder names inside of basePath - */ -function getDirectories(basePath) { - return fs - .readdirSync(basePath) - .filter(function(file) { - try { - var stats = fs.lstatSync(path.join(basePath, file)); - if (stats.isSymbolicLink()) { - return false; - } - var isDir = stats.isDirectory(); - var isNotDotFile = path.basename(file).indexOf('.') !== 0; - return isDir && isNotDotFile; - } catch (e) { - return false; - } - }) - .sort(); -} - - /** * Module exports */ -module.exports = Prompt; \ No newline at end of file +module.exports = Prompt; diff --git a/test/helpers/readline.js b/test/helpers/readline.js index 81619ef..767abba 100644 --- a/test/helpers/readline.js +++ b/test/helpers/readline.js @@ -1,8 +1,8 @@ "use strict"; -var EventEmitter = require('events').EventEmitter; -var sinon = require('sinon'); -var util = require('util'); -var _ = require('lodash'); +var EventEmitter = require("events").EventEmitter; +var sinon = require("sinon"); +var util = require("util"); +var _ = require("lodash"); var stub = {}; @@ -21,7 +21,7 @@ _.extend(stub, { end: sinon.stub(), mute: sinon.stub(), unmute: sinon.stub(), - __raw__: '', + __raw__: "", write: function(str) { this.__raw__ += str; }, @@ -32,7 +32,7 @@ _.extend(stub, { }); var ReadlineStub = function() { - this.line = ''; + this.line = ""; this.input = new EventEmitter(); EventEmitter.apply(this, arguments); }; @@ -41,23 +41,33 @@ util.inherits(ReadlineStub, EventEmitter); _.assign(ReadlineStub.prototype, stub); ReadlineStub.prototype.keyPress = function(letter) { - this.input.emit('keypress', letter, { - name: letter, + this.output.clear(); + this.input.emit("keypress", letter, { + name: letter }); }; +ReadlineStub.prototype.sendWord = function(word) { + word = word || ""; + word.split("").forEach(function(letter) { + this.keyPress(letter); + }, this); +}; ReadlineStub.prototype.moveDown = function() { - this.input.emit('keypress', '', { - name: 'down' + this.output.clear(); + this.input.emit("keypress", "", { + name: "down" }); }; ReadlineStub.prototype.moveUp = function() { - this.input.emit('keypress', '', { - name: 'up' + this.output.clear(); + this.input.emit("keypress", "", { + name: "up" }); }; ReadlineStub.prototype.enter = function() { - this.emit('line'); + this.output.clear(); + this.emit("line"); }; -module.exports = ReadlineStub; \ No newline at end of file +module.exports = ReadlineStub; diff --git a/test/spec/index.spec.js b/test/spec/index.spec.js index fa1421a..5470177 100644 --- a/test/spec/index.spec.js +++ b/test/spec/index.spec.js @@ -1,24 +1,25 @@ "use strict"; -var expect = require('chai').expect; -var mock = require('mock-fs'); -var ReadlineStub = require('../helpers/readline'); -var Prompt = require('../../src/index'); -var path = require('path'); +var expect = require("chai").expect; +var mock = require("mock-fs"); +var ReadlineStub = require("../helpers/readline"); +var Prompt = require("../../src/index"); +var path = require("path"); -describe('inquirer-directory', function() { +describe("inquirer-directory", function() { before(function() { mock({ - 'root': { - '.git': {}, - 'folder1': { - 'folder1-1': {} + "root": { + ".git": {}, + "folder1": { + "folder1-1": {} }, - 'zfolder2': {}, - 'some.png': new Buffer([8, 6, 7, 5, 3, 0, 9]), - 'a-symlink': mock.symlink({ - path: 'folder1' + "folder2": {}, + "zfolder2": {}, + "some.png": new Buffer([8, 6, 7, 5, 3, 0, 9]), + "a-symlink": mock.symlink({ + path: "folder1" }) } }); @@ -30,79 +31,120 @@ describe('inquirer-directory', function() { // need to clear "console after every action" this.rl = new ReadlineStub(); this.prompt = new Prompt({ - message: 'Choose a directory', - name: 'name', + message: "Choose a directory", + name: "name", basePath: "./root/" }, this.rl); }); afterEach(function() { this.rl.output.clear(); }); - it('requires a basePath', function() { + it("requires a basePath", function() { expect(function() { new Prompt({ - message: 'foo', - name: 'name', + message: "foo", + name: "name" }); }).to.throw(/basePath/); }); - it('should list folders', function() { + it("should list folders", function() { this.prompt.run(); - expect(this.rl.output.__raw__).to.contain('folder1'); - expect(this.rl.output.__raw__).to.contain('zfolder2'); + expect(this.rl.output.__raw__).to.contain("folder1"); + expect(this.rl.output.__raw__).to.contain("folder2"); + expect(this.rl.output.__raw__).to.contain("zfolder2"); }); - it('should not contain folders starting with "." (private folders)', function() { + it("should not contain folders starting with '.' (private folders)", function() { this.prompt.run(); - expect(this.rl.output.__raw__).to.not.contain('.git'); + expect(this.rl.output.__raw__).to.not.contain(".git"); }); - it('should not contain files', function() { + it("should not contain files", function() { this.prompt.run(); - expect(this.rl.output.__raw__).to.not.contain('some.png'); + expect(this.rl.output.__raw__).to.not.contain("some.png"); }); - it('should allow users to drill into folder', function() { + it("should allow users to drill into folder", function() { this.prompt.run(); this.rl.moveDown(); + this.rl.moveDown(); this.rl.enter(); - expect(this.rl.output.__raw__).to.contain('folder1-1'); + expect(this.rl.output.__raw__).to.contain("folder1-1"); }); - it('should allow users to go back after drilling', function() { + it("should allow users to go back after drilling", function() { this.prompt.run(); + this.rl.moveDown(); + this.rl.moveDown(); this.rl.enter(); - expect(this.rl.output.__raw__).to.contain('..'); - this.rl.output.clear(); + expect(this.rl.output.__raw__).to.contain(".."); this.rl.moveDown(); + expect(this.rl.output.__raw__).to.not.contain("zfolder2"); this.rl.enter(); - expect(this.rl.output.__raw__).to.contain('zfolder2'); + expect(this.rl.output.__raw__).to.contain("zfolder2"); }); - - it('should allow users to go past basePath', function() { + // + it("should allow users to go past basePath", function() { this.prompt.run(); + this.rl.moveDown(); this.rl.enter(); - expect(this.rl.output.__raw__).to.contain('..'); - expect(this.prompt.opt.choices.realChoices[0].name).to.equal('..'); + expect(this.rl.output.__raw__).to.contain(".."); + expect(this.prompt.opt.choices.realChoices[1].name).to.equal(".."); }); - it('should not display back option in root folder', function () { + it("should not display back option in root folder", function () { this.prompt.run(); - while (this.prompt.currentPath !== path.parse(path.resolve('.')).root) { - this.rl.output.clear(); + while (this.prompt.currentPath !== path.parse(path.resolve(".")).root) { + this.rl.moveDown(); this.rl.enter(); } - expect(this.rl.output.__raw__).to.not.contain('..'); + expect(this.rl.output.__raw__).to.not.contain(".."); + }); + it("should allow users to go back using '-' shortcut", function() { + this.prompt.run(); + expect(this.rl.output.__raw__).to.contain("zfolder2"); + this.rl.keyPress("-"); + expect(this.rl.output.__raw__).to.contain(".."); + expect(this.rl.output.__raw__).to.not.contain("zfolder2"); + }); + + it("should allow users search for a folder using '/' shortcut", function() { + this.prompt.run(); + expect(this.rl.output.__raw__).to.not.contain("Search:"); + this.rl.keyPress("/"); + var raw = this.rl.output.__raw__.replace("❯", ">"); + expect(raw).to.not.contain("> folder1"); + expect(this.rl.output.__raw__).to.contain("Search:"); + this.rl.keyPress("f"); + + raw = this.rl.output.__raw__.replace("❯", ">"); + expect(raw).to.have.string("> folder1"); + this.rl.sendWord("older2"); + raw = this.rl.output.__raw__.replace("❯", ">"); + expect(raw).to.contain("> folder2"); + }); + + it("should allow users to select a folder using 'choose this directory' choice", function() { + this.prompt.run(); + this.rl.moveUp(); + this.rl.enter(); + expect(this.prompt.currentPath.split('/').slice(-1)[0]).to.equal("root"); + }); + + it("should allow users to select a folder using '.' choice", function() { + this.prompt.run(); + this.rl.enter(); + expect(this.prompt.currentPath.split('/').slice(-1)[0]).to.equal("root"); }); - // it('should allow users to press keys to shortcut to that value', function (done) { + // it("should allow users to press keys to shortcut to that value", function (done) { // prompt.run(function (answer) { - // expect(answer).to.equal('zfolder2'); + // expect(answer).to.equal("zfolder2"); // done(); // }); - // keyPress('z'); + // keyPress("z"); // enter(); // enter(); // }); -}); \ No newline at end of file +});