• 掌握:readlineeventsstreamansi-escapesrxjs
  • 掌握命令行交互的实现原理,并实现一个可交互的列表
  • 分析inquirer源码掌握其中的关键实现

generator

1
2
3
4
5
6
7
8
9
10
11
function* g(){
console.log('read')
let a=yield
console.log(a)
let b=yield
console.log(b)
}
const f=g();
f.next()
f.next('one')
f.next('two')

readline源码分析

1
2
3
4
5
6
// 强制将函数转换为构造函数
if(!this instanceof Interface){
return new Interface(input,output,completer,terminal)
}
// 当前 this 继承 EventEmitter 的所有方法
EventEmitter.call(this)

demo

1
2
3
4
5
6
7
8
9
const readline=require('readline')
const rl=readline.createInterface({
input:process.stdin,
output:process.stdout
})
rl.question('your name: ',(answer)=>{
console.log('your name: '+answer)
rl.close()
})

stepread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function stepRead(cb){
function onkeypress(s){
output.write(s);
line+=s;
switch (s){
case '\r':
input.pause()
cb(line)
break;
}

}
const input=process.stdin
const output=process.stdout
let line=''
emitKeypressEvents(input)
input.on('keypress',onkeypress)
input.setRawMode(true)
input.resume()
}
function emitKeypressEvents(stream){
function onData(chunk){
g.next(chunk.toString())
}
const g=emitKeys(stream)
g.next()
stream.on('data',onData)
}
function* emitKeys(stream){
while (true){
let ch=yield;
stream.emit('keypress',ch)
}
}
stepRead(function (s){
console.log('answer:'+s)
})

ansi(支持 js、nodejs)

ansi-escapes支持的颜色表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
参考文档:https://handwiki.org/wiki/ANSI_escape_code
终端不同、显示效果不一样
cmd下划线不显示,可以使用git bash
*/
//字体颜色蓝色,从这开始的字体颜色都是蓝色(34)
console.log('\x1B[34m%s','your name:')
//背景颜色红色,从这开始的背景颜色都是红色(41)
console.log('\x1B[41m%s','your name:')
//背景颜色红色,下一行终止背景颜色为红色(\x1B[0m)
console.log('\x1B[41m%s\x1B[0m','your name:')
//字体白色,下划线、下一行终止这些样式
console.log('\x1B[97m\x1B[4m%s\x1B[0m','your name:')
//光标向下移动两行打印
console.log('\x1B[2B%s','your name:')
//光标向右移动两格打印
console.log('\x1B[2G%s','your name123:')

rxjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { range } = require('rxjs');
const { map, filter } = require('rxjs/operators');

// range(1, 200)
// .pipe(
// filter(x => x % 2 === 1),
// map(x => x + x)
// )
// .subscribe(x => console.log(x));

const pipe=range(1, 200)
.pipe(
filter(x => x % 2 === 1),
map(x => x + x),
filter(x => x % 3 === 0),
filter(x => x % 5 === 0),
filter(x => x % 9 === 0)
)
pipe.subscribe(x => console.log('log1:',x));
pipe.subscribe(x => console.log('log2:',x*100));

手动实现终端列表选择(up+down+enter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
const EventEmitter=require('events')
const readline=require('readline')
const MuteStream=require('mute-stream')
const {fromEvent}=require('rxjs')
const ansiEscapes=require('ansi-escapes');

const option={
type:'list',
name:'name',
message:'select your name',
choices:[{
name:'Baran',
value:'isMe'
},{
name:'张三',
value:'他塔'
},{
name:'李四',
value:'他他'
}]
}

function Prompt(option){
return new Promise((resolve, reject)=>{
try {
const list=new List(option)
list.render()
list.on('exit',function (answers){
resolve(answers)
})
}catch (e){
reject(e)
}
})
}

// EventEmitter.call(this)
class List extends EventEmitter{
constructor(option) {
super();
this.message=option.message
this.choices=option.choices
this.input=process.stdin
const ms=new MuteStream();
ms.pipe(process.stdout)
this.output=ms //process.stdout
this.rl=readline.createInterface({
input:this.input,
output:this.output
})
this.selected=0;
this.height=-10;
this.keypress=fromEvent(this.rl.input,'keypress')
.forEach(this.onkeypress)
this.haveSelected=false; // 是否已经选择完毕
}
onkeypress=(keymap)=>{
// console.log(keymap)
const key=keymap[1];
switch (key.name){
case 'down':
this.selected++;
if(this.selected>this.choices.length-1){
this.selected=0
}
this.render()
break;
case 'up':
this.selected--;
if(this.selected<0){
this.selected=this.choices.length-1
}
this.render()
break;
default:
this.haveSelected=true;
this.render()
this.close()
this.emit('exit',this.choices[this.selected])
}
}
render(){
this.output.unmute(); //
this.clean()
this.output.write(this.getContent())
this.output.mute();
}
getContent=()=>{
if(!this.haveSelected){
// let title=this.message+'(Use arrow keys)\n'
let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';
this.choices.forEach((choice,index)=>{
// 判断是否为最后一个元素,如果是,则不加 \n
if(index===this.selected){
if(index===this.choices.length-1){
// title+='> '+choice.name
title+='\x1B[36m> '+choice.name+'\x1B[39m '
}else{
// title+='> '+choice.name+'\n'
title+='\x1B[36m> '+choice.name+'\x1B[39m \n'
}
}else{
if(index===this.choices.length-1){
title+=' '+choice.name
}else{
title+=' '+choice.name+'\n'
}
}
})
this.height=this.choices.length+1;
return title;
}else{
// 输入结束后的逻辑
const name = this.choices[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;
}
}
clean(){
const emptyLines=ansiEscapes.eraseLines(this.height)
this.output.write(emptyLines)
}
close(){
this.output.unmute()
this.rl.output.end()
this.rl.pause()
this.rl.close()
}

}

Prompt(option)
.then(answers=>{
console.log(answers)
})
.catch(error=>console.log({error}))