summaryrefslogtreecommitdiff
path: root/libgo/go/exp/terminal/shell.go
diff options
context:
space:
mode:
Diffstat (limited to 'libgo/go/exp/terminal/shell.go')
-rw-r--r--libgo/go/exp/terminal/shell.go359
1 files changed, 359 insertions, 0 deletions
diff --git a/libgo/go/exp/terminal/shell.go b/libgo/go/exp/terminal/shell.go
new file mode 100644
index 00000000000..e3f584774e3
--- /dev/null
+++ b/libgo/go/exp/terminal/shell.go
@@ -0,0 +1,359 @@
+// 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 (
+ "os"
+ "io"
+)
+
+// Shell contains the state for running a VT100 terminal that is capable of
+// reading lines of input.
+type Shell struct {
+ c io.ReadWriter
+ prompt string
+
+ // line is the current line being entered.
+ line []byte
+ // pos is the logical position of the cursor in line
+ pos int
+
+ // 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
+}
+
+// NewShell 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 NewShell(c io.ReadWriter, prompt string) *Shell {
+ return &Shell{
+ c: c,
+ prompt: prompt,
+ termWidth: 80,
+ termHeight: 24,
+ }
+}
+
+const (
+ keyCtrlD = 4
+ keyEnter = '\r'
+ keyEscape = 27
+ keyBackspace = 127
+ keyUnknown = 256 + iota
+ keyUp
+ keyDown
+ keyLeft
+ keyRight
+ keyAltLeft
+ keyAltRight
+)
+
+// bytesToKey tries to parse a key sequence from b. If successful, it returns
+// the key and the remainder of the input. Otherwise it returns -1.
+func bytesToKey(b []byte) (int, []byte) {
+ if len(b) == 0 {
+ return -1, nil
+ }
+
+ if b[0] != keyEscape {
+ return int(b[0]), b[1:]
+ }
+
+ 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) >= 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 -1, b
+}
+
+// queue appends data to the end of ss.outBuf
+func (ss *Shell) queue(data []byte) {
+ if len(ss.outBuf)+len(data) > cap(ss.outBuf) {
+ newOutBuf := make([]byte, len(ss.outBuf), 2*(len(ss.outBuf)+len(data)))
+ copy(newOutBuf, ss.outBuf)
+ ss.outBuf = newOutBuf
+ }
+
+ oldLen := len(ss.outBuf)
+ ss.outBuf = ss.outBuf[:len(ss.outBuf)+len(data)]
+ copy(ss.outBuf[oldLen:], data)
+}
+
+var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'}
+
+func isPrintable(key int) bool {
+ return key >= 32 && key < 127
+}
+
+// moveCursorToPos appends data to ss.outBuf which will move the cursor to the
+// given, logical position in the text.
+func (ss *Shell) moveCursorToPos(pos int) {
+ x := len(ss.prompt) + pos
+ y := x / ss.termWidth
+ x = x % ss.termWidth
+
+ up := 0
+ if y < ss.cursorY {
+ up = ss.cursorY - y
+ }
+
+ down := 0
+ if y > ss.cursorY {
+ down = y - ss.cursorY
+ }
+
+ left := 0
+ if x < ss.cursorX {
+ left = ss.cursorX - x
+ }
+
+ right := 0
+ if x > ss.cursorX {
+ right = x - ss.cursorX
+ }
+
+ movement := make([]byte, 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:]
+ }
+
+ ss.cursorX = x
+ ss.cursorY = y
+ ss.queue(movement)
+}
+
+const maxLineLength = 4096
+
+// handleKey processes the given key and, optionally, returns a line of text
+// that the user has entered.
+func (ss *Shell) handleKey(key int) (line string, ok bool) {
+ switch key {
+ case keyBackspace:
+ if ss.pos == 0 {
+ return
+ }
+ ss.pos--
+
+ copy(ss.line[ss.pos:], ss.line[1+ss.pos:])
+ ss.line = ss.line[:len(ss.line)-1]
+ ss.writeLine(ss.line[ss.pos:])
+ ss.moveCursorToPos(ss.pos)
+ ss.queue(eraseUnderCursor)
+ case keyAltLeft:
+ // move left by a word.
+ if ss.pos == 0 {
+ return
+ }
+ ss.pos--
+ for ss.pos > 0 {
+ if ss.line[ss.pos] != ' ' {
+ break
+ }
+ ss.pos--
+ }
+ for ss.pos > 0 {
+ if ss.line[ss.pos] == ' ' {
+ ss.pos++
+ break
+ }
+ ss.pos--
+ }
+ ss.moveCursorToPos(ss.pos)
+ case keyAltRight:
+ // move right by a word.
+ for ss.pos < len(ss.line) {
+ if ss.line[ss.pos] == ' ' {
+ break
+ }
+ ss.pos++
+ }
+ for ss.pos < len(ss.line) {
+ if ss.line[ss.pos] != ' ' {
+ break
+ }
+ ss.pos++
+ }
+ ss.moveCursorToPos(ss.pos)
+ case keyLeft:
+ if ss.pos == 0 {
+ return
+ }
+ ss.pos--
+ ss.moveCursorToPos(ss.pos)
+ case keyRight:
+ if ss.pos == len(ss.line) {
+ return
+ }
+ ss.pos++
+ ss.moveCursorToPos(ss.pos)
+ case keyEnter:
+ ss.moveCursorToPos(len(ss.line))
+ ss.queue([]byte("\r\n"))
+ line = string(ss.line)
+ ok = true
+ ss.line = ss.line[:0]
+ ss.pos = 0
+ ss.cursorX = 0
+ ss.cursorY = 0
+ ss.maxLine = 0
+ default:
+ if !isPrintable(key) {
+ return
+ }
+ if len(ss.line) == maxLineLength {
+ return
+ }
+ if len(ss.line) == cap(ss.line) {
+ newLine := make([]byte, len(ss.line), 2*(1+len(ss.line)))
+ copy(newLine, ss.line)
+ ss.line = newLine
+ }
+ ss.line = ss.line[:len(ss.line)+1]
+ copy(ss.line[ss.pos+1:], ss.line[ss.pos:])
+ ss.line[ss.pos] = byte(key)
+ ss.writeLine(ss.line[ss.pos:])
+ ss.pos++
+ ss.moveCursorToPos(ss.pos)
+ }
+ return
+}
+
+func (ss *Shell) writeLine(line []byte) {
+ for len(line) != 0 {
+ if ss.cursorX == ss.termWidth {
+ ss.queue([]byte("\r\n"))
+ ss.cursorX = 0
+ ss.cursorY++
+ if ss.cursorY > ss.maxLine {
+ ss.maxLine = ss.cursorY
+ }
+ }
+
+ remainingOnLine := ss.termWidth - ss.cursorX
+ todo := len(line)
+ if todo > remainingOnLine {
+ todo = remainingOnLine
+ }
+ ss.queue(line[:todo])
+ ss.cursorX += todo
+ line = line[todo:]
+ }
+}
+
+func (ss *Shell) Write(buf []byte) (n int, err os.Error) {
+ return ss.c.Write(buf)
+}
+
+// ReadLine returns a line of input from the terminal.
+func (ss *Shell) ReadLine() (line string, err os.Error) {
+ ss.writeLine([]byte(ss.prompt))
+ ss.c.Write(ss.outBuf)
+ ss.outBuf = ss.outBuf[:0]
+
+ for {
+ // ss.remainder is a slice at the beginning of ss.inBuf
+ // containing a partial key sequence
+ readBuf := ss.inBuf[len(ss.remainder):]
+ var n int
+ n, err = ss.c.Read(readBuf)
+ if err != nil {
+ return
+ }
+
+ if err == nil {
+ ss.remainder = ss.inBuf[:n+len(ss.remainder)]
+ rest := ss.remainder
+ lineOk := false
+ for !lineOk {
+ var key int
+ key, rest = bytesToKey(rest)
+ if key < 0 {
+ break
+ }
+ if key == keyCtrlD {
+ return "", os.EOF
+ }
+ line, lineOk = ss.handleKey(key)
+ }
+ if len(rest) > 0 {
+ n := copy(ss.inBuf[:], rest)
+ ss.remainder = ss.inBuf[:n]
+ } else {
+ ss.remainder = nil
+ }
+ ss.c.Write(ss.outBuf)
+ ss.outBuf = ss.outBuf[:0]
+ if lineOk {
+ return
+ }
+ continue
+ }
+ }
+ panic("unreachable")
+}