diff --git a/src/JInquirer/JInquirer.js b/src/JInquirer/JInquirer.js new file mode 100644 index 0000000000000000000000000000000000000000..e60350e645358e39a7b141e2aa7acd759a00a689 --- /dev/null +++ b/src/JInquirer/JInquirer.js @@ -0,0 +1,114 @@ +const inquirer = require("inquirer"); +const path = require('path'); +const fs = require('fs'); +const ansiEscapes = require('ansi-escapes') // 用于输出空行 +const MuteStream = require('mute-stream') // 用于沉默输出流 + +class JInquirer{ + constructor(options,config){ + this.options = options; + this.config = config; + this.answer = {}; + } + getFileList = (dirPath)=>{ + const list = fs.readdirSync(dirPath); + return ['../(返回上一级)',...list]; + } + getFolderList = (dirPath)=>{ + const list = fs.readdirSync(dirPath); + let resList = []; + list.map(item=>{ + const fullPath = path.join(dirPath,item); + if(fs.statSync(fullPath).isDirectory()){ + resList.push(item + '(进入文件夹)'); + resList.push(item + '(选择文件夹)'); + } + }); + return ['../(返回上一级)',...resList]; + } + clear = (number) => { // 清屏 + const emptyLines = ansiEscapes.eraseLines(number) // 根据函数通过 eraseLines 获取空行 + this.output.write(emptyLines) // 输出空行,完成清屏 + } + async prompt(){ + const options = this.options; + const ms = new MuteStream() // 封装沉默输出流 + ms.pipe(process.stdout) + this.output = ms // 导入实例 + for(let i = 0; i < options.length; i++){ + const res = await this.run(options[i]); + if(options[i].pathType == 'absolute'){ + this.answer[options[i].name] = path.join(__dirname,res); + }else{ + this.answer[options[i].name] = res; + } + } + return this.answer; + } + async run(option){ + if(option.type === 'file'){ + return this.chooseFile(option); + }else if(option.type === 'folder'){ + return this.chooseFolder(option); + }else{ + if(option.notNull){ + const flag = option.message.slice(-1); + if([":",":"].includes(flag)){ + option.message = option.message.slice(0,-1) + '(不能为空)' + flag; + } + } + return this.defaultType(option); + } + } + async chooseFile(option,dirPath = './'){ + option.type = 'list'; + option.suffix = "(当前浏览目录:" + path.join(__dirname,dirPath) + ')'; + option.pageSize = fs.readdirSync('./').length + 1; + option.choices = [...this.getFileList(dirPath)]; + const answer = await inquirer.prompt([ + option + ]); + if(answer[option.name] == '../(返回上一级)'){ + this.clear(2); + return this.chooseFile(option,path.join(dirPath,'/../')); + }else{ + const fullPath = path.join(dirPath, answer[option.name]); + if(!fs.statSync(fullPath).isFile()){ + this.clear(2); + return this.chooseFile(option,fullPath); + }else{ + return path.join(dirPath, answer[option.name]); + } + } + } + async chooseFolder(option,dirPath = './'){ + option.type = 'list'; + option.suffix = "(当前浏览目录:" + path.join(__dirname,dirPath) + ')'; + option.pageSize = fs.readdirSync('./').length + 1; + option.choices = [...this.getFolderList(dirPath)]; + const answer = await inquirer.prompt([ + option + ]); + if(answer[option.name] == '../(返回上一级)'){ + this.clear(2); + return this.chooseFolder(option,path.join(dirPath,'/../')); + }else if(answer[option.name].endsWith('(进入文件夹)')){ + const iPath = answer[option.name].slice(0,-7); + this.clear(2); + return this.chooseFolder(option,path.join(dirPath,iPath)); + }else{ + return path.join(dirPath, answer[option.name].slice(0,-7)); + } + } + async defaultType(option){ + const answer = await inquirer.prompt([ + option + ]); + if(option.notNull && answer[option.name] === ''){ + this.clear(2); + return this.defaultType(option); + } + return answer[option.name]; + } +} +module.exports = JInquirer; \ No newline at end of file diff --git a/src/JInquirer/README.md b/src/JInquirer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..478d2abd745cb0d5d6fcd4732d18bb41682ea9d4 --- /dev/null +++ b/src/JInquirer/README.md @@ -0,0 +1,120 @@ +## JInquirer +对[inquirer](//https://www.npmjs.com/package/inquirer#examples)插件进行二次封装,支持选择文件和文件夹。 +## 配置参数说明 +### inquirer原有参数 +- type +> 表示提问的类型,包括:input、confirm、 list、rawlist、expand、checkbox、password、editor。 +- name +> 存储当前输入的值。 +- message +> 问题的描述。 +- default +> 默认值。 +- choices +> 列表选项,在某些type下可用,并且包含一个分隔符(separator); +- validate +> 对用户的回答进行校验。 +- filter +> 对用户的回答进行过滤处理,返回处理后的值。 +- when +> 根据前面问题的回答,判断当前问题是否需要被回答。 +- pageSize +> 修改某些type类型下的渲染行数。 +- prefix +> 修改message默认前缀。 +- suffix +> 修改message默认后缀。 +### 新增参数 +- type +> 在原有类型中新增两种类型:file、folder,分别为文件选择器和目录选择器。 +- pathType +> 此项为新增配置,设置目录和文件选择器选中路径输出的格式,默认为相对路径,可以设置为**absolute**,此时会输出绝对路径。 +## 示例 +```shell +npm install @jyeontu/j-inquirer +``` +```javascript +const JInquirer = require('@jyeontu/j-inquirer'); +let options = [ + { + type:"input", + message:"请输入你的姓名:", + name:"name", + notNull:true + },{ + type:"input", + message:"请输入你的年龄:", + name:"age", + default:18, + validate:(val)=>{ + if(val < 0 || val > 150){ + return "请输入0~150之间的数字"; + } + return true; + } + },{ + type:"file", + message:"请选择文件:", + name:"fileName", + default:"", + },{ + type:"folder", + message:"请选择文件夹:", + name:"folderName", + default:"", + pathType:'absolute' + },{ + type:"list", + message:"请选择你喜欢的水果:", + name:"fruit", + default:"Apple", + choices:[ + "Apple", + "pear", + "Banana" + ], + },{ + type:"expand", + message:"请选择一个颜色:", + name:"color", + default:"red", + choices:[ + { + key : 'R', + value : "red" + }, + { + key : 'B', + value : "blue" + }, + { + key : 'G', + value : "green" + } + ] + },{ + type:"checkbox", + message:"选择一至多种颜色:", + name:"color2", + choices:[ + "red", + "blue", + "green", + "pink", + "orange" + ] + },{ + type:"password", + message:"请输入你的密码:", + name:"pwd" + },{ + type:"editor", + message:"写下你想写的东西:", + name:"editor" + } +]; +let j = new JInquirer(options); +let res = j.prompt().then(res=>{ + console.log(res); +}); +``` \ No newline at end of file diff --git a/src/JInquirer/example.js b/src/JInquirer/example.js new file mode 100644 index 0000000000000000000000000000000000000000..fda368adfe9d7a2e342e330644e3eed7afe13998 --- /dev/null +++ b/src/JInquirer/example.js @@ -0,0 +1,83 @@ +const JInquirer = require('./index'); +let options = [ + { + type:"input", + message:"请输入你的姓名:", + name:"name", + notNull:true + },{ + type:"input", + message:"请输入你的年龄:", + name:"age", + default:18, + validate:(val)=>{ + if(val < 0 || val > 150){ + return "请输入0~150之间的数字"; + } + return true; + } + },{ + type:"file", + message:"请选择文件:", + name:"fileName", + default:"", + },{ + type:"folder", + message:"请选择文件夹:", + name:"folderName", + default:"", + pathType:'absolute' + },{ + type:"list", + message:"请选择你喜欢的水果:", + name:"fruit", + default:"Apple", + choices:[ + "Apple", + "pear", + "Banana" + ], + },{ + type:"expand", + message:"请选择一个颜色:", + name:"color", + default:"red", + choices:[ + { + key : 'R', + value : "red" + }, + { + key : 'B', + value : "blue" + }, + { + key : 'G', + value : "green" + } + ] + },{ + type:"checkbox", + message:"选择一至多种颜色:", + name:"color2", + choices:[ + "red", + "blue", + "green", + "pink", + "orange" + ] + },{ + type:"password", + message:"请输入你的密码:", + name:"pwd" + },{ + type:"editor", + message:"写下你想写的东西:", + name:"editor" + } +]; +let j = new JInquirer(options); +let res = j.prompt().then(res=>{ + console.log(res); +}); diff --git a/src/JInquirer/index.js b/src/JInquirer/index.js new file mode 100644 index 0000000000000000000000000000000000000000..71609b5a675e7f01d77f4315afbfe0120dc139c0 --- /dev/null +++ b/src/JInquirer/index.js @@ -0,0 +1,2 @@ +const JInquirer = require('./JInquirer'); +module.exports = JInquirer; \ No newline at end of file diff --git a/src/JInquirer/other.js b/src/JInquirer/other.js new file mode 100644 index 0000000000000000000000000000000000000000..d944e3e6573e6d996cf7a8d04e2ed6659c20d1be --- /dev/null +++ b/src/JInquirer/other.js @@ -0,0 +1,152 @@ +const EventsEmitter = require('events') +const readline = require('readline') + +const MuteStream = require('mute-stream') // 用于沉默输出流 +const ansiEscapes = require('ansi-escapes') // 用于输出空行 + +const option = { // 选项参数 + type: 'list', + name: 'name', + message: 'your chioce: ', + chioces: [ + { + name: '1000', + value: 1000, + }, + { + name: '2000', + value: 2000, + }, + { + name: '3000', + value: 3000, + } + ] +} + +function prompt(option) { // 核心方法 + return new Promise(function (resolve, reject) { // 返回一个 Promise + try { + const list = new List(option); // 构建 List 实例 + list.render(); // 调用渲染方法,渲染出列表 + list.on('exit', function (answer) { // 监听退出 + resolve(answer) // 返回结果 + }) + } catch (e) { + reject(e) // 返回错误 + } + }) +} + +class List extends EventsEmitter { // List 实例, 继承 events + constructor(option) { + super() + // 接收参数 + this.name = option.name + this.message = option.message + this.chioces = option.chioces + // 创建输入、输出流 + this.input = process.stdin + const ms = new MuteStream() // 封装沉默输出流 + ms.pipe(process.stdout) + this.output = ms // 导入实例 + this.rl = readline.createInterface({ // 启动 readline 监听 + input: this.input, + output: this.output, + }) + this.selected = 0 // 默认选项下标 + this.heigth = 0 // 默认渲染行数,用于创建空行 + this.keypress = this.rl.input.on('keypress', this.onKeypress) // 监听键盘输入 + this.haveSelected = false // 是否选择完毕 + } + + onKeypress = (...keymap) => { // 处理键盘输入,keymap 为数组 + /** + 如: + [ + undefined, + { + sequence: '\x1B[B', + name: 'down', + ctrl: false, + meta: false, + shift: false, + code: '[B' + } + ] + */ + let key = keymap[1] // 取第二个参数 + // 判断上下,操作 selected 下标 + if (key.name == 'up') { + this.selected-- + if (this.selected < 0) { + this.selected = this.chioces.length - 1 + } + this.render() // 处理完执行渲染函数 + } else if (key.name == 'down') { + this.selected++ + if (this.selected > this.chioces.length - 1) { + this.selected = 0 + } + this.render() // 处理完执行渲染函数 + } else if (key.name === 'return') { // 判断回车 + this.haveSelected = true // 完成选择 + this.render() // 处理完执行渲染函数 + this.close() // 调用关闭方法 + this.emit('exit', this.chioces[this.selected]) // 发布完成事件 + } + } + + render() { // 渲染函数 + this.output.unmute() // 取消输出流静默 + this.clear() // 清屏 + this.output.write(this.getContent()) // 输出内容 + this.output.mute() // 静默输出流,让用户的输入,不会显示到控制台 + } + + getContent = () => { // 获取输出内容 + if (!this.haveSelected) { // 判断是否完成选择 + let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + + '\x1B[22m(Use arrow Keys)\x1B[0m\n' // 标题内容 \x1B[1m 加粗 + this.chioces.forEach((chioce, index) => { // 处理选项 + if (this.selected === index) { + if (index === this.chioces.length - 1) { + title += '\x1B[36m> ' + chioce.name + '\x1B[0m' // 当前选中,添加颜色 + } else { + title += '\x1B[36m> ' + chioce.name + '\x1B[0m\n' + } + } else { + if (index === this.chioces.length - 1) { + title += ' ' + chioce.name + } else { + title += ' ' + chioce.name + '\n' + } + } + }) + this.heigth = this.chioces.length + 1 // 保存显示内容行数 + return title + } else { + // 完成选择内容 + const name = this.chioces[this.selected].name + let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + + '\x1B[22m\x1B[0m\x1B[36m' + name + '\x1B[39m\x1B[0m \n' + return title + } + } + + clear() { // 清屏 + const emptyLines = ansiEscapes.eraseLines(this.heigth) // 根据函数通过 eraseLines 获取空行 + this.output.write(emptyLines) // 输出空行,完成清屏 + } + + close() { // 关闭 + this.output.unmute() // 输出流解除静默 + this.rl.output.end() // 结束输出流 + this.rl.pause() // 关闭 readline 输入 + this.rl.close() // 调用 readline 关闭方法 + } +} + +prompt(option).then(answer => { // 调用 prompt 方法 + console.log(answer) // 输出结果 +}) diff --git a/src/JInquirer/package.json b/src/JInquirer/package.json new file mode 100644 index 0000000000000000000000000000000000000000..eefc47e018b9ae113f481c97afc4a33862e689ef --- /dev/null +++ b/src/JInquirer/package.json @@ -0,0 +1,18 @@ +{ + "name": "@jyeontu/j-inquirer", + "version": "1.0.0", + "description": "基于inquirer控制台交互的二次封装", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["command", "prompt", "stdin", "cli", "tty", "menu"], + "author": "jyeontu", + "license": "ISC", + "dependencies": { + "ansi-escapes": "^4.0.0", + "inquirer": "^8.0.0", + "mute-stream": "0.0.8", + "readline": "^1.3.0" + } +}