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