go.crypto/ssh/terminal: better handling of window resizing.

There doesn't appear to be perfect behaviour for line editing
code in the face of terminal resizing. But this change works
pretty well on xterm and gnome-terminal and certainly a lot
better than it used to.

LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/105580043
This commit is contained in:
Adam Langley
2014-08-01 11:22:47 -07:00
parent 2aff5f12d8
commit 76cee1b7b1
+116 -27
View File
@@ -53,7 +53,7 @@ type Terminal struct {
lock sync.Mutex
c io.ReadWriter
prompt string
prompt []rune
// line is the current line being entered.
line []rune
@@ -98,7 +98,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
return &Terminal{
Escape: &vt100EscapeCodes,
c: c,
prompt: prompt,
prompt: []rune(prompt),
termWidth: 80,
termHeight: 24,
echo: true,
@@ -220,7 +220,7 @@ func (t *Terminal) moveCursorToPos(pos int) {
return
}
x := len(t.prompt) + pos
x := visualLength(t.prompt) + pos
y := x / t.termWidth
x = x % t.termWidth
@@ -300,6 +300,29 @@ func (t *Terminal) setLine(newLine []rune, newPos int) {
t.pos = newPos
}
func (t *Terminal) advanceCursor(places int) {
t.cursorX += places
t.cursorY += t.cursorX / t.termWidth
if t.cursorY > t.maxLine {
t.maxLine = t.cursorY
}
t.cursorX = t.cursorX % t.termWidth
if places > 0 && t.cursorX == 0 {
// Normally terminals will advance the current position
// when writing a character. But that doesn't happen
// for the last character in a line. However, when
// writing a character (except a new line) that causes
// a line wrap, the position will be advanced two
// places.
//
// So, if we are stopping at the end of a line, we
// need to write a newline so that our cursor can be
// advanced to the next line.
t.outBuf = append(t.outBuf, '\n')
}
}
func (t *Terminal) eraseNPreviousChars(n int) {
if n == 0 {
return
@@ -318,7 +341,7 @@ func (t *Terminal) eraseNPreviousChars(n int) {
for i := 0; i < n; i++ {
t.queue(space)
}
t.cursorX += n
t.advanceCursor(n)
t.moveCursorToPos(t.pos)
}
}
@@ -367,6 +390,27 @@ func (t *Terminal) countToRightWord() int {
return pos - t.pos
}
// visualLength returns the number of visible glyphs in s.
func visualLength(runes []rune) int {
inEscapeSeq := false
length := 0
for _, r := range runes {
switch {
case inEscapeSeq:
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
inEscapeSeq = false
}
case r == '\x1b':
inEscapeSeq = true
default:
length++
}
}
return length
}
// 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) {
@@ -453,7 +497,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
// end of line.
for i := t.pos; i < len(t.line); i++ {
t.queue(space)
t.cursorX++
t.advanceCursor(1)
}
t.line = t.line[:t.pos]
t.moveCursorToPos(t.pos)
@@ -470,9 +514,9 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
case keyClearScreen:
// Erases the screen and moves the cursor to the home position.
t.queue([]rune("\x1b[2J\x1b[H"))
t.queue([]rune(t.prompt))
t.cursorX = len(t.prompt)
t.cursorY = 0
t.queue(t.prompt)
t.cursorX, t.cursorY = 0, 0
t.advanceCursor(visualLength(t.prompt))
t.setLine(t.line, t.pos)
default:
if t.AutoCompleteCallback != nil {
@@ -519,16 +563,8 @@ func (t *Terminal) writeLine(line []rune) {
todo = remainingOnLine
}
t.queue(line[:todo])
t.cursorX += todo
t.advanceCursor(visualLength(line[:todo]))
line = line[todo:]
if t.cursorX == t.termWidth {
t.cursorX = 0
t.cursorY++
if t.cursorY > t.maxLine {
t.maxLine = t.cursorY
}
}
}
}
@@ -563,14 +599,11 @@ func (t *Terminal) Write(buf []byte) (n int, err error) {
return
}
t.queue([]rune(t.prompt))
chars := len(t.prompt)
t.writeLine(t.prompt)
if t.echo {
t.queue(t.line)
chars += len(t.line)
t.writeLine(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 {
@@ -587,7 +620,7 @@ func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
defer t.lock.Unlock()
oldPrompt := t.prompt
t.prompt = prompt
t.prompt = []rune(prompt)
t.echo = false
line, err = t.readLine()
@@ -610,7 +643,7 @@ 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.writeLine(t.prompt)
t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
}
@@ -671,14 +704,70 @@ func (t *Terminal) SetPrompt(prompt string) {
t.lock.Lock()
defer t.lock.Unlock()
t.prompt = prompt
t.prompt = []rune(prompt)
}
func (t *Terminal) SetSize(width, height int) {
func (t *Terminal) clearAndRepaintLinePlusNPrevious(numPrevLines int) {
// Move cursor to column zero at the start of the line.
t.move(t.cursorY, 0, t.cursorX, 0)
t.cursorX, t.cursorY = 0, 0
t.clearLineToRight()
for t.cursorY < numPrevLines {
// Move down a line
t.move(0, 1, 0, 0)
t.cursorY++
t.clearLineToRight()
}
// Move back to beginning.
t.move(t.cursorY, 0, 0, 0)
t.cursorX, t.cursorY = 0, 0
t.queue(t.prompt)
t.advanceCursor(visualLength(t.prompt))
t.writeLine(t.line)
t.moveCursorToPos(t.pos)
}
func (t *Terminal) SetSize(width, height int) error {
t.lock.Lock()
defer t.lock.Unlock()
oldWidth := t.termWidth
t.termWidth, t.termHeight = width, height
switch {
case width == oldWidth || len(t.line) == 0:
// If the width didn't change then nothing else needs to be
// done.
return nil
case width < oldWidth:
// Some terminals (e.g. xterm) will truncate lines that were
// too long when shinking. Others, (e.g. gnome-terminal) will
// attempt to wrap them. For the former, repainting t.maxLine
// works great, but that behaviour goes badly wrong in the case
// of the latter because they have doubled every full line.
// We assume that we are working on a terminal that wraps lines
// and adjust the cursor position based on every previous line
// wrapping and turning into two. This causes the prompt on
// xterms to move upwards, which isn't great, but it avoids a
// huge mess with gnome-terminal.
t.cursorY *= 2
t.clearAndRepaintLinePlusNPrevious(t.maxLine * 2)
case width > oldWidth:
// If the terminal expands then our position calculations will
// be wrong in the future because we think the cursor is
// |t.pos| chars into the string, but there will be a gap at
// the end of any wrapped line.
//
// But the position will actually be correct until we move, so
// we can move back to the beginning and repaint everything.
t.clearAndRepaintLinePlusNPrevious(t.maxLine)
}
_, err := t.c.Write(t.outBuf)
t.outBuf = t.outBuf[:0]
return err
}
// stRingBuffer is a ring buffer of strings.