index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. 'use strict';
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  4. return new (P || (P = Promise))(function (resolve, reject) {
  5. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  6. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  7. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  8. step((generator = generator.apply(thisArg, _arguments || [])).next());
  9. });
  10. };
  11. var __importDefault = (this && this.__importDefault) || function (mod) {
  12. return (mod && mod.__esModule) ? mod : { "default": mod };
  13. };
  14. Object.defineProperty(exports, "__esModule", { value: true });
  15. /**
  16. * `file-tree-slection` type prompt
  17. */
  18. const chalk = require('chalk');
  19. const figures = require('figures');
  20. const cliCursor = require('cli-cursor');
  21. const path = require('path');
  22. const fs = require('fs');
  23. const { fromEvent } = require('rxjs');
  24. const { filter, share, map, takeUntil } = require('rxjs/operators');
  25. const observe = require('inquirer/lib/utils/events');
  26. const base_1 = __importDefault(require("inquirer/lib/prompts/base"));
  27. const paginator_1 = __importDefault(require("inquirer/lib/utils/paginator"));
  28. const utils_1 = require("./utils");
  29. const upperDir_1 = require("./upperDir");
  30. /**
  31. * type: string
  32. * onlyShowDir: boolean (default: false)
  33. */
  34. class FileTreeSelectionPrompt extends base_1.default {
  35. constructor(questions, rl, answers) {
  36. super(questions, rl, answers);
  37. const root = path.resolve(process.cwd(), this.opt.root || '.');
  38. const rootNode = {
  39. path: root,
  40. type: 'directory',
  41. name: '.(root directory)',
  42. _rootNode: true
  43. };
  44. this.rootNode = rootNode;
  45. this.shownList = [];
  46. this.firstRender = true;
  47. this.opt = Object.assign(Object.assign({
  48. default: null,
  49. pageSize: 10,
  50. onlyShowDir: false,
  51. multiple: false,
  52. states: false,
  53. }, this.opt), { selectedList: this.opt.selectedList || this.opt.default });
  54. // Make sure no default is set (so it won't be printed)
  55. // this.opt.default = null;
  56. this.selectedList = this.opt.selectedList;
  57. if (this.selectedList) {
  58. !Array.isArray(this.selectedList) && (this.selectedList = [this.selectedList]);
  59. }
  60. else {
  61. if (this.opt.states) {
  62. this.selectedList = {};
  63. }
  64. else {
  65. this.selectedList = [];
  66. }
  67. }
  68. this.paginator = new paginator_1.default(this.screen);
  69. }
  70. get fileTree() {
  71. if (this.opt.hideRoot) {
  72. return this.rootNode;
  73. }
  74. return {
  75. children: [this.rootNode]
  76. };
  77. }
  78. /**
  79. * Start the Inquiry session
  80. * @param {Function} cb Callback when prompt is done
  81. * @return {this}
  82. */
  83. _run(cb) {
  84. return __awaiter(this, void 0, void 0, function* () {
  85. this.done = cb;
  86. var events = observe(this.rl);
  87. var validation = this.handleSubmitEvents(events.line.pipe(map(() => this.active.path)));
  88. validation.success.forEach(this.onSubmit.bind(this));
  89. validation.error.forEach(this.onError.bind(this));
  90. events.normalizedUpKey
  91. .pipe(takeUntil(validation.success))
  92. .forEach(this.onUpKey.bind(this));
  93. events.normalizedDownKey
  94. .pipe(takeUntil(validation.success))
  95. .forEach(this.onDownKey.bind(this));
  96. events.keypress.pipe(filter(({ key }) => key.name === 'right'), share())
  97. .pipe(takeUntil(validation.success))
  98. .forEach(this.onRigthKey.bind(this));
  99. events.keypress.pipe(filter(({ key }) => key.name === 'left'), share())
  100. .pipe(takeUntil(validation.success))
  101. .forEach(this.onLeftKey.bind(this));
  102. events.spaceKey
  103. .pipe(takeUntil(validation.success))
  104. .forEach(this.onSpaceKey.bind(this, false));
  105. function normalizeKeypressEvents(value, key) {
  106. return { value: value, key: key || {} };
  107. }
  108. fromEvent(this.rl.input, 'keypress', normalizeKeypressEvents)
  109. .pipe(filter(({ key }) => key && key.name === 'tab'), share())
  110. .pipe(takeUntil(validation.success))
  111. .forEach(this.onSpaceKey.bind(this, true));
  112. cliCursor.hide();
  113. if (this.firstRender) {
  114. const rootNode = this.rootNode;
  115. yield this.prepareChildren(rootNode);
  116. rootNode.open = true;
  117. this.active = this.active || rootNode.children[0];
  118. this.prepareChildren(this.active);
  119. this.render();
  120. }
  121. return this;
  122. });
  123. }
  124. renderFileTree(root = this.fileTree, indent = 2) {
  125. const children = root.children || [];
  126. let output = '';
  127. const transformer = this.opt.transformer;
  128. const isFinal = this.status === 'answered';
  129. let showValue;
  130. children.forEach(itemPath => {
  131. if (this.opt.onlyShowDir && itemPath.type !== 'directory') {
  132. return;
  133. }
  134. this.shownList.push(itemPath);
  135. let prefix = itemPath.type === 'directory'
  136. ? itemPath.open
  137. ? figures.arrowDown + ' '
  138. : figures.arrowRight + ' '
  139. : itemPath === this.active
  140. ? figures.play + ' '
  141. : '';
  142. // when multiple is true, add radio icon at prefix
  143. if (this.opt.multiple) {
  144. if (this.opt.states) {
  145. prefix += itemPath.path in this.selectedList ? this.opt.states.find((state) => state.state == this.selectedList[itemPath.path]).label : figures.radioOff;
  146. }
  147. else {
  148. prefix += this.selectedList.includes(itemPath.path) ? figures.radioOn : figures.radioOff;
  149. }
  150. prefix += ' ';
  151. }
  152. const safeIndent = (indent - prefix.length + 2) > 0
  153. ? indent - prefix.length + 2
  154. : 0;
  155. if (itemPath.name == '..') {
  156. showValue = `${' '.repeat(safeIndent)}${prefix}..(Press \`Space\` to go parent directory)\n`;
  157. }
  158. else if (transformer) {
  159. const transformedValue = transformer(itemPath.path, this.answers, { isFinal });
  160. showValue = ' '.repeat(safeIndent) + prefix + transformedValue + '\n';
  161. }
  162. else {
  163. showValue = ' '.repeat(safeIndent) + prefix + itemPath.name + (itemPath.type === 'directory' ? path.sep : '') + '\n';
  164. }
  165. if (itemPath === this.active && itemPath.isValid) {
  166. output += chalk.cyan(showValue);
  167. }
  168. else if (itemPath === this.active && !itemPath.isValid) {
  169. output += chalk.red(showValue);
  170. }
  171. else {
  172. output += showValue;
  173. }
  174. if (itemPath.open) {
  175. output += this.renderFileTree(itemPath, indent + 2);
  176. }
  177. });
  178. return output;
  179. }
  180. prepareChildren(node) {
  181. return __awaiter(this, void 0, void 0, function* () {
  182. const parentPath = node.path;
  183. try {
  184. if (node.name == '..' || !fs.lstatSync(parentPath).isDirectory() || node.children || node.open === true) {
  185. return;
  186. }
  187. const children = fs.readdirSync(parentPath, { withFileTypes: true }).map(item => {
  188. return {
  189. parent: node,
  190. type: item.isDirectory() ? 'directory' : 'file',
  191. name: item.name,
  192. path: path.resolve(parentPath, item.name)
  193. };
  194. });
  195. node.children = children;
  196. }
  197. catch (e) {
  198. // maybe for permission denied, we cant read the dir
  199. // do nothing here
  200. }
  201. const validate = this.opt.validate;
  202. const filter = (val) => __awaiter(this, void 0, void 0, function* () {
  203. if (!this.opt.filter) {
  204. return val;
  205. }
  206. return yield this.opt.filter(val, this.answers);
  207. });
  208. if (validate) {
  209. const addValidity = (fileObj) => __awaiter(this, void 0, void 0, function* () {
  210. const isValid = yield validate(yield filter(fileObj.path), this.answers);
  211. fileObj.isValid = false;
  212. if (isValid === true) {
  213. if (this.opt.onlyShowDir) {
  214. if (fileObj.type == 'directory') {
  215. fileObj.isValid = true;
  216. }
  217. }
  218. else {
  219. fileObj.isValid = true;
  220. }
  221. }
  222. if (fileObj.children) {
  223. if (this.opt.hideChildrenOfValid && fileObj.isValid) {
  224. fileObj.children.length = 0;
  225. }
  226. const children = fileObj.children.map(x => x);
  227. for (let index = 0, length = children.length; index < length; index++) {
  228. const child = children[index];
  229. yield addValidity(child);
  230. if (child.isValid) {
  231. fileObj.hasValidChild = true;
  232. }
  233. if (this.opt.onlyShowValid && !child.hasValidChild && !child.isValid) {
  234. const spliceIndex = fileObj.children.indexOf(child);
  235. fileObj.children.splice(spliceIndex, 1);
  236. }
  237. }
  238. }
  239. });
  240. yield addValidity(node);
  241. }
  242. if (this.opt.enableGoUpperDirectory && node === this.rootNode) {
  243. this.rootNode.children.unshift((0, upperDir_1.getUpperDirNode)(this.rootNode.path));
  244. }
  245. // When it's single selection and has default value, we should expand to the default file.
  246. if (this.firstRender && this.opt.default && !this.opt.multiple) {
  247. const defaultPath = this.opt.default;
  248. const founded = node.children.find(item => {
  249. if (item.name === '..') {
  250. return false;
  251. }
  252. if (item.path === defaultPath) {
  253. return true;
  254. }
  255. if (defaultPath.includes(`${item.path}${path.sep}`)) {
  256. return true;
  257. }
  258. });
  259. if (founded) {
  260. if (founded.path === defaultPath) {
  261. this.active = founded;
  262. let parent = founded.parent;
  263. while (parent && !parent._rootNode) {
  264. parent.open = true;
  265. parent = parent.parent;
  266. }
  267. }
  268. else {
  269. return yield this.prepareChildren(founded);
  270. }
  271. }
  272. }
  273. !this.firstRender && this.render();
  274. });
  275. }
  276. /**
  277. * Render the prompt to screen
  278. * @return {FileTreeSelectionPrompt} self
  279. */
  280. render(error) {
  281. // Render question
  282. var message = this.getQuestion();
  283. if (this.firstRender) {
  284. message += chalk.dim('(Use arrow keys, Use space to toggle folder)');
  285. }
  286. if (this.status === 'answered') {
  287. message += chalk.cyan(this.opt.multiple ? JSON.stringify(this.selectedList) : this.active.path);
  288. }
  289. else {
  290. this.shownList = [];
  291. const fileTreeStr = this.renderFileTree();
  292. message += '\n' + this.paginator.paginate(fileTreeStr + '----------------', this.shownList.indexOf(this.active), this.opt.pageSize);
  293. }
  294. let bottomContent;
  295. if (error) {
  296. bottomContent = '\n' + chalk.red('>> ') + error;
  297. }
  298. this.firstRender = false;
  299. this.screen.render(message, bottomContent);
  300. }
  301. onEnd(state) {
  302. this.status = 'answered';
  303. // this.answer = state.value;
  304. // Re-render prompt
  305. this.render();
  306. this.screen.done();
  307. this.done(state.value);
  308. }
  309. onError(state) {
  310. this.render(state.isValid);
  311. }
  312. /**
  313. * When user press `enter` key
  314. */
  315. onSubmit(state) {
  316. this.status = 'answered';
  317. this.render();
  318. this.screen.done();
  319. cliCursor.show();
  320. this.done(this.opt.multiple ? this.selectedList : state.value);
  321. }
  322. moveActive(distance = 0) {
  323. const currentIndex = this.shownList.indexOf(this.active);
  324. let index = currentIndex + distance;
  325. if (index >= this.shownList.length) {
  326. index = 0;
  327. }
  328. else if (index < 0) {
  329. index = this.shownList.length - 1;
  330. }
  331. this.active = this.shownList[index];
  332. if (this.active.name !== '..') {
  333. this.prepareChildren(this.active);
  334. }
  335. this.render();
  336. }
  337. /**
  338. * When user press a key
  339. */
  340. onUpKey() {
  341. this.moveActive(-1);
  342. }
  343. onDownKey() {
  344. this.moveActive(1);
  345. }
  346. onLeftKey() {
  347. if ((this.active.type === 'file' || !this.active.open) && this.active.parent) {
  348. this.active = this.active.parent;
  349. }
  350. this.active.open = false;
  351. this.render();
  352. }
  353. onRigthKey() {
  354. this.active.open = true;
  355. this.render();
  356. }
  357. onSpaceKey(triggerByTab = false) {
  358. var _a;
  359. return __awaiter(this, void 0, void 0, function* () {
  360. if (!triggerByTab && this.active.name == '..' && (0, utils_1.isSubPath)(this.active.path, this.rootNode.path)) {
  361. this.rootNode = Object.assign(Object.assign({}, this.active), { name: path.basename(this.active.path) });
  362. yield this.prepareChildren(this.rootNode);
  363. this.active = (_a = this.rootNode.children) === null || _a === void 0 ? void 0 : _a[0];
  364. this.firstRender = true;
  365. this.rootNode.open = true;
  366. this.render();
  367. this.firstRender = false;
  368. return;
  369. }
  370. if (!triggerByTab && this.opt.multiple) {
  371. if (this.active.isValid === false) {
  372. return;
  373. }
  374. if (this.opt.states) {
  375. if (this.active.path in this.selectedList) {
  376. let nextStateIndex = 1 + this.opt.states.findIndex((state) => state.state == this.selectedList[this.active.path]);
  377. if (nextStateIndex < this.opt.states.length) {
  378. this.selectedList[this.active.path] = this.opt.states[nextStateIndex].state;
  379. }
  380. else {
  381. delete this.selectedList[this.active.path];
  382. }
  383. }
  384. else {
  385. this.selectedList[this.active.path] = this.opt.states[0].state;
  386. }
  387. }
  388. else {
  389. if (this.selectedList.includes(this.active.path)) {
  390. this.selectedList.splice(this.selectedList.indexOf(this.active.path), 1);
  391. }
  392. else {
  393. this.selectedList.push(this.active.path);
  394. }
  395. }
  396. this.render();
  397. return;
  398. }
  399. if (this.active.children && this.active.children.length === 0) {
  400. return;
  401. }
  402. this.active.open = !this.active.open;
  403. this.render();
  404. });
  405. }
  406. }
  407. module.exports = FileTreeSelectionPrompt;
  408. exports.default = FileTreeSelectionPrompt;