mirror of
https://github.com/golang/go
synced 2024-11-20 03:04:40 -07:00
exp/terminal: rename shell to terminal and add SetSize
It was never really a shell, but the name carried over from SSH's ServerShell. Two small functional changes: Add SetSize, which allows the size of the terminal to be changed in response, say, to an SSH message. Don't write the prompt if there's already something on the current line. R=rsc CC=golang-dev https://golang.org/cl/5376066
This commit is contained in:
parent
791aef1821
commit
252ef18d04
@ -6,7 +6,7 @@ include ../../../Make.inc
|
|||||||
|
|
||||||
TARG=exp/terminal
|
TARG=exp/terminal
|
||||||
GOFILES=\
|
GOFILES=\
|
||||||
shell.go\
|
terminal.go\
|
||||||
|
|
||||||
ifneq ($(GOOS),windows)
|
ifneq ($(GOOS),windows)
|
||||||
GOFILES+=util.go
|
GOFILES+=util.go
|
||||||
|
@ -6,9 +6,9 @@ package terminal
|
|||||||
|
|
||||||
import "io"
|
import "io"
|
||||||
|
|
||||||
// Shell contains the state for running a VT100 terminal that is capable of
|
// Terminal contains the state for running a VT100 terminal that is capable of
|
||||||
// reading lines of input.
|
// reading lines of input.
|
||||||
type Shell struct {
|
type Terminal struct {
|
||||||
c io.ReadWriter
|
c io.ReadWriter
|
||||||
prompt string
|
prompt string
|
||||||
|
|
||||||
@ -34,12 +34,12 @@ type Shell struct {
|
|||||||
inBuf [256]byte
|
inBuf [256]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShell runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
|
// 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.
|
// 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.
|
// prompt is a string that is written at the start of each input line (i.e.
|
||||||
// "> ").
|
// "> ").
|
||||||
func NewShell(c io.ReadWriter, prompt string) *Shell {
|
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
|
||||||
return &Shell{
|
return &Terminal{
|
||||||
c: c,
|
c: c,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
termWidth: 80,
|
termWidth: 80,
|
||||||
@ -107,17 +107,17 @@ func bytesToKey(b []byte) (int, []byte) {
|
|||||||
return -1, b
|
return -1, b
|
||||||
}
|
}
|
||||||
|
|
||||||
// queue appends data to the end of ss.outBuf
|
// queue appends data to the end of t.outBuf
|
||||||
func (ss *Shell) queue(data []byte) {
|
func (t *Terminal) queue(data []byte) {
|
||||||
if len(ss.outBuf)+len(data) > cap(ss.outBuf) {
|
if len(t.outBuf)+len(data) > cap(t.outBuf) {
|
||||||
newOutBuf := make([]byte, len(ss.outBuf), 2*(len(ss.outBuf)+len(data)))
|
newOutBuf := make([]byte, len(t.outBuf), 2*(len(t.outBuf)+len(data)))
|
||||||
copy(newOutBuf, ss.outBuf)
|
copy(newOutBuf, t.outBuf)
|
||||||
ss.outBuf = newOutBuf
|
t.outBuf = newOutBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
oldLen := len(ss.outBuf)
|
oldLen := len(t.outBuf)
|
||||||
ss.outBuf = ss.outBuf[:len(ss.outBuf)+len(data)]
|
t.outBuf = t.outBuf[:len(t.outBuf)+len(data)]
|
||||||
copy(ss.outBuf[oldLen:], data)
|
copy(t.outBuf[oldLen:], data)
|
||||||
}
|
}
|
||||||
|
|
||||||
var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'}
|
var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'}
|
||||||
@ -126,31 +126,31 @@ func isPrintable(key int) bool {
|
|||||||
return key >= 32 && key < 127
|
return key >= 32 && key < 127
|
||||||
}
|
}
|
||||||
|
|
||||||
// moveCursorToPos appends data to ss.outBuf which will move the cursor to the
|
// moveCursorToPos appends data to t.outBuf which will move the cursor to the
|
||||||
// given, logical position in the text.
|
// given, logical position in the text.
|
||||||
func (ss *Shell) moveCursorToPos(pos int) {
|
func (t *Terminal) moveCursorToPos(pos int) {
|
||||||
x := len(ss.prompt) + pos
|
x := len(t.prompt) + pos
|
||||||
y := x / ss.termWidth
|
y := x / t.termWidth
|
||||||
x = x % ss.termWidth
|
x = x % t.termWidth
|
||||||
|
|
||||||
up := 0
|
up := 0
|
||||||
if y < ss.cursorY {
|
if y < t.cursorY {
|
||||||
up = ss.cursorY - y
|
up = t.cursorY - y
|
||||||
}
|
}
|
||||||
|
|
||||||
down := 0
|
down := 0
|
||||||
if y > ss.cursorY {
|
if y > t.cursorY {
|
||||||
down = y - ss.cursorY
|
down = y - t.cursorY
|
||||||
}
|
}
|
||||||
|
|
||||||
left := 0
|
left := 0
|
||||||
if x < ss.cursorX {
|
if x < t.cursorX {
|
||||||
left = ss.cursorX - x
|
left = t.cursorX - x
|
||||||
}
|
}
|
||||||
|
|
||||||
right := 0
|
right := 0
|
||||||
if x > ss.cursorX {
|
if x > t.cursorX {
|
||||||
right = x - ss.cursorX
|
right = x - t.cursorX
|
||||||
}
|
}
|
||||||
|
|
||||||
movement := make([]byte, 3*(up+down+left+right))
|
movement := make([]byte, 3*(up+down+left+right))
|
||||||
@ -180,152 +180,154 @@ func (ss *Shell) moveCursorToPos(pos int) {
|
|||||||
m = m[3:]
|
m = m[3:]
|
||||||
}
|
}
|
||||||
|
|
||||||
ss.cursorX = x
|
t.cursorX = x
|
||||||
ss.cursorY = y
|
t.cursorY = y
|
||||||
ss.queue(movement)
|
t.queue(movement)
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLineLength = 4096
|
const maxLineLength = 4096
|
||||||
|
|
||||||
// handleKey processes the given key and, optionally, returns a line of text
|
// handleKey processes the given key and, optionally, returns a line of text
|
||||||
// that the user has entered.
|
// that the user has entered.
|
||||||
func (ss *Shell) handleKey(key int) (line string, ok bool) {
|
func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
||||||
switch key {
|
switch key {
|
||||||
case keyBackspace:
|
case keyBackspace:
|
||||||
if ss.pos == 0 {
|
if t.pos == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ss.pos--
|
t.pos--
|
||||||
|
|
||||||
copy(ss.line[ss.pos:], ss.line[1+ss.pos:])
|
copy(t.line[t.pos:], t.line[1+t.pos:])
|
||||||
ss.line = ss.line[:len(ss.line)-1]
|
t.line = t.line[:len(t.line)-1]
|
||||||
ss.writeLine(ss.line[ss.pos:])
|
t.writeLine(t.line[t.pos:])
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
ss.queue(eraseUnderCursor)
|
t.queue(eraseUnderCursor)
|
||||||
case keyAltLeft:
|
case keyAltLeft:
|
||||||
// move left by a word.
|
// move left by a word.
|
||||||
if ss.pos == 0 {
|
if t.pos == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ss.pos--
|
t.pos--
|
||||||
for ss.pos > 0 {
|
for t.pos > 0 {
|
||||||
if ss.line[ss.pos] != ' ' {
|
if t.line[t.pos] != ' ' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
ss.pos--
|
t.pos--
|
||||||
}
|
}
|
||||||
for ss.pos > 0 {
|
for t.pos > 0 {
|
||||||
if ss.line[ss.pos] == ' ' {
|
if t.line[t.pos] == ' ' {
|
||||||
ss.pos++
|
t.pos++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
ss.pos--
|
t.pos--
|
||||||
}
|
}
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
case keyAltRight:
|
case keyAltRight:
|
||||||
// move right by a word.
|
// move right by a word.
|
||||||
for ss.pos < len(ss.line) {
|
for t.pos < len(t.line) {
|
||||||
if ss.line[ss.pos] == ' ' {
|
if t.line[t.pos] == ' ' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
ss.pos++
|
t.pos++
|
||||||
}
|
}
|
||||||
for ss.pos < len(ss.line) {
|
for t.pos < len(t.line) {
|
||||||
if ss.line[ss.pos] != ' ' {
|
if t.line[t.pos] != ' ' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
ss.pos++
|
t.pos++
|
||||||
}
|
}
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
case keyLeft:
|
case keyLeft:
|
||||||
if ss.pos == 0 {
|
if t.pos == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ss.pos--
|
t.pos--
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
case keyRight:
|
case keyRight:
|
||||||
if ss.pos == len(ss.line) {
|
if t.pos == len(t.line) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ss.pos++
|
t.pos++
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
case keyEnter:
|
case keyEnter:
|
||||||
ss.moveCursorToPos(len(ss.line))
|
t.moveCursorToPos(len(t.line))
|
||||||
ss.queue([]byte("\r\n"))
|
t.queue([]byte("\r\n"))
|
||||||
line = string(ss.line)
|
line = string(t.line)
|
||||||
ok = true
|
ok = true
|
||||||
ss.line = ss.line[:0]
|
t.line = t.line[:0]
|
||||||
ss.pos = 0
|
t.pos = 0
|
||||||
ss.cursorX = 0
|
t.cursorX = 0
|
||||||
ss.cursorY = 0
|
t.cursorY = 0
|
||||||
ss.maxLine = 0
|
t.maxLine = 0
|
||||||
default:
|
default:
|
||||||
if !isPrintable(key) {
|
if !isPrintable(key) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(ss.line) == maxLineLength {
|
if len(t.line) == maxLineLength {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(ss.line) == cap(ss.line) {
|
if len(t.line) == cap(t.line) {
|
||||||
newLine := make([]byte, len(ss.line), 2*(1+len(ss.line)))
|
newLine := make([]byte, len(t.line), 2*(1+len(t.line)))
|
||||||
copy(newLine, ss.line)
|
copy(newLine, t.line)
|
||||||
ss.line = newLine
|
t.line = newLine
|
||||||
}
|
}
|
||||||
ss.line = ss.line[:len(ss.line)+1]
|
t.line = t.line[:len(t.line)+1]
|
||||||
copy(ss.line[ss.pos+1:], ss.line[ss.pos:])
|
copy(t.line[t.pos+1:], t.line[t.pos:])
|
||||||
ss.line[ss.pos] = byte(key)
|
t.line[t.pos] = byte(key)
|
||||||
ss.writeLine(ss.line[ss.pos:])
|
t.writeLine(t.line[t.pos:])
|
||||||
ss.pos++
|
t.pos++
|
||||||
ss.moveCursorToPos(ss.pos)
|
t.moveCursorToPos(t.pos)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Shell) writeLine(line []byte) {
|
func (t *Terminal) writeLine(line []byte) {
|
||||||
for len(line) != 0 {
|
for len(line) != 0 {
|
||||||
if ss.cursorX == ss.termWidth {
|
if t.cursorX == t.termWidth {
|
||||||
ss.queue([]byte("\r\n"))
|
t.queue([]byte("\r\n"))
|
||||||
ss.cursorX = 0
|
t.cursorX = 0
|
||||||
ss.cursorY++
|
t.cursorY++
|
||||||
if ss.cursorY > ss.maxLine {
|
if t.cursorY > t.maxLine {
|
||||||
ss.maxLine = ss.cursorY
|
t.maxLine = t.cursorY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remainingOnLine := ss.termWidth - ss.cursorX
|
remainingOnLine := t.termWidth - t.cursorX
|
||||||
todo := len(line)
|
todo := len(line)
|
||||||
if todo > remainingOnLine {
|
if todo > remainingOnLine {
|
||||||
todo = remainingOnLine
|
todo = remainingOnLine
|
||||||
}
|
}
|
||||||
ss.queue(line[:todo])
|
t.queue(line[:todo])
|
||||||
ss.cursorX += todo
|
t.cursorX += todo
|
||||||
line = line[todo:]
|
line = line[todo:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Shell) Write(buf []byte) (n int, err error) {
|
func (t *Terminal) Write(buf []byte) (n int, err error) {
|
||||||
return ss.c.Write(buf)
|
return t.c.Write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadLine returns a line of input from the terminal.
|
// ReadLine returns a line of input from the terminal.
|
||||||
func (ss *Shell) ReadLine() (line string, err error) {
|
func (t *Terminal) ReadLine() (line string, err error) {
|
||||||
ss.writeLine([]byte(ss.prompt))
|
if t.cursorX == 0 {
|
||||||
ss.c.Write(ss.outBuf)
|
t.writeLine([]byte(t.prompt))
|
||||||
ss.outBuf = ss.outBuf[:0]
|
t.c.Write(t.outBuf)
|
||||||
|
t.outBuf = t.outBuf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// ss.remainder is a slice at the beginning of ss.inBuf
|
// t.remainder is a slice at the beginning of t.inBuf
|
||||||
// containing a partial key sequence
|
// containing a partial key sequence
|
||||||
readBuf := ss.inBuf[len(ss.remainder):]
|
readBuf := t.inBuf[len(t.remainder):]
|
||||||
var n int
|
var n int
|
||||||
n, err = ss.c.Read(readBuf)
|
n, err = t.c.Read(readBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ss.remainder = ss.inBuf[:n+len(ss.remainder)]
|
t.remainder = t.inBuf[:n+len(t.remainder)]
|
||||||
rest := ss.remainder
|
rest := t.remainder
|
||||||
lineOk := false
|
lineOk := false
|
||||||
for !lineOk {
|
for !lineOk {
|
||||||
var key int
|
var key int
|
||||||
@ -336,16 +338,16 @@ func (ss *Shell) ReadLine() (line string, err error) {
|
|||||||
if key == keyCtrlD {
|
if key == keyCtrlD {
|
||||||
return "", io.EOF
|
return "", io.EOF
|
||||||
}
|
}
|
||||||
line, lineOk = ss.handleKey(key)
|
line, lineOk = t.handleKey(key)
|
||||||
}
|
}
|
||||||
if len(rest) > 0 {
|
if len(rest) > 0 {
|
||||||
n := copy(ss.inBuf[:], rest)
|
n := copy(t.inBuf[:], rest)
|
||||||
ss.remainder = ss.inBuf[:n]
|
t.remainder = t.inBuf[:n]
|
||||||
} else {
|
} else {
|
||||||
ss.remainder = nil
|
t.remainder = nil
|
||||||
}
|
}
|
||||||
ss.c.Write(ss.outBuf)
|
t.c.Write(t.outBuf)
|
||||||
ss.outBuf = ss.outBuf[:0]
|
t.outBuf = t.outBuf[:0]
|
||||||
if lineOk {
|
if lineOk {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -354,3 +356,7 @@ func (ss *Shell) ReadLine() (line string, err error) {
|
|||||||
}
|
}
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) SetSize(width, height int) {
|
||||||
|
t.termWidth, t.termHeight = width, height
|
||||||
|
}
|
@ -41,7 +41,7 @@ func (c *MockTerminal) Write(data []byte) (n int, err error) {
|
|||||||
|
|
||||||
func TestClose(t *testing.T) {
|
func TestClose(t *testing.T) {
|
||||||
c := &MockTerminal{}
|
c := &MockTerminal{}
|
||||||
ss := NewShell(c, "> ")
|
ss := NewTerminal(c, "> ")
|
||||||
line, err := ss.ReadLine()
|
line, err := ss.ReadLine()
|
||||||
if line != "" {
|
if line != "" {
|
||||||
t.Errorf("Expected empty line but got: %s", line)
|
t.Errorf("Expected empty line but got: %s", line)
|
||||||
@ -95,7 +95,7 @@ func TestKeyPresses(t *testing.T) {
|
|||||||
toSend: []byte(test.in),
|
toSend: []byte(test.in),
|
||||||
bytesPerRead: j,
|
bytesPerRead: j,
|
||||||
}
|
}
|
||||||
ss := NewShell(c, "> ")
|
ss := NewTerminal(c, "> ")
|
||||||
line, err := ss.ReadLine()
|
line, err := ss.ReadLine()
|
||||||
if line != test.line {
|
if line != test.line {
|
||||||
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
Loading…
Reference in New Issue
Block a user