|
|
// Copyright 2011 The Go Authors. 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 ( "bytes" "io" "sync" "unicode/utf8" )
// EscapeCodes contains escape sequences that can be written to the terminal in
// order to achieve different styles of text.
type EscapeCodes struct { // Foreground colors
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte
// Reset all attributes
Reset []byte }
var vt100EscapeCodes = EscapeCodes{ Black: []byte{keyEscape, '[', '3', '0', 'm'}, Red: []byte{keyEscape, '[', '3', '1', 'm'}, Green: []byte{keyEscape, '[', '3', '2', 'm'}, Yellow: []byte{keyEscape, '[', '3', '3', 'm'}, Blue: []byte{keyEscape, '[', '3', '4', 'm'}, Magenta: []byte{keyEscape, '[', '3', '5', 'm'}, Cyan: []byte{keyEscape, '[', '3', '6', 'm'}, White: []byte{keyEscape, '[', '3', '7', 'm'},
Reset: []byte{keyEscape, '[', '0', 'm'}, }
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type Terminal struct { // AutoCompleteCallback, if non-null, is called for each keypress with
// the full input line and the current position of the cursor (in
// bytes, as an index into |line|). If it returns ok=false, the key
// press is processed normally. Otherwise it returns a replacement line
// and the new cursor position.
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
// Escape contains a pointer to the escape codes for this terminal.
// It's always a valid pointer, although the escape codes themselves
// may be empty if the terminal doesn't support them.
Escape *EscapeCodes
// lock protects the terminal and the state in this object from
// concurrent processing of a key press and a Write() call.
lock sync.Mutex
c io.ReadWriter prompt []rune
// line is the current line being entered.
line []rune // pos is the logical position of the cursor in line
pos int // echo is true if local echo is enabled
echo bool // pasteActive is true iff there is a bracketed paste operation in
// progress.
pasteActive bool
// cursorX contains the current X value of the cursor where the left
// edge is 0. cursorY contains the row number where the first row of
// the current line is 0.
cursorX, cursorY int // maxLine is the greatest value of cursorY so far.
maxLine int
termWidth, termHeight int
// outBuf contains the terminal data to be sent.
outBuf []byte // remainder contains the remainder of any partial key sequences after
// a read. It aliases into inBuf.
remainder []byte inBuf [256]byte
// 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 }
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
// a local terminal, that terminal must first have been put into raw mode.
// prompt is a string that is written at the start of each input line (i.e.
// "> ").
func NewTerminal(c io.ReadWriter, prompt string) *Terminal { return &Terminal{ Escape: &vt100EscapeCodes, c: c, prompt: []rune(prompt), termWidth: 80, termHeight: 24, echo: true, historyIndex: -1, } }
const ( keyCtrlD = 4 keyCtrlU = 21 keyEnter = '\r' keyEscape = 27 keyBackspace = 127 keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota keyUp keyDown keyLeft keyRight keyAltLeft keyAltRight keyHome keyEnd keyDeleteWord keyDeleteLine keyClearScreen keyPasteStart keyPasteEnd )
var ( crlf = []byte{'\r', '\n'} pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} )
// bytesToKey tries to parse a key sequence from b. If successful, it returns
// the key and the remainder of the input. Otherwise it returns utf8.RuneError.
func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { if len(b) == 0 { return utf8.RuneError, nil }
if !pasteActive { switch b[0] { case 1: // ^A
return keyHome, b[1:] case 5: // ^E
return keyEnd, b[1:] case 8: // ^H
return keyBackspace, b[1:] case 11: // ^K
return keyDeleteLine, b[1:] case 12: // ^L
return keyClearScreen, b[1:] case 23: // ^W
return keyDeleteWord, b[1:] } }
if b[0] != keyEscape { if !utf8.FullRune(b) { return utf8.RuneError, b } r, l := utf8.DecodeRune(b) return r, b[l:] }
if !pasteActive && len(b) >= 3 && b[0] == keyEscape && b[1] == '[' { switch b[2] { case 'A': return keyUp, b[3:] case 'B': return keyDown, b[3:] case 'C': return keyRight, b[3:] case 'D': return keyLeft, b[3:] case 'H': return keyHome, b[3:] case 'F': return keyEnd, b[3:] } }
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { switch b[5] { case 'C': return keyAltRight, b[6:] case 'D': return keyAltLeft, b[6:] } }
if !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { return keyPasteStart, b[6:] }
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { return keyPasteEnd, b[6:] }
// If we get here then we have a key that we don't recognise, or a
// partial sequence. It's not clear how one should find the end of a
// sequence without knowing them all, but it seems that [a-zA-Z~] only
// appears at the end of a sequence.
for i, c := range b[0:] { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { return keyUnknown, b[i+1:] } }
return utf8.RuneError, b }
// queue appends data to the end of t.outBuf
func (t *Terminal) queue(data []rune) { t.outBuf = append(t.outBuf, []byte(string(data))...) }
var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} var space = []rune{' '}
func isPrintable(key rune) bool { isInSurrogateArea := key >= 0xd800 && key <= 0xdbff return key >= 32 && !isInSurrogateArea }
// moveCursorToPos appends data to t.outBuf which will move the cursor to the
// given, logical position in the text.
func (t *Terminal) moveCursorToPos(pos int) { if !t.echo { return }
x := visualLength(t.prompt) + pos y := x / t.termWidth x = x % t.termWidth
up := 0 if y < t.cursorY { up = t.cursorY - y }
down := 0 if y > t.cursorY { down = y - t.cursorY }
left := 0 if x < t.cursorX { left = t.cursorX - x }
right := 0 if x > t.cursorX { right = x - t.cursorX }
t.cursorX = x t.cursorY = y t.move(up, down, left, right) }
func (t *Terminal) move(up, down, left, right int) { movement := make([]rune, 3*(up+down+left+right)) m := movement for i := 0; i < up; i++ { m[0] = keyEscape m[1] = '[' m[2] = 'A' m = m[3:] } for i := 0; i < down; i++ { m[0] = keyEscape m[1] = '[' m[2] = 'B' m = m[3:] } for i := 0; i < left; i++ { m[0] = keyEscape m[1] = '[' m[2] = 'D' m = m[3:] } for i := 0; i < right; i++ { m[0] = keyEscape m[1] = '[' m[2] = 'C' m = m[3:] }
t.queue(movement) }
func (t *Terminal) clearLineToRight() { op := []rune{keyEscape, '[', 'K'} t.queue(op) }
const maxLineLength = 4096
func (t *Terminal) setLine(newLine []rune, newPos int) { if t.echo { t.moveCursorToPos(0) t.writeLine(newLine) for i := len(newLine); i < len(t.line); i++ { t.writeLine(space) } t.moveCursorToPos(newPos) } t.line = newLine t.pos = newPos }
func (t *Terminal) advanceCursor(places int) { t.cursorX += places t.cursorY += t.cursorX / t.termWidth if t.cursorY > t.maxLine { t.maxLine = t.cursorY } t.cursorX = t.cursorX % t.termWidth
if places > 0 && t.cursorX == 0 { // Normally terminals will advance the current position
// when writing a character. But that doesn't happen
// for the last character in a line. However, when
// writing a character (except a new line) that causes
// a line wrap, the position will be advanced two
// places.
//
// So, if we are stopping at the end of a line, we
// need to write a newline so that our cursor can be
// advanced to the next line.
t.outBuf = append(t.outBuf, '\r', '\n') } }
func (t *Terminal) eraseNPreviousChars(n int) { if n == 0 { return }
if t.pos < n { n = t.pos } t.pos -= n t.moveCursorToPos(t.pos)
copy(t.line[t.pos:], t.line[n+t.pos:]) t.line = t.line[:len(t.line)-n] if t.echo { t.writeLine(t.line[t.pos:]) for i := 0; i < n; i++ { t.queue(space) } t.advanceCursor(n) t.moveCursorToPos(t.pos) } }
// countToLeftWord returns then number of characters from the cursor to the
// start of the previous word.
func (t *Terminal) countToLeftWord() int { if t.pos == 0 { return 0 }
pos := t.pos - 1 for pos > 0 { if t.line[pos] != ' ' { break } pos-- } for pos > 0 { if t.line[pos] == ' ' { pos++ break } pos-- }
return t.pos - pos }
// countToRightWord returns then number of characters from the cursor to the
// start of the next word.
func (t *Terminal) countToRightWord() int { pos := t.pos for pos < len(t.line) { if t.line[pos] == ' ' { break } pos++ } for pos < len(t.line) { if t.line[pos] != ' ' { break } pos++ } return pos - t.pos }
// visualLength returns the number of visible glyphs in s.
func visualLength(runes []rune) int { inEscapeSeq := false length := 0
for _, r := range runes { switch { case inEscapeSeq: if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { inEscapeSeq = false } case r == '\x1b': inEscapeSeq = true default: length++ } }
return length }
// handleKey processes the given key and, optionally, returns a line of text
// that the user has entered.
func (t *Terminal) handleKey(key rune) (line string, ok bool) { if t.pasteActive && key != keyEnter { t.addKeyToLine(key) return }
switch key { case keyBackspace: if t.pos == 0 { return } t.eraseNPreviousChars(1) case keyAltLeft: // move left by a word.
t.pos -= t.countToLeftWord() t.moveCursorToPos(t.pos) case keyAltRight: // move right by a word.
t.pos += t.countToRightWord() t.moveCursorToPos(t.pos) case keyLeft: if t.pos == 0 { return } t.pos-- t.moveCursorToPos(t.pos) case keyRight: if t.pos == len(t.line) { return } t.pos++ t.moveCursorToPos(t.pos) case keyHome: if t.pos == 0 { return } t.pos = 0 t.moveCursorToPos(t.pos) case keyEnd: if t.pos == len(t.line) { return } t.pos = len(t.line) t.moveCursorToPos(t.pos) case keyUp: entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) if !ok { return "", false } if t.historyIndex == -1 { t.historyPending = string(t.line) } t.historyIndex++ runes := []rune(entry) t.setLine(runes, len(runes)) case keyDown: switch t.historyIndex { case -1: return case 0: runes := []rune(t.historyPending) t.setLine(runes, len(runes)) t.historyIndex-- default: entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) if ok { t.historyIndex-- runes := []rune(entry) t.setLine(runes, len(runes)) } } case keyEnter: t.moveCursorToPos(len(t.line)) t.queue([]rune("\r\n")) line = string(t.line) ok = true t.line = t.line[:0] t.pos = 0 t.cursorX = 0 t.cursorY = 0 t.maxLine = 0 case keyDeleteWord: // Delete zero or more spaces and then one or more characters.
t.eraseNPreviousChars(t.countToLeftWord()) case keyDeleteLine: // Delete everything from the current cursor position to the
// end of line.
for i := t.pos; i < len(t.line); i++ { t.queue(space) t.advanceCursor(1) } t.line = t.line[:t.pos] t.moveCursorToPos(t.pos) case keyCtrlD: // Erase the character under the current position.
// The EOF case when the line is empty is handled in
// readLine().
if t.pos < len(t.line) { t.pos++ t.eraseNPreviousChars(1) } case keyCtrlU: t.eraseNPreviousChars(t.pos) case keyClearScreen: // Erases the screen and moves the cursor to the home position.
t.queue([]rune("\x1b[2J\x1b[H")) t.queue(t.prompt) t.cursorX, t.cursorY = 0, 0 t.advanceCursor(visualLength(t.prompt)) t.setLine(t.line, t.pos) default: if t.AutoCompleteCallback != nil { prefix := string(t.line[:t.pos]) suffix := string(t.line[t.pos:])
t.lock.Unlock() newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) t.lock.Lock()
if completeOk { t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) return } } if !isPrintable(key) { return } if len(t.line) == maxLineLength { return } t.addKeyToLine(key) } return }
// addKeyToLine inserts the given key at the current position in the current
// line.
func (t *Terminal) addKeyToLine(key rune) { if len(t.line) == cap(t.line) { newLine := make([]rune, len(t.line), 2*(1+len(t.line))) copy(newLine, t.line) t.line = newLine } t.line = t.line[:len(t.line)+1] copy(t.line[t.pos+1:], t.line[t.pos:]) t.line[t.pos] = key if t.echo { t.writeLine(t.line[t.pos:]) } t.pos++ t.moveCursorToPos(t.pos) }
func (t *Terminal) writeLine(line []rune) { for len(line) != 0 { remainingOnLine := t.termWidth - t.cursorX todo := len(line) if todo > remainingOnLine { todo = remainingOnLine } t.queue(line[:todo]) t.advanceCursor(visualLength(line[:todo])) line = line[todo:] } }
// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n.
func writeWithCRLF(w io.Writer, buf []byte) (n int, err error) { for len(buf) > 0 { i := bytes.IndexByte(buf, '\n') todo := len(buf) if i >= 0 { todo = i }
var nn int nn, err = w.Write(buf[:todo]) n += nn if err != nil { return n, err } buf = buf[todo:]
if i >= 0 { if _, err = w.Write(crlf); err != nil { return n, err } n++ buf = buf[1:] } }
return n, nil }
func (t *Terminal) Write(buf []byte) (n int, err error) { t.lock.Lock() defer t.lock.Unlock()
if t.cursorX == 0 && t.cursorY == 0 { // This is the easy case: there's nothing on the screen that we
// have to move out of the way.
return writeWithCRLF(t.c, buf) }
// We have a prompt and possibly user input on the screen. We
// have to clear it first.
t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) t.cursorX = 0 t.clearLineToRight()
for t.cursorY > 0 { t.move(1 /* up */, 0, 0, 0) t.cursorY-- t.clearLineToRight() }
if _, err = t.c.Write(t.outBuf); err != nil { return } t.outBuf = t.outBuf[:0]
if n, err = writeWithCRLF(t.c, buf); err != nil { return }
t.writeLine(t.prompt) if t.echo { t.writeLine(t.line) }
t.moveCursorToPos(t.pos)
if _, err = t.c.Write(t.outBuf); err != nil { return } t.outBuf = t.outBuf[:0] return }
// ReadPassword temporarily changes the prompt and reads a password, without
// echo, from the terminal.
func (t *Terminal) ReadPassword(prompt string) (line string, err error) { t.lock.Lock() defer t.lock.Unlock()
oldPrompt := t.prompt t.prompt = []rune(prompt) t.echo = false
line, err = t.readLine()
t.prompt = oldPrompt t.echo = true
return }
// ReadLine returns a line of input from the terminal.
func (t *Terminal) ReadLine() (line string, err error) { t.lock.Lock() defer t.lock.Unlock()
return t.readLine() }
func (t *Terminal) readLine() (line string, err error) { // t.lock must be held at this point
if t.cursorX == 0 && t.cursorY == 0 { t.writeLine(t.prompt) t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] }
lineIsPasted := t.pasteActive
for { rest := t.remainder lineOk := false for !lineOk { var key rune key, rest = bytesToKey(rest, t.pasteActive) if key == utf8.RuneError { break } if !t.pasteActive { if key == keyCtrlD { if len(t.line) == 0 { return "", io.EOF } } if key == keyPasteStart { t.pasteActive = true if len(t.line) == 0 { lineIsPasted = true } continue } } else if key == keyPasteEnd { t.pasteActive = false continue } if !t.pasteActive { lineIsPasted = false } line, lineOk = t.handleKey(key) } if len(rest) > 0 { n := copy(t.inBuf[:], rest) t.remainder = t.inBuf[:n] } else { t.remainder = nil } t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] if lineOk { if t.echo { t.historyIndex = -1 t.history.Add(line) } if lineIsPasted { err = ErrPasteIndicator } return }
// t.remainder is a slice at the beginning of t.inBuf
// containing a partial key sequence
readBuf := t.inBuf[len(t.remainder):] var n int
t.lock.Unlock() n, err = t.c.Read(readBuf) t.lock.Lock()
if err != nil { return }
t.remainder = t.inBuf[:n+len(t.remainder)] } }
// SetPrompt sets the prompt to be used when reading subsequent lines.
func (t *Terminal) SetPrompt(prompt string) { t.lock.Lock() defer t.lock.Unlock()
t.prompt = []rune(prompt) }
func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) { // Move cursor to column zero at the start of the line.
t.move(t.cursorY, 0, t.cursorX, 0) t.cursorX, t.cursorY = 0, 0 t.clearLineToRight() for t.cursorY < numPrevLines { // Move down a line
t.move(0, 1, 0, 0) t.cursorY++ t.clearLineToRight() } // Move back to beginning.
t.move(t.cursorY, 0, 0, 0) t.cursorX, t.cursorY = 0, 0
t.queue(t.prompt) t.advanceCursor(visualLength(t.prompt)) t.writeLine(t.line) t.moveCursorToPos(t.pos) }
func (t *Terminal) SetSize(width, height int) error { t.lock.Lock() defer t.lock.Unlock()
if width == 0 { width = 1 }
oldWidth := t.termWidth t.termWidth, t.termHeight = width, height
switch { case width == oldWidth: // If the width didn't change then nothing else needs to be
// done.
return nil case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: // If there is nothing on current line and no prompt printed,
// just do nothing
return nil case width < oldWidth: // Some terminals (e.g. xterm) will truncate lines that were
// too long when shinking. Others, (e.g. gnome-terminal) will
// attempt to wrap them. For the former, repainting t.maxLine
// works great, but that behaviour goes badly wrong in the case
// of the latter because they have doubled every full line.
// We assume that we are working on a terminal that wraps lines
// and adjust the cursor position based on every previous line
// wrapping and turning into two. This causes the prompt on
// xterms to move upwards, which isn't great, but it avoids a
// huge mess with gnome-terminal.
if t.cursorX >= t.termWidth { t.cursorX = t.termWidth - 1 } t.cursorY *= 2 t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2) case width > oldWidth: // If the terminal expands then our position calculations will
// be wrong in the future because we think the cursor is
// |t.pos| chars into the string, but there will be a gap at
// the end of any wrapped line.
//
// But the position will actually be correct until we move, so
// we can move back to the beginning and repaint everything.
t.clearAndRepaintLinePlusNPrevious(t.maxLine) }
_, err := t.c.Write(t.outBuf) t.outBuf = t.outBuf[:0] return err }
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{}
// SetBracketedPasteMode requests that the terminal bracket paste operations
// with markers. Not all terminals support this but, if it is supported, then
// enabling this mode will stop any autocomplete callback from running due to
// pastes. Additionally, any lines that are completely pasted will be returned
// from ReadLine with the error set to ErrPasteIndicator.
func (t *Terminal) SetBracketedPasteMode(on bool) { if on { io.WriteString(t.c, "\x1b[?2004h") } else { io.WriteString(t.c, "\x1b[?2004l") } }
// 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 }
// readPasswordLine reads from reader until it finds \n or io.EOF.
// The slice returned does not include the \n.
// readPasswordLine also ignores any \r it finds.
func readPasswordLine(reader io.Reader) ([]byte, error) { var buf [1]byte var ret []byte
for { n, err := reader.Read(buf[:]) if n > 0 { switch buf[0] { case '\n': return ret, nil case '\r': // remove \r from passwords on Windows
default: ret = append(ret, buf[0]) } continue } if err != nil { if err == io.EOF && len(ret) > 0 { return ret, nil } return ret, err } } }
|