Previously, terminal only supported ASCII characters. This change alters some []byte to []rune so that the full range of Unicode is supported. The only thing that doesn't appear to work correctly are grapheme clusters as the code still assumes one rune per glyph. Still, this change allows many more languages to work than did previously. R=golang-dev, rsc CC=golang-dev https://golang.org/cl/13704043
693 lines
15 KiB
Go
693 lines
15 KiB
Go
// 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 (
|
|
"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 string
|
|
|
|
// 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
|
|
|
|
// 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: prompt,
|
|
termWidth: 80,
|
|
termHeight: 24,
|
|
echo: true,
|
|
historyIndex: -1,
|
|
}
|
|
}
|
|
|
|
const (
|
|
keyCtrlD = 4
|
|
keyEnter = '\r'
|
|
keyEscape = 27
|
|
keyBackspace = 127
|
|
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
|
|
keyUp
|
|
keyDown
|
|
keyLeft
|
|
keyRight
|
|
keyAltLeft
|
|
keyAltRight
|
|
keyHome
|
|
keyEnd
|
|
keyDeleteWord
|
|
keyDeleteLine
|
|
)
|
|
|
|
// 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) (rune, []byte) {
|
|
if len(b) == 0 {
|
|
return utf8.RuneError, nil
|
|
}
|
|
|
|
switch b[0] {
|
|
case 8: // ^H
|
|
return keyBackspace, b[1:]
|
|
case 11: // ^K
|
|
return keyDeleteLine, 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 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:]
|
|
}
|
|
}
|
|
|
|
if len(b) >= 3 && b[0] == keyEscape && b[1] == 'O' {
|
|
switch b[2] {
|
|
case 'H':
|
|
return keyHome, b[3:]
|
|
case 'F':
|
|
return keyEnd, b[3:]
|
|
}
|
|
}
|
|
|
|
if 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 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' {
|
|
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 := len(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) 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.cursorX += 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
|
|
}
|
|
|
|
// 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) {
|
|
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++
|
|
t.setLine([]rune(entry), len(entry))
|
|
case keyDown:
|
|
switch t.historyIndex {
|
|
case -1:
|
|
return
|
|
case 0:
|
|
t.setLine([]rune(t.historyPending), len(t.historyPending))
|
|
t.historyIndex--
|
|
default:
|
|
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
|
|
if ok {
|
|
t.historyIndex--
|
|
t.setLine([]rune(entry), len(entry))
|
|
}
|
|
}
|
|
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.cursorX++
|
|
}
|
|
t.line = t.line[:t.pos]
|
|
t.moveCursorToPos(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
|
|
}
|
|
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)
|
|
}
|
|
return
|
|
}
|
|
|
|
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.cursorX += todo
|
|
line = line[todo:]
|
|
|
|
if t.cursorX == t.termWidth {
|
|
t.cursorX = 0
|
|
t.cursorY++
|
|
if t.cursorY > t.maxLine {
|
|
t.maxLine = t.cursorY
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 t.c.Write(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 = t.c.Write(buf); err != nil {
|
|
return
|
|
}
|
|
|
|
t.queue([]rune(t.prompt))
|
|
chars := len(t.prompt)
|
|
if t.echo {
|
|
t.queue(t.line)
|
|
chars += len(t.line)
|
|
}
|
|
t.cursorX = chars % t.termWidth
|
|
t.cursorY = chars / t.termWidth
|
|
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 = 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([]rune(t.prompt))
|
|
t.c.Write(t.outBuf)
|
|
t.outBuf = t.outBuf[:0]
|
|
}
|
|
|
|
for {
|
|
rest := t.remainder
|
|
lineOk := false
|
|
for !lineOk {
|
|
var key rune
|
|
key, rest = bytesToKey(rest)
|
|
if key == utf8.RuneError {
|
|
break
|
|
}
|
|
if key == keyCtrlD {
|
|
return "", io.EOF
|
|
}
|
|
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)
|
|
}
|
|
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)]
|
|
}
|
|
|
|
panic("unreachable") // for Go 1.0.
|
|
}
|
|
|
|
// 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 = prompt
|
|
}
|
|
|
|
func (t *Terminal) SetSize(width, height int) {
|
|
t.lock.Lock()
|
|
defer t.lock.Unlock()
|
|
|
|
t.termWidth, t.termHeight = width, height
|
|
}
|
|
|
|
// 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
|
|
}
|