mirror of
https://github.com/golang/go
synced 2024-11-21 23:44:39 -07:00
exp/terminal: several cleanups
1) Add EscapeCodes to the terminal so that applications don't wire them in. 2) Add a callback for auto-complete 3) Fix an issue with input lines longer than the width of the terminal. 4) Have Write() not stomp the current line. It now erases the current input, writes the output and reprints the prompt and partial input. 5) Support prompting without local echo in Terminal. 6) Add GetSize to report the size of terminal. R=bradfitz CC=golang-dev https://golang.org/cl/5479043
This commit is contained in:
parent
f1ebbf80bd
commit
7350c771f8
@ -6,11 +6,52 @@
|
||||
|
||||
package terminal
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// If it returns a nil newLine, the key press is processed normally.
|
||||
// Otherwise it returns a replacement line and the new cursor position.
|
||||
AutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int)
|
||||
|
||||
// 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
|
||||
|
||||
@ -18,6 +59,8 @@ type Terminal struct {
|
||||
line []byte
|
||||
// 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
|
||||
@ -42,10 +85,12 @@ type Terminal struct {
|
||||
// "> ").
|
||||
func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
|
||||
return &Terminal{
|
||||
Escape: &vt100EscapeCodes,
|
||||
c: c,
|
||||
prompt: prompt,
|
||||
termWidth: 80,
|
||||
termHeight: 24,
|
||||
echo: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,18 +156,11 @@ func bytesToKey(b []byte) (int, []byte) {
|
||||
|
||||
// queue appends data to the end of t.outBuf
|
||||
func (t *Terminal) queue(data []byte) {
|
||||
if len(t.outBuf)+len(data) > cap(t.outBuf) {
|
||||
newOutBuf := make([]byte, len(t.outBuf), 2*(len(t.outBuf)+len(data)))
|
||||
copy(newOutBuf, t.outBuf)
|
||||
t.outBuf = newOutBuf
|
||||
}
|
||||
|
||||
oldLen := len(t.outBuf)
|
||||
t.outBuf = t.outBuf[:len(t.outBuf)+len(data)]
|
||||
copy(t.outBuf[oldLen:], data)
|
||||
t.outBuf = append(t.outBuf, data...)
|
||||
}
|
||||
|
||||
var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'}
|
||||
var space = []byte{' '}
|
||||
|
||||
func isPrintable(key int) bool {
|
||||
return key >= 32 && key < 127
|
||||
@ -131,6 +169,10 @@ func isPrintable(key int) bool {
|
||||
// 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
|
||||
@ -155,6 +197,12 @@ func (t *Terminal) moveCursorToPos(pos int) {
|
||||
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([]byte, 3*(up+down+left+right))
|
||||
m := movement
|
||||
for i := 0; i < up; i++ {
|
||||
@ -182,11 +230,14 @@ func (t *Terminal) moveCursorToPos(pos int) {
|
||||
m = m[3:]
|
||||
}
|
||||
|
||||
t.cursorX = x
|
||||
t.cursorY = y
|
||||
t.queue(movement)
|
||||
}
|
||||
|
||||
func (t *Terminal) clearLineToRight() {
|
||||
op := []byte{keyEscape, '[', 'K'}
|
||||
t.queue(op)
|
||||
}
|
||||
|
||||
const maxLineLength = 4096
|
||||
|
||||
// handleKey processes the given key and, optionally, returns a line of text
|
||||
@ -198,12 +249,15 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
||||
return
|
||||
}
|
||||
t.pos--
|
||||
t.moveCursorToPos(t.pos)
|
||||
|
||||
copy(t.line[t.pos:], t.line[1+t.pos:])
|
||||
t.line = t.line[:len(t.line)-1]
|
||||
if t.echo {
|
||||
t.writeLine(t.line[t.pos:])
|
||||
t.moveCursorToPos(t.pos)
|
||||
}
|
||||
t.queue(eraseUnderCursor)
|
||||
t.moveCursorToPos(t.pos)
|
||||
case keyAltLeft:
|
||||
// move left by a word.
|
||||
if t.pos == 0 {
|
||||
@ -262,6 +316,25 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
||||
t.cursorY = 0
|
||||
t.maxLine = 0
|
||||
default:
|
||||
if t.AutoCompleteCallback != nil {
|
||||
t.lock.Unlock()
|
||||
newLine, newPos := t.AutoCompleteCallback(t.line, t.pos, key)
|
||||
t.lock.Lock()
|
||||
|
||||
if newLine != nil {
|
||||
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
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isPrintable(key) {
|
||||
return
|
||||
}
|
||||
@ -276,7 +349,9 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
||||
t.line = t.line[:len(t.line)+1]
|
||||
copy(t.line[t.pos+1:], t.line[t.pos:])
|
||||
t.line[t.pos] = byte(key)
|
||||
if t.echo {
|
||||
t.writeLine(t.line[t.pos:])
|
||||
}
|
||||
t.pos++
|
||||
t.moveCursorToPos(t.pos)
|
||||
}
|
||||
@ -285,15 +360,6 @@ func (t *Terminal) handleKey(key int) (line string, ok bool) {
|
||||
|
||||
func (t *Terminal) writeLine(line []byte) {
|
||||
for len(line) != 0 {
|
||||
if t.cursorX == t.termWidth {
|
||||
t.queue([]byte("\r\n"))
|
||||
t.cursorX = 0
|
||||
t.cursorY++
|
||||
if t.cursorY > t.maxLine {
|
||||
t.maxLine = t.cursorY
|
||||
}
|
||||
}
|
||||
|
||||
remainingOnLine := t.termWidth - t.cursorX
|
||||
todo := len(line)
|
||||
if todo > remainingOnLine {
|
||||
@ -302,16 +368,95 @@ func (t *Terminal) writeLine(line []byte) {
|
||||
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([]byte(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) {
|
||||
if t.cursorX == 0 {
|
||||
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([]byte(t.prompt))
|
||||
t.c.Write(t.outBuf)
|
||||
t.outBuf = t.outBuf[:0]
|
||||
@ -322,7 +467,11 @@ func (t *Terminal) ReadLine() (line string, err error) {
|
||||
// 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
|
||||
}
|
||||
@ -360,5 +509,8 @@ func (t *Terminal) ReadLine() (line string, err error) {
|
||||
}
|
||||
|
||||
func (t *Terminal) SetSize(width, height int) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
t.termWidth, t.termHeight = width, height
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
//
|
||||
// oldState, err := terminal.MakeRaw(0)
|
||||
// if err != nil {
|
||||
// panic(err.String())
|
||||
// panic(err)
|
||||
// }
|
||||
// defer terminal.Restore(0, oldState)
|
||||
package terminal
|
||||
@ -60,6 +60,16 @@ func Restore(fd int, state *State) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSize returns the dimensions of the given terminal.
|
||||
func GetSize(fd int) (width, height int, err error) {
|
||||
var dimensions [4]uint16
|
||||
|
||||
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 {
|
||||
return -1, -1, err
|
||||
}
|
||||
return int(dimensions[1]), int(dimensions[0]), nil
|
||||
}
|
||||
|
||||
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||
// returned does not include the \n.
|
||||
|
Loading…
Reference in New Issue
Block a user