UNPKG

inquirer-file-tree-selection-prompt

Version:
404 lines (403 loc) 16.3 kB
'use strict'; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; /** * `file-tree-slection` type prompt */ import chalk from 'chalk'; import figures from 'figures'; import cliCursor from 'cli-cursor'; import path from 'node:path'; import fs from 'node:fs'; import { fromEvent } from 'rxjs'; import { filter, share, map, takeUntil } from 'rxjs/operators'; import observe from 'inquirer/lib/utils/events.js'; import Base from 'inquirer/lib/prompts/base.js'; import Paginator from 'inquirer/lib/utils/paginator.js'; import { isSubPath } from './utils.js'; import { getUpperDirNode } from './upperDir.js'; /** * type: string * onlyShowDir: boolean (default: false) */ class FileTreeSelectionPrompt extends Base { constructor(questions, rl, answers) { super(questions, rl, answers); const root = path.resolve(process.cwd(), this.opt.root || '.'); const rootNode = { path: root, type: 'directory', name: '.(root directory)', _rootNode: true }; this.rootNode = rootNode; this.shownList = []; this.firstRender = true; this.opt = Object.assign(Object.assign({ default: null, pageSize: 10, onlyShowDir: false, multiple: false, states: false, }, this.opt), { selectedList: this.opt.selectedList || this.opt.default }); // Make sure no default is set (so it won't be printed) // this.opt.default = null; this.selectedList = this.opt.selectedList; if (this.selectedList) { !Array.isArray(this.selectedList) && (this.selectedList = [this.selectedList]); } else { if (this.opt.states) { this.selectedList = {}; } else { this.selectedList = []; } } this.paginator = new Paginator(this.screen); } get fileTree() { if (this.opt.hideRoot) { return this.rootNode; } return { children: [this.rootNode] }; } /** * Start the Inquiry session * @param {Function} cb Callback when prompt is done * @return {this} */ _run(cb) { return __awaiter(this, void 0, void 0, function* () { this.done = cb; var events = observe(this.rl); var validation = this.handleSubmitEvents(events.line.pipe(map(() => this.active.path))); validation.success.forEach(this.onSubmit.bind(this)); validation.error.forEach(this.onError.bind(this)); events.normalizedUpKey .pipe(takeUntil(validation.success)) .forEach(this.onUpKey.bind(this)); events.normalizedDownKey .pipe(takeUntil(validation.success)) .forEach(this.onDownKey.bind(this)); events.keypress.pipe(filter(({ key }) => key.name === 'right'), share()) .pipe(takeUntil(validation.success)) .forEach(this.onRigthKey.bind(this)); events.keypress.pipe(filter(({ key }) => key.name === 'left'), share()) .pipe(takeUntil(validation.success)) .forEach(this.onLeftKey.bind(this)); events.spaceKey .pipe(takeUntil(validation.success)) .forEach(this.onSpaceKey.bind(this, false)); function normalizeKeypressEvents(value, key) { return { value: value, key: key || {} }; } fromEvent(this.rl.input, 'keypress', normalizeKeypressEvents) .pipe(filter(({ key }) => key && key.name === 'tab'), share()) .pipe(takeUntil(validation.success)) .forEach(this.onSpaceKey.bind(this, true)); cliCursor.hide(); if (this.firstRender) { const rootNode = this.rootNode; yield this.prepareChildren(rootNode); rootNode.open = true; this.active = this.active || rootNode.children[0]; this.prepareChildren(this.active); this.render(); } return this; }); } renderFileTree(root = this.fileTree, indent = 2) { const children = root.children || []; let output = ''; const transformer = this.opt.transformer; const isFinal = this.status === 'answered'; let showValue; children.forEach(itemPath => { if (this.opt.onlyShowDir && itemPath.type !== 'directory') { return; } this.shownList.push(itemPath); let prefix = itemPath.type === 'directory' ? itemPath.open ? figures.arrowDown + ' ' : figures.arrowRight + ' ' : itemPath === this.active ? figures.play + ' ' : ''; // when multiple is true, add radio icon at prefix if (this.opt.multiple) { if (this.opt.states) { prefix += itemPath.path in this.selectedList ? this.opt.states.find((state) => state.state == this.selectedList[itemPath.path]).label : figures.radioOff; } else { prefix += this.selectedList.includes(itemPath.path) ? figures.radioOn : figures.radioOff; } prefix += ' '; } const safeIndent = (indent - prefix.length + 2) > 0 ? indent - prefix.length + 2 : 0; if (itemPath.name == '..') { showValue = `${' '.repeat(safeIndent)}${prefix}..(Press \`Space\` to go parent directory)\n`; } else if (transformer) { const transformedValue = transformer(itemPath.path, this.answers, { isFinal }); showValue = ' '.repeat(safeIndent) + prefix + transformedValue + '\n'; } else { showValue = ' '.repeat(safeIndent) + prefix + itemPath.name + (itemPath.type === 'directory' ? path.sep : '') + '\n'; } if (itemPath === this.active && itemPath.isValid) { output += chalk.cyan(showValue); } else if (itemPath === this.active && !itemPath.isValid) { output += chalk.red(showValue); } else { output += showValue; } if (itemPath.open) { output += this.renderFileTree(itemPath, indent + 2); } }); return output; } prepareChildren(node) { return __awaiter(this, void 0, void 0, function* () { const parentPath = node.path; try { if (node.name == '..' || fs.lstatSync(parentPath).isFile() || node.children || node.open === true) { return; } const children = fs.readdirSync(parentPath, { withFileTypes: true }).map(item => { return { parent: node, type: item.isFile() ? 'file' : 'directory', name: item.name, path: path.resolve(parentPath, item.name) }; }); node.children = children; } catch (e) { // maybe for permission denied, we cant read the dir // do nothing here } const validate = this.opt.validate; const filter = (val) => __awaiter(this, void 0, void 0, function* () { if (!this.opt.filter) { return val; } return yield this.opt.filter(val, this.answers); }); if (validate) { const addValidity = (fileObj) => __awaiter(this, void 0, void 0, function* () { const isValid = yield validate(yield filter(fileObj.path), this.answers); fileObj.isValid = false; if (isValid === true) { if (this.opt.onlyShowDir) { if (fileObj.type == 'directory') { fileObj.isValid = true; } } else { fileObj.isValid = true; } } if (fileObj.children) { if (this.opt.hideChildrenOfValid && fileObj.isValid) { fileObj.children.length = 0; } const children = fileObj.children.map(x => x); for (let index = 0, length = children.length; index < length; index++) { const child = children[index]; yield addValidity(child); if (child.isValid) { fileObj.hasValidChild = true; } if (this.opt.onlyShowValid && !child.hasValidChild && !child.isValid) { const spliceIndex = fileObj.children.indexOf(child); fileObj.children.splice(spliceIndex, 1); } } } }); yield addValidity(node); } if (this.opt.enableGoUpperDirectory && node === this.rootNode) { this.rootNode.children.unshift(getUpperDirNode(this.rootNode.path)); } // When it's single selection and has default value, we should expand to the default file. if (this.firstRender && this.opt.default && !this.opt.multiple) { const defaultPath = this.opt.default; const founded = node.children.find(item => { if (item.name === '..') { return false; } if (item.path === defaultPath) { return true; } if (defaultPath.includes(`${item.path}${path.sep}`)) { return true; } }); if (founded) { if (founded.path === defaultPath) { this.active = founded; let parent = founded.parent; while (parent && !parent._rootNode) { parent.open = true; parent = parent.parent; } } else { return yield this.prepareChildren(founded); } } } !this.firstRender && this.render(); }); } /** * Render the prompt to screen * @return {FileTreeSelectionPrompt} self */ render(error) { // Render question var message = this.getQuestion(); if (this.firstRender) { message += chalk.dim('(Use arrow keys, Use space to toggle folder)'); } if (this.status === 'answered') { message += chalk.cyan(this.opt.multiple ? JSON.stringify(this.selectedList) : this.active.path); } else { this.shownList = []; const fileTreeStr = this.renderFileTree(); message += '\n' + this.paginator.paginate(fileTreeStr + '----------------', this.shownList.indexOf(this.active), this.opt.pageSize); } let bottomContent; if (error) { bottomContent = '\n' + chalk.red('>> ') + error; } this.firstRender = false; this.screen.render(message, bottomContent); } onEnd(state) { this.status = 'answered'; // this.answer = state.value; // Re-render prompt this.render(); this.screen.done(); this.done(state.value); } onError(state) { this.render(state.isValid); } /** * When user press `enter` key */ onSubmit(state) { this.status = 'answered'; this.render(); this.screen.done(); cliCursor.show(); this.done(this.opt.multiple ? this.selectedList : state.value); } moveActive(distance = 0) { const currentIndex = this.shownList.indexOf(this.active); let index = currentIndex + distance; if (index >= this.shownList.length) { index = 0; } else if (index < 0) { index = this.shownList.length - 1; } this.active = this.shownList[index]; if (this.active.name !== '..') { this.prepareChildren(this.active); } this.render(); } /** * When user press a key */ onUpKey() { this.moveActive(-1); } onDownKey() { this.moveActive(1); } onLeftKey() { if ((this.active.type === 'file' || !this.active.open) && this.active.parent) { this.active = this.active.parent; } this.active.open = false; this.render(); } onRigthKey() { this.active.open = true; this.render(); } onSpaceKey(triggerByTab = false) { var _a; return __awaiter(this, void 0, void 0, function* () { if (!triggerByTab && this.active.name == '..' && isSubPath(this.active.path, this.rootNode.path)) { this.rootNode = Object.assign(Object.assign({}, this.active), { name: path.basename(this.active.path) }); yield this.prepareChildren(this.rootNode); this.active = (_a = this.rootNode.children) === null || _a === void 0 ? void 0 : _a[0]; this.firstRender = true; this.rootNode.open = true; this.render(); this.firstRender = false; return; } if (!triggerByTab && this.opt.multiple) { if (this.active.isValid === false) { return; } if (this.opt.states) { if (this.active.path in this.selectedList) { let nextStateIndex = 1 + this.opt.states.findIndex((state) => state.state == this.selectedList[this.active.path]); if (nextStateIndex < this.opt.states.length) { this.selectedList[this.active.path] = this.opt.states[nextStateIndex].state; } else { delete this.selectedList[this.active.path]; } } else { this.selectedList[this.active.path] = this.opt.states[0].state; } } else { if (this.selectedList.includes(this.active.path)) { this.selectedList.splice(this.selectedList.indexOf(this.active.path), 1); } else { this.selectedList.push(this.active.path); } } this.render(); return; } if (this.active.children && this.active.children.length === 0) { return; } this.active.open = !this.active.open; this.render(); }); } } export default FileTreeSelectionPrompt;