| // 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") |
| } |