-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathreader.go
353 lines (311 loc) · 8.58 KB
/
reader.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
// Portions Copyright 2011- The Go Authors. All rights reserved.
// Portions Copyright 2016- Jeremy Echols. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import (
"io"
"sync"
"unicode/utf8"
)
// DefaultMaxLineLength is the default MaxLineLength for a Reader; once a line
// reaches this length, the Reader no longer accepts input which would increase
// the line
const DefaultMaxLineLength = 4096
// KeyEvent is used for OnKeypress handlers to get the key and modify handler
// state when the custom handler needs default handlers to be bypassed
type KeyEvent struct {
Keypress
Line *Line
IgnoreDefaultHandlers bool
}
// Reader contains the state for running a VT100 terminal that is capable of
// reading lines of input. It is similar to the golang crypto/ssh/terminal
// package except that it doesn't write, leaving that to the caller. The idea
// is to store what the user is typing, and where the cursor should be, while
// letting something else decide what to draw and where on the screen to draw
// it. This separation enables more complex applications where there's other
// real-time data being rendered at the same time as the input line.
type Reader struct {
// OnKeypress, if non-null, is called for each keypress with the key and
// input line sent in
OnKeypress func(event *KeyEvent)
// AfterKeypress, if non-nil, is called after each keypress has been
// processed. event should be considered read-only, as any changes will be
// ignored since the key has already been processed.
AfterKeypress func(event *KeyEvent)
keyReader *KeyReader
m sync.RWMutex
// NoHistory is on when we don't want to preserve history, such as when a
// password is being entered
NoHistory bool
// MaxLineLength tells us when to stop accepting input (other than things
// like allowing up/down/left/right and other control keys)
MaxLineLength int
// CloseKey is the key which, when used on a terminal line by itself, closes
// the terminal. Defaults to CTRL + D.
CloseKey rune
// line is the current line being entered, and the cursor position
line *Line
// pasteActive is true iff there is a bracketed paste operation in
// progress.
pasteActive bool
// history contains previously entered commands so that they can be
// accessed with the up and down keys.
history stRingBuffer
// historyIndex stores the currently accessed history entry, where zero
// means the immediately previous entry.
historyIndex int
// When navigating up and down the history it's possible to return to
// the incomplete, initial line. That value is stored in
// historyPending.
historyPending string
}
// NewReader runs a terminal reader on the given io.Reader. If the Reader is a
// local terminal, that terminal must first have been put into raw mode.
func NewReader(r io.Reader) *Reader {
return &Reader{
keyReader: NewKeyReader(r),
MaxLineLength: DefaultMaxLineLength,
CloseKey: KeyCtrlD,
historyIndex: -1,
line: &Line{},
}
}
// handleKeypress processes the given keypress data and, optionally, returns a
// line of text that the user has entered.
func (r *Reader) handleKeypress(kp Keypress) (line string, ok bool) {
r.m.Lock()
defer r.m.Unlock()
var e = &KeyEvent{Keypress: kp, Line: r.line}
if r.OnKeypress != nil {
r.OnKeypress(e)
if e.IgnoreDefaultHandlers {
return
}
kp.Key = e.Key
}
line, ok = r.processKeypress(kp)
if r.AfterKeypress != nil {
r.AfterKeypress(e)
}
return
}
// processKeypress applies all non-overrideable logic needed for various
// keypresses to have their desired effects
func (r *Reader) processKeypress(kp Keypress) (output string, ok bool) {
var key = kp.Key
var line = r.line
if r.pasteActive && key != KeyEnter {
line.AddKeyToLine(key)
return
}
if kp.Modifier == ModAlt {
switch key {
case KeyLeft:
line.MoveToLeftWord()
case KeyRight:
line.MoveToRightWord()
}
}
if kp.Modifier != ModNone {
return
}
switch key {
case KeyBackspace, KeyCtrlH:
line.EraseNPreviousChars(1)
case KeyLeft:
line.MoveLeft()
case KeyRight:
line.MoveRight()
case KeyHome, KeyCtrlA:
line.MoveHome()
case KeyEnd, KeyCtrlE:
line.MoveEnd()
case KeyUp:
fetched := r.fetchPreviousHistory()
if !fetched {
return "", false
}
case KeyDown:
r.fetchNextHistory()
case KeyEnter:
output = line.String()
ok = true
line.Clear()
case KeyCtrlW:
line.EraseNPreviousChars(line.CountToLeftWord())
case KeyCtrlK:
line.DeleteLine()
case KeyCtrlD, KeyDelete:
line.DeleteRuneUnderCursor()
case KeyCtrlU:
line.DeleteToBeginningOfLine()
default:
if !isPrintable(key) {
return
}
if len(line.Text) == r.MaxLineLength {
return
}
line.AddKeyToLine(key)
}
return
}
// ReadPassword temporarily reads a password without saving to history
func (r *Reader) ReadPassword() (line string, err error) {
oldNoHistory := r.NoHistory
r.NoHistory = true
line, err = r.ReadLine()
r.NoHistory = oldNoHistory
return
}
// ReadLine returns a line of input from the terminal.
func (r *Reader) ReadLine() (line string, err error) {
lineIsPasted := r.pasteActive
for {
lineOk := false
for !lineOk {
var kp Keypress
kp, err = r.keyReader.ReadKeypress()
if err != nil {
return
}
key := kp.Key
if key == utf8.RuneError {
break
}
r.m.RLock()
lineLen := len(r.line.Text)
r.m.RUnlock()
if !r.pasteActive {
if key == r.CloseKey {
if lineLen == 0 {
return "", io.EOF
}
}
if key == KeyPasteStart {
r.pasteActive = true
if lineLen == 0 {
lineIsPasted = true
}
continue
}
} else if key == KeyPasteEnd {
r.pasteActive = false
continue
}
if !r.pasteActive {
lineIsPasted = false
}
line, lineOk = r.handleKeypress(kp)
}
if lineOk {
if !r.NoHistory {
r.historyIndex = -1
r.history.Add(line)
}
if lineIsPasted {
err = ErrPasteIndicator
}
return
}
}
}
// LinePos returns the current input line and cursor position
func (r *Reader) LinePos() (string, int) {
r.m.RLock()
defer r.m.RUnlock()
return r.line.String(), r.line.Pos
}
// Pos returns the position of the cursor
func (r *Reader) Pos() int {
r.m.RLock()
defer r.m.RUnlock()
return r.line.Pos
}
// fetchPreviousHistory sets the input line to the previous entry in our history
func (r *Reader) fetchPreviousHistory() bool {
// lock has to be held here
if r.NoHistory {
return false
}
entry, ok := r.history.NthPreviousEntry(r.historyIndex + 1)
if !ok {
return false
}
if r.historyIndex == -1 {
r.historyPending = string(r.line.Text)
}
r.historyIndex++
runes := []rune(entry)
r.line.Set(runes, len(runes))
return true
}
// fetchNextHistory sets the input line to the next entry in our history
func (r *Reader) fetchNextHistory() {
// lock has to be held here
if r.NoHistory {
return
}
switch r.historyIndex {
case -1:
return
case 0:
runes := []rune(r.historyPending)
r.line.Set(runes, len(runes))
r.historyIndex--
default:
entry, ok := r.history.NthPreviousEntry(r.historyIndex - 1)
if ok {
r.historyIndex--
runes := []rune(entry)
r.line.Set(runes, len(runes))
}
}
}
type pasteIndicatorError struct{}
func (pasteIndicatorError) Error() string {
return "terminal: ErrPasteIndicator not correctly handled"
}
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
// to valid line data. It indicates that bracketed paste mode is enabled and
// that the returned line consists only of pasted data. Programs may wish to
// interpret pasted data more literally than typed data.
var ErrPasteIndicator = pasteIndicatorError{}
// stRingBuffer is a ring buffer of strings.
type stRingBuffer struct {
// entries contains max elements.
entries []string
max int
// head contains the index of the element most recently added to the ring.
head int
// size contains the number of elements in the ring.
size int
}
func (s *stRingBuffer) Add(a string) {
if s.entries == nil {
const defaultNumEntries = 100
s.entries = make([]string, defaultNumEntries)
s.max = defaultNumEntries
}
s.head = (s.head + 1) % s.max
s.entries[s.head] = a
if s.size < s.max {
s.size++
}
}
// NthPreviousEntry returns the value passed to the nth previous call to Add.
// If n is zero then the immediately prior value is returned, if one, then the
// next most recent, and so on. If such an element doesn't exist then ok is
// false.
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
if n >= s.size {
return "", false
}
index := s.head - n
if index < 0 {
index += s.max
}
return s.entries[index], true
}