-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtcommandline.go
570 lines (517 loc) · 14.5 KB
/
tcommandline.go
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
package startprompt
import (
"fmt"
"os"
"runtime/debug"
"sync"
"time"
"github.com/gdamore/tcell/v2"
"golang.design/x/clipboard"
"golang.org/x/term"
)
type inputStruct struct {
text string
err error
}
type outputStruct struct {
text string
flush bool
}
var defaultTCommandLineOption = &CommandLineOption{
Schema: defaultSchema,
Handler: newTBaseEventHandler(),
History: NewMemHistory(),
CodeFactory: newBaseCode,
PromptFactory: newBasePrompt,
OnAbort: AbortActionRetry,
OnExit: AbortActionReturnError,
AutoIndent: false,
EnableDebug: false,
}
type TCommandLine struct {
// 命令行当前使用的 Line 和 TRenderer 对象
line *Line
renderer *TRenderer
tscreen tcell.Screen
// 是否按下鼠标左键
mousePrimaryPressed bool
lastPrimaryEvent *tcell.EventMouse
lastDblclickEvent *tcell.EventMouse
// 点击间隔,用来判断鼠标双击、三击等
clickInterval time.Duration
// 配置选项
option *CommandLineOption
// 下面几个都用用于并发的情况
// 传递输入 channel
inputChannel chan *inputStruct
// 传递输出 channel
outputChannel chan outputStruct
// 传递重新渲染事件 channel
redrawChannel chan struct{}
// 传递关闭事件 channel
closeChannel chan struct{}
// 下面两个用于 tcell.Screen ChannelEvents
tEventChannel chan tcell.Event
tQuitChannel chan struct{}
// 缓冲读取的 EventKey
tEventKeyChannel chan *tcell.EventKey
// 是否正在读取用户输入
isReadingInput bool
running bool
// 下面几个对应用户的特殊操作:退出、丢弃、确定
exitFlag bool
abortFlag bool
acceptFlag bool
// wg 用来等待协程结束
wg sync.WaitGroup
}
// NewTCommandLine 新建命令行对象
func NewTCommandLine(option *CommandLineOption) (*TCommandLine, error) {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return nil, fmt.Errorf("not in a terminal")
}
if !term.IsTerminal(int(os.Stdout.Fd())) {
return nil, fmt.Errorf("not in a terminal")
}
// Init returns an error if the package is not ready for use.
err := clipboard.Init()
if err != nil {
return nil, err
}
// update option default
actualOption := defaultTCommandLineOption.copy()
if option != nil {
actualOption.update(option)
}
// Initialize screen
s, err := tcell.NewScreen()
if err != nil {
return nil, fmt.Errorf("failed to NewScreen: %w", err)
}
if err := s.Init(); err != nil {
return nil, fmt.Errorf("failed to Screen.Init: %w", err)
}
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
s.SetStyle(defStyle)
s.EnableMouse()
s.EnablePaste()
s.Clear()
s.SetCursorStyle(tcell.CursorStyleSteadyBar)
c := &TCommandLine{
tscreen: s,
option: actualOption,
inputChannel: make(chan *inputStruct),
redrawChannel: make(chan struct{}, 16),
closeChannel: make(chan struct{}),
outputChannel: make(chan outputStruct, 16),
tEventChannel: make(chan tcell.Event, 1024),
tQuitChannel: make(chan struct{}),
renderer: newTRenderer(s, actualOption.Schema, actualOption.PromptFactory),
}
c.setup()
DebugLog("start tcommandline")
return c, nil
}
func (tc *TCommandLine) setup() {
tc.reset()
tc.clickInterval = 200 * time.Millisecond
if tc.option.EnableDebug {
enableDebugLog()
} else {
disableDebugLog()
}
tc.wg.Add(1)
tc.running = true
// 根据 ChannelEvents 注释,需要单独开 goroutine 调用
go func() {
tc.tscreen.ChannelEvents(tc.tEventChannel, tc.tQuitChannel)
}()
// ReadInput 返回后,控制权转移到了调用者手里,此时我们处理不了事件
// 因为我们接管了鼠标操作,所以需要后台响应鼠标事件
// 干脆把整个事件的处理都放到后台
go tc.run()
}
func (tc *TCommandLine) reset() {
tc.exitFlag = false
tc.abortFlag = false
tc.acceptFlag = false
}
// Close 关闭命令行,恢复终端到原先的模式
func (tc *TCommandLine) Close() {
maybePanic := recover()
// 取消鼠标事件的上报(鼠标移动会上报大量事件,减少后面 discardTEvent 丢弃的事件)
tc.tscreen.DisableMouse()
tc.running = false
close(tc.closeChannel)
close(tc.redrawChannel)
close(tc.outputChannel)
tc.wg.Wait()
// 继续读取事件
// 因为 tcell 内部也是用 channel 传递的事件
// 如果有大量事件上报,又没有读取事件,tcell 内部就可能卡在 channel send 发送
// 从而导致调用 tcell.Screen.Fini 卡住(tcell 内部开了 goroutine 读取事件发送到 channel , Fini 会等待这些 goroutine 结束)
// 比如说开启鼠标支持,在 Close 前调用了 time.Sleep ,这个时候就会有大量的鼠标事件堆积
tc.discardTEvent()
tc.tscreen.Fini()
if maybePanic != nil {
DebugLog("panic: %v\n%s", maybePanic, string(debug.Stack()))
panic(maybePanic)
}
DebugLog("close tcommandline")
}
// RequestRedraw 请求重绘( goroutine 安全)
func (tc *TCommandLine) RequestRedraw() {
if tc.isReadingInput {
tc.redrawChannel <- struct{}{}
}
}
// RunInExecutor 运行后台任务
func (tc *TCommandLine) RunInExecutor(callback func()) {
go callback()
}
// ReadInput 读取当前输入
func (tc *TCommandLine) ReadInput() (string, error) {
if tc.isReadingInput {
return "", fmt.Errorf("already reading input")
}
tc.isReadingInput = true
DebugLog("reading input")
tc.outputChannel <- outputStruct{"", true}
in := <-tc.inputChannel
tc.isReadingInput = false
DebugLog("return input: <%s>, err: %v", in.text, in.err)
return in.text, in.err
}
// ReadRune 读取 rune ,不能与 ReadInput 同时调用
func (tc *TCommandLine) ReadRune() (rune, error) {
for event := range tc.tEventKeyChannel {
if event.Key() == tcell.KeyRune {
return event.Rune(), nil
}
}
return 0, nil
}
func (tc *TCommandLine) run() {
defer func() {
maybePanic := recover()
tc.wg.Done()
if maybePanic != nil {
DebugLog("panic: %v\n%s", maybePanic, string(debug.Stack()))
panic(maybePanic)
}
}()
for tc.running {
tc.tEventKeyChannel = make(chan *tcell.EventKey, 1024)
tc.runOther()
close(tc.tEventKeyChannel)
tc.runLoop()
}
tc.flushOutput()
DebugLog("run stopped")
}
// runLoop 在用户调用 ReadInput 时执行
// 对应用户输入文本阶段
func (tc *TCommandLine) runLoop() {
if !tc.running {
return
}
DebugLog("runLoop")
renderer := tc.renderer
line := newLine(
tc.option.CodeFactory,
tc.option.History,
tc.option.AutoIndent,
)
tc.line = line
renderer.render(line.GetRenderContext(), false, false)
resetFunc := func() {
line.reset()
renderer.reset()
tc.reset()
}
resetFunc()
for {
if len(tc.tEventKeyChannel) == 0 {
select {
case <-tc.closeChannel:
DebugLog("close")
return
case <-tc.redrawChannel:
DebugLog("redraw")
// 将缓冲的信息都读取出来,以免循环中不断触发
loop := len(tc.redrawChannel)
for i := 0; i < loop; i++ {
<-tc.redrawChannel
}
// 渲染用户输入
renderer.render(line.GetRenderContext(), false, false)
continue
case ev := <-tc.tEventChannel:
eventEmited := tc.emitEvent(ev)
// 非阻塞的读取后续事件,优化粘贴大量文本的情况,快速处理,减少多次 render 导致的停顿感
eventReading := true
for eventReading {
select {
case ev = <-tc.tEventChannel:
if tc.emitEvent(ev) {
eventEmited = true
}
default:
eventReading = false
}
}
// 没有触发事件,直接进入下一次循环,避免没必要的渲染
if !eventEmited {
continue
}
}
} else {
for eventKey := range tc.tEventKeyChannel {
if !tc.emitEvent(eventKey) {
continue
}
}
}
// 处理特别的输入事件结果
if tc.exitFlag {
DebugLog("handle exit flag, action: %s", tc.option.OnExit)
// 一般是用户按了 Ctrl-D
switch tc.option.OnExit {
case AbortActionReturnError:
renderer.render(line.GetRenderContext(), true, false)
tc.sendInput("", ExitError)
return
case AbortActionReturnNone:
renderer.render(line.GetRenderContext(), true, false)
tc.sendInput("", nil)
return
case AbortActionRetry:
resetFunc()
case AbortActionIgnore:
}
}
if tc.abortFlag {
DebugLog("handle abort flag, action: %s", tc.option.OnAbort)
// 一般是用户按了 Ctrl-C
switch tc.option.OnAbort {
case AbortActionReturnError:
renderer.render(line.GetRenderContext(), true, false)
tc.sendInput("", ExitError)
return
case AbortActionReturnNone:
renderer.render(line.GetRenderContext(), true, false)
tc.sendInput("", nil)
return
case AbortActionRetry:
resetFunc()
case AbortActionIgnore:
}
}
if tc.acceptFlag {
DebugLog("handle accept flag")
// 一般是用户按了 Enter
// 返回用户输入的文本内容
renderer.render(line.GetRenderContext(), false, true)
inputText := line.text()
tc.sendInput(inputText, nil)
break
}
renderer.update()
// 画出用户输入
renderer.render(line.GetRenderContext(), false, false)
}
}
// runOther 在用户调用 ReadInput 前和返回后执行
// 对应非用户输入阶段,因为我们接管了鼠标操作,所以还要继续处理鼠标事件
func (tc *TCommandLine) runOther() {
if !tc.running {
return
}
DebugLog("runOther")
renderer := tc.renderer
for {
select {
case <-tc.closeChannel:
DebugLog("close")
return
case <-tc.redrawChannel:
DebugLog("redraw")
// 将缓冲的信息都读取出来,以免循环中不断触发
loop := len(tc.redrawChannel)
for i := 0; i < loop; i++ {
<-tc.redrawChannel
}
renderer.Show()
continue
case ev := <-tc.tEventChannel:
switch tev := ev.(type) {
case *tcell.EventKey:
// 这里用 select 监听写入,是为了防止阻塞在这里,ref: https://stackoverflow.com/a/25657232
// 如果 tEventKeyChannel 满了,丢弃事件
select {
case tc.tEventKeyChannel <- tev:
default:
}
default:
// 没有触发事件,直接进入下一次循环,避免没必要的渲染
if !tc.emitEvent(ev) {
continue
}
}
case output := <-tc.outputChannel:
// 用户调用了 ReadInput
if output.flush {
return
}
renderer.renderOutput(output.text)
continue
}
renderer.update()
renderer.Show()
}
}
func (tc *TCommandLine) emitEvent(tevent tcell.Event) bool {
var event Event
switch ev := tevent.(type) {
case *tcell.EventResize:
tc.renderer.Resize()
case *tcell.EventKey:
eventType, found := tkeyMapping[ev.Key()]
if found {
var data []rune
if ev.Key() == tcell.KeyRune {
data = []rune{ev.Rune()}
}
event = NewEventKey(eventType, data, nil, tc)
} else {
DebugLog("unsupported tcell.EventKey: %+v", ev)
}
case *tcell.EventMouse:
x, y := ev.Position()
coor := Coordinate{x, y}
switch ev.Buttons() {
case tcell.ButtonPrimary:
if tc.mousePrimaryPressed {
event = NewEventMouse(EventTypeMouseMove, coor, nil, tc)
} else if tc.IsDblClick(ev) {
event = NewEventMouse(EventTypeMouseDblclick, coor, nil, tc)
tc.mousePrimaryPressed = false
tc.lastPrimaryEvent = nil
tc.lastDblclickEvent = ev
} else if tc.IsTripleClick(ev) {
event = NewEventMouse(EventTypeMouseTripleClick, coor, nil, tc)
tc.mousePrimaryPressed = false
tc.lastPrimaryEvent = nil
tc.lastDblclickEvent = nil
} else {
event = NewEventMouse(EventTypeMouseDown, coor, nil, tc)
tc.mousePrimaryPressed = true
tc.lastPrimaryEvent = ev
}
case tcell.ButtonNone:
if tc.mousePrimaryPressed {
event = NewEventMouse(EventTypeMouseUp, coor, nil, tc)
tc.mousePrimaryPressed = false
}
case tcell.WheelUp:
event = NewEventMouse(EventTypeMouseWheelUp, coor, nil, tc)
case tcell.WheelDown:
event = NewEventMouse(EventTypeMouseWheelDown, coor, nil, tc)
}
}
if event == nil {
return false
}
DebugLog("emit event=%s", event.Type())
tc.option.Handler.Handle(event)
return true
}
func (tc *TCommandLine) IsDblClick(ev *tcell.EventMouse) bool {
// clickInterval 内相同位置按下鼠标左键两次
if tc.lastPrimaryEvent == nil {
return false
}
lastX, lastY := tc.lastPrimaryEvent.Position()
x, y := ev.Position()
return lastX == x && lastY == y && tc.lastPrimaryEvent.When().Add(tc.clickInterval).After(time.Now())
}
func (tc *TCommandLine) IsTripleClick(ev *tcell.EventMouse) bool {
// clickInterval 内相同位置按下鼠标左键三次
if tc.lastDblclickEvent == nil {
return false
}
lastX, lastY := tc.lastDblclickEvent.Position()
x, y := ev.Position()
return lastX == x && lastY == y && tc.lastDblclickEvent.When().Add(tc.clickInterval).After(time.Now())
}
func (tc *TCommandLine) sendInput(text string, err error) {
in := &inputStruct{text: text, err: err}
tc.inputChannel <- in
}
// GetLine 获取当前的 Line 对象,如果为 nil ,则 panic
func (tc *TCommandLine) GetLine() *Line {
if tc.line == nil {
panic("not found Line from TCommandLine")
}
return tc.line
}
// GetRenderer 获取当前的 TRenderer 对象,如果为 nil ,则 panic
func (tc *TCommandLine) GetRenderer() *TRenderer {
if tc.renderer == nil {
panic("not found TRenderer from TCommandLine")
}
return tc.renderer
}
func (tc *TCommandLine) flushOutput() {
ch := tc.outputChannel
for output := range ch {
if output.flush {
break
}
tc.renderer.renderOutput(output.text)
}
}
func (tc *TCommandLine) discardTEvent() {
// 这是使用超时,而不是等待 channel 关闭
// 是因为我们要在 tcell.Screen.Fini 之前调用这个方法,丢弃掉 tcell 内部 channel 堆积的事件
// 调用 tcell.Screen.Fini 之后, tEventChannel 会被关闭,这样我们就无法读取到 tcell 内部 channel 堆积的事件
timer := time.NewTimer(100 * time.Millisecond)
for {
select {
case <-tc.tEventChannel:
case <-timer.C:
return
}
}
}
func (tc *TCommandLine) Write(p []byte) (int, error) {
tc.outputChannel <- outputStruct{string(p), false}
return len(p), nil
}
func (tc *TCommandLine) Print(a ...any) {
_, _ = fmt.Fprint(tc, a...)
}
func (tc *TCommandLine) Println(a ...any) {
_, _ = fmt.Fprintln(tc, a...)
}
func (tc *TCommandLine) Printf(format string, a ...any) {
_, _ = fmt.Fprintf(tc, format, a...)
}
func (tc *TCommandLine) SetOnAbort(action AbortAction) {
tc.option.OnAbort = action
}
func (tc *TCommandLine) SetOnExit(action AbortAction) {
tc.option.OnExit = action
}
func (tc *TCommandLine) SetExit() {
tc.exitFlag = true
}
func (tc *TCommandLine) SetAbort() {
tc.abortFlag = true
}
func (tc *TCommandLine) SetAccept() {
tc.acceptFlag = true
}
func (tc *TCommandLine) IsReadingInput() bool {
return tc.isReadingInput
}