1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
12 // EscapeCodes contains escape sequences that can be written to the terminal in
13 // order to achieve different styles of text.
14 type EscapeCodes
struct {
16 Black
, Red
, Green
, Yellow
, Blue
, Magenta
, Cyan
, White
[]byte
18 // Reset all attributes
22 var vt100EscapeCodes
= EscapeCodes
{
23 Black
: []byte{keyEscape
, '[', '3', '0', 'm'},
24 Red
: []byte{keyEscape
, '[', '3', '1', 'm'},
25 Green
: []byte{keyEscape
, '[', '3', '2', 'm'},
26 Yellow
: []byte{keyEscape
, '[', '3', '3', 'm'},
27 Blue
: []byte{keyEscape
, '[', '3', '4', 'm'},
28 Magenta
: []byte{keyEscape
, '[', '3', '5', 'm'},
29 Cyan
: []byte{keyEscape
, '[', '3', '6', 'm'},
30 White
: []byte{keyEscape
, '[', '3', '7', 'm'},
32 Reset
: []byte{keyEscape
, '[', '0', 'm'},
35 // Terminal contains the state for running a VT100 terminal that is capable of
36 // reading lines of input.
37 type Terminal
struct {
38 // AutoCompleteCallback, if non-null, is called for each keypress
39 // with the full input line and the current position of the cursor.
40 // If it returns a nil newLine, the key press is processed normally.
41 // Otherwise it returns a replacement line and the new cursor position.
42 AutoCompleteCallback
func(line
[]byte, pos
, key
int) (newLine
[]byte, newPos
int)
44 // Escape contains a pointer to the escape codes for this terminal.
45 // It's always a valid pointer, although the escape codes themselves
46 // may be empty if the terminal doesn't support them.
49 // lock protects the terminal and the state in this object from
50 // concurrent processing of a key press and a Write() call.
56 // line is the current line being entered.
58 // pos is the logical position of the cursor in line
60 // echo is true if local echo is enabled
63 // cursorX contains the current X value of the cursor where the left
64 // edge is 0. cursorY contains the row number where the first row of
65 // the current line is 0.
67 // maxLine is the greatest value of cursorY so far.
70 termWidth
, termHeight
int
72 // outBuf contains the terminal data to be sent.
74 // remainder contains the remainder of any partial key sequences after
75 // a read. It aliases into inBuf.
80 // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
81 // a local terminal, that terminal must first have been put into raw mode.
82 // prompt is a string that is written at the start of each input line (i.e.
84 func NewTerminal(c io
.ReadWriter
, prompt
string) *Terminal
{
86 Escape
: &vt100EscapeCodes
,
100 keyUnknown
= 256 + iota
109 // bytesToKey tries to parse a key sequence from b. If successful, it returns
110 // the key and the remainder of the input. Otherwise it returns -1.
111 func bytesToKey(b
[]byte) (int, []byte) {
116 if b
[0] != keyEscape
{
117 return int(b
[0]), b
[1:]
120 if len(b
) >= 3 && b
[0] == keyEscape
&& b
[1] == '[' {
125 return keyDown
, b
[3:]
127 return keyRight
, b
[3:]
129 return keyLeft
, b
[3:]
133 if len(b
) >= 6 && b
[0] == keyEscape
&& b
[1] == '[' && b
[2] == '1' && b
[3] == ';' && b
[4] == '3' {
136 return keyAltRight
, b
[6:]
138 return keyAltLeft
, b
[6:]
142 // If we get here then we have a key that we don't recognise, or a
143 // partial sequence. It's not clear how one should find the end of a
144 // sequence without knowing them all, but it seems that [a-zA-Z] only
145 // appears at the end of a sequence.
146 for i
, c
:= range b
[0:] {
147 if c
>= 'a' && c
<= 'z' || c
>= 'A' && c
<= 'Z' {
148 return keyUnknown
, b
[i
+1:]
155 // queue appends data to the end of t.outBuf
156 func (t
*Terminal
) queue(data
[]byte) {
157 t
.outBuf
= append(t
.outBuf
, data
...)
160 var eraseUnderCursor
= []byte{' ', keyEscape
, '[', 'D'}
161 var space
= []byte{' '}
163 func isPrintable(key
int) bool {
164 return key
>= 32 && key
< 127
167 // moveCursorToPos appends data to t.outBuf which will move the cursor to the
168 // given, logical position in the text.
169 func (t
*Terminal
) moveCursorToPos(pos
int) {
174 x
:= len(t
.prompt
) + pos
195 right
= x
- t
.cursorX
200 t
.move(up
, down
, left
, right
)
203 func (t
*Terminal
) move(up
, down
, left
, right
int) {
204 movement
:= make([]byte, 3*(up
+down
+left
+right
))
206 for i
:= 0; i
< up
; i
++ {
212 for i
:= 0; i
< down
; i
++ {
218 for i
:= 0; i
< left
; i
++ {
224 for i
:= 0; i
< right
; i
++ {
234 func (t
*Terminal
) clearLineToRight() {
235 op
:= []byte{keyEscape
, '[', 'K'}
239 const maxLineLength
= 4096
241 // handleKey processes the given key and, optionally, returns a line of text
242 // that the user has entered.
243 func (t
*Terminal
) handleKey(key
int) (line
string, ok
bool) {
250 t
.moveCursorToPos(t
.pos
)
252 copy(t
.line
[t
.pos
:], t
.line
[1+t
.pos
:])
253 t
.line
= t
.line
[:len(t
.line
)-1]
255 t
.writeLine(t
.line
[t
.pos
:])
257 t
.queue(eraseUnderCursor
)
258 t
.moveCursorToPos(t
.pos
)
260 // move left by a word.
266 if t
.line
[t
.pos
] != ' ' {
272 if t
.line
[t
.pos
] == ' ' {
278 t
.moveCursorToPos(t
.pos
)
280 // move right by a word.
281 for t
.pos
< len(t
.line
) {
282 if t
.line
[t
.pos
] == ' ' {
287 for t
.pos
< len(t
.line
) {
288 if t
.line
[t
.pos
] != ' ' {
293 t
.moveCursorToPos(t
.pos
)
299 t
.moveCursorToPos(t
.pos
)
301 if t
.pos
== len(t
.line
) {
305 t
.moveCursorToPos(t
.pos
)
307 t
.moveCursorToPos(len(t
.line
))
308 t
.queue([]byte("\r\n"))
309 line
= string(t
.line
)
317 if t
.AutoCompleteCallback
!= nil {
319 newLine
, newPos
:= t
.AutoCompleteCallback(t
.line
, t
.pos
, key
)
326 for i
:= len(newLine
); i
< len(t
.line
); i
++ {
329 t
.moveCursorToPos(newPos
)
336 if !isPrintable(key
) {
339 if len(t
.line
) == maxLineLength
{
342 if len(t
.line
) == cap(t
.line
) {
343 newLine
:= make([]byte, len(t
.line
), 2*(1+len(t
.line
)))
344 copy(newLine
, t
.line
)
347 t
.line
= t
.line
[:len(t
.line
)+1]
348 copy(t
.line
[t
.pos
+1:], t
.line
[t
.pos
:])
349 t
.line
[t
.pos
] = byte(key
)
351 t
.writeLine(t
.line
[t
.pos
:])
354 t
.moveCursorToPos(t
.pos
)
359 func (t
*Terminal
) writeLine(line
[]byte) {
361 remainingOnLine
:= t
.termWidth
- t
.cursorX
363 if todo
> remainingOnLine
{
364 todo
= remainingOnLine
370 if t
.cursorX
== t
.termWidth
{
373 if t
.cursorY
> t
.maxLine
{
374 t
.maxLine
= t
.cursorY
380 func (t
*Terminal
) Write(buf
[]byte) (n
int, err error
) {
382 defer t
.lock
.Unlock()
384 if t
.cursorX
== 0 && t
.cursorY
== 0 {
385 // This is the easy case: there's nothing on the screen that we
386 // have to move out of the way.
387 return t
.c
.Write(buf
)
390 // We have a prompt and possibly user input on the screen. We
391 // have to clear it first.
392 t
.move(0 /* up */, 0 /* down */, t
.cursorX
/* left */, 0 /* right */)
397 t
.move(1 /* up */, 0, 0, 0)
402 if _
, err
= t
.c
.Write(t
.outBuf
); err
!= nil {
405 t
.outBuf
= t
.outBuf
[:0]
407 if n
, err
= t
.c
.Write(buf
); err
!= nil {
411 t
.queue([]byte(t
.prompt
))
412 chars
:= len(t
.prompt
)
417 t
.cursorX
= chars
% t
.termWidth
418 t
.cursorY
= chars
/ t
.termWidth
419 t
.moveCursorToPos(t
.pos
)
421 if _
, err
= t
.c
.Write(t
.outBuf
); err
!= nil {
424 t
.outBuf
= t
.outBuf
[:0]
428 // ReadPassword temporarily changes the prompt and reads a password, without
429 // echo, from the terminal.
430 func (t
*Terminal
) ReadPassword(prompt
string) (line
string, err error
) {
432 defer t
.lock
.Unlock()
434 oldPrompt
:= t
.prompt
438 line
, err
= t
.readLine()
446 // ReadLine returns a line of input from the terminal.
447 func (t
*Terminal
) ReadLine() (line
string, err error
) {
449 defer t
.lock
.Unlock()
454 func (t
*Terminal
) readLine() (line
string, err error
) {
455 // t.lock must be held at this point
457 if t
.cursorX
== 0 && t
.cursorY
== 0 {
458 t
.writeLine([]byte(t
.prompt
))
460 t
.outBuf
= t
.outBuf
[:0]
468 key
, rest
= bytesToKey(rest
)
475 line
, lineOk
= t
.handleKey(key
)
478 n
:= copy(t
.inBuf
[:], rest
)
479 t
.remainder
= t
.inBuf
[:n
]
484 t
.outBuf
= t
.outBuf
[:0]
489 // t.remainder is a slice at the beginning of t.inBuf
490 // containing a partial key sequence
491 readBuf
:= t
.inBuf
[len(t
.remainder
):]
495 n
, err
= t
.c
.Read(readBuf
)
502 t
.remainder
= t
.inBuf
[:n
+len(t
.remainder
)]
507 // SetPrompt sets the prompt to be used when reading subsequent lines.
508 func (t
*Terminal
) SetPrompt(prompt
string) {
510 defer t
.lock
.Unlock()
515 func (t
*Terminal
) SetSize(width
, height
int) {
517 defer t
.lock
.Unlock()
519 t
.termWidth
, t
.termHeight
= width
, height