mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
892 lines
20 KiB
892 lines
20 KiB
// 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 ( |
|
"bytes" |
|
"io" |
|
"sync" |
|
"unicode/utf8" |
|
) |
|
|
|
// 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 (in |
|
// bytes, as an index into |line|). If it returns ok=false, the key |
|
// press is processed normally. Otherwise it returns a replacement line |
|
// and the new cursor position. |
|
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool) |
|
|
|
// 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 []rune |
|
|
|
// line is the current line being entered. |
|
line []rune |
|
// pos is the logical position of the cursor in line |
|
pos int |
|
// echo is true if local echo is enabled |
|
echo bool |
|
// pasteActive is true iff there is a bracketed paste operation in |
|
// progress. |
|
pasteActive 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 |
|
// 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 |
|
|
|
// history contains previously entered commands so that they can be |
|
// accessed with the up and down keys. |
|
history stRingBuffer |
|
// historyIndex stores the currently accessed history entry, where zero |
|
// means the immediately previous entry. |
|
historyIndex int |
|
// When navigating up and down the history it's possible to return to |
|
// the incomplete, initial line. That value is stored in |
|
// historyPending. |
|
historyPending string |
|
} |
|
|
|
// 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. |
|
// prompt is a string that is written at the start of each input line (i.e. |
|
// "> "). |
|
func NewTerminal(c io.ReadWriter, prompt string) *Terminal { |
|
return &Terminal{ |
|
Escape: &vt100EscapeCodes, |
|
c: c, |
|
prompt: []rune(prompt), |
|
termWidth: 80, |
|
termHeight: 24, |
|
echo: true, |
|
historyIndex: -1, |
|
} |
|
} |
|
|
|
const ( |
|
keyCtrlD = 4 |
|
keyCtrlU = 21 |
|
keyEnter = '\r' |
|
keyEscape = 27 |
|
keyBackspace = 127 |
|
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota |
|
keyUp |
|
keyDown |
|
keyLeft |
|
keyRight |
|
keyAltLeft |
|
keyAltRight |
|
keyHome |
|
keyEnd |
|
keyDeleteWord |
|
keyDeleteLine |
|
keyClearScreen |
|
keyPasteStart |
|
keyPasteEnd |
|
) |
|
|
|
var pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} |
|
var pasteEnd = []byte{keyEscape, '[', '2', '0', '1', '~'} |
|
|
|
// bytesToKey tries to parse a key sequence from b. If successful, it returns |
|
// the key and the remainder of the input. Otherwise it returns utf8.RuneError. |
|
func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { |
|
if len(b) == 0 { |
|
return utf8.RuneError, nil |
|
} |
|
|
|
if !pasteActive { |
|
switch b[0] { |
|
case 1: // ^A |
|
return keyHome, b[1:] |
|
case 5: // ^E |
|
return keyEnd, b[1:] |
|
case 8: // ^H |
|
return keyBackspace, b[1:] |
|
case 11: // ^K |
|
return keyDeleteLine, b[1:] |
|
case 12: // ^L |
|
return keyClearScreen, b[1:] |
|
case 23: // ^W |
|
return keyDeleteWord, b[1:] |
|
} |
|
} |
|
|
|
if b[0] != keyEscape { |
|
if !utf8.FullRune(b) { |
|
return utf8.RuneError, b |
|
} |
|
r, l := utf8.DecodeRune(b) |
|
return r, b[l:] |
|
} |
|
|
|
if !pasteActive && 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:] |
|
case 'H': |
|
return keyHome, b[3:] |
|
case 'F': |
|
return keyEnd, b[3:] |
|
} |
|
} |
|
|
|
if !pasteActive && 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 !pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { |
|
return keyPasteStart, b[6:] |
|
} |
|
|
|
if pasteActive && len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { |
|
return keyPasteEnd, 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' || c == '~' { |
|
return keyUnknown, b[i+1:] |
|
} |
|
} |
|
|
|
return utf8.RuneError, b |
|
} |
|
|
|
// queue appends data to the end of t.outBuf |
|
func (t *Terminal) queue(data []rune) { |
|
t.outBuf = append(t.outBuf, []byte(string(data))...) |
|
} |
|
|
|
var eraseUnderCursor = []rune{' ', keyEscape, '[', 'D'} |
|
var space = []rune{' '} |
|
|
|
func isPrintable(key rune) bool { |
|
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff |
|
return key >= 32 && !isInSurrogateArea |
|
} |
|
|
|
// 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 := visualLength(t.prompt) + pos |
|
y := x / t.termWidth |
|
x = x % t.termWidth |
|
|
|
up := 0 |
|
if y < t.cursorY { |
|
up = t.cursorY - y |
|
} |
|
|
|
down := 0 |
|
if y > t.cursorY { |
|
down = y - t.cursorY |
|
} |
|
|
|
left := 0 |
|
if x < t.cursorX { |
|
left = t.cursorX - x |
|
} |
|
|
|
right := 0 |
|
if x > t.cursorX { |
|
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([]rune, 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:] |
|
} |
|
|
|
t.queue(movement) |
|
} |
|
|
|
func (t *Terminal) clearLineToRight() { |
|
op := []rune{keyEscape, '[', 'K'} |
|
t.queue(op) |
|
} |
|
|
|
const maxLineLength = 4096 |
|
|
|
func (t *Terminal) setLine(newLine []rune, newPos int) { |
|
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 |
|
} |
|
|
|
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 |
|
} |
|
|
|
if t.pos < n { |
|
n = t.pos |
|
} |
|
t.pos -= n |
|
t.moveCursorToPos(t.pos) |
|
|
|
copy(t.line[t.pos:], t.line[n+t.pos:]) |
|
t.line = t.line[:len(t.line)-n] |
|
if t.echo { |
|
t.writeLine(t.line[t.pos:]) |
|
for i := 0; i < n; i++ { |
|
t.queue(space) |
|
} |
|
t.advanceCursor(n) |
|
t.moveCursorToPos(t.pos) |
|
} |
|
} |
|
|
|
// countToLeftWord returns then number of characters from the cursor to the |
|
// start of the previous word. |
|
func (t *Terminal) countToLeftWord() int { |
|
if t.pos == 0 { |
|
return 0 |
|
} |
|
|
|
pos := t.pos - 1 |
|
for pos > 0 { |
|
if t.line[pos] != ' ' { |
|
break |
|
} |
|
pos-- |
|
} |
|
for pos > 0 { |
|
if t.line[pos] == ' ' { |
|
pos++ |
|
break |
|
} |
|
pos-- |
|
} |
|
|
|
return t.pos - pos |
|
} |
|
|
|
// countToRightWord returns then number of characters from the cursor to the |
|
// start of the next word. |
|
func (t *Terminal) countToRightWord() int { |
|
pos := t.pos |
|
for pos < len(t.line) { |
|
if t.line[pos] == ' ' { |
|
break |
|
} |
|
pos++ |
|
} |
|
for pos < len(t.line) { |
|
if t.line[pos] != ' ' { |
|
break |
|
} |
|
pos++ |
|
} |
|
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) { |
|
if t.pasteActive && key != keyEnter { |
|
t.addKeyToLine(key) |
|
return |
|
} |
|
|
|
switch key { |
|
case keyBackspace: |
|
if t.pos == 0 { |
|
return |
|
} |
|
t.eraseNPreviousChars(1) |
|
case keyAltLeft: |
|
// move left by a word. |
|
t.pos -= t.countToLeftWord() |
|
t.moveCursorToPos(t.pos) |
|
case keyAltRight: |
|
// move right by a word. |
|
t.pos += t.countToRightWord() |
|
t.moveCursorToPos(t.pos) |
|
case keyLeft: |
|
if t.pos == 0 { |
|
return |
|
} |
|
t.pos-- |
|
t.moveCursorToPos(t.pos) |
|
case keyRight: |
|
if t.pos == len(t.line) { |
|
return |
|
} |
|
t.pos++ |
|
t.moveCursorToPos(t.pos) |
|
case keyHome: |
|
if t.pos == 0 { |
|
return |
|
} |
|
t.pos = 0 |
|
t.moveCursorToPos(t.pos) |
|
case keyEnd: |
|
if t.pos == len(t.line) { |
|
return |
|
} |
|
t.pos = len(t.line) |
|
t.moveCursorToPos(t.pos) |
|
case keyUp: |
|
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) |
|
if !ok { |
|
return "", false |
|
} |
|
if t.historyIndex == -1 { |
|
t.historyPending = string(t.line) |
|
} |
|
t.historyIndex++ |
|
runes := []rune(entry) |
|
t.setLine(runes, len(runes)) |
|
case keyDown: |
|
switch t.historyIndex { |
|
case -1: |
|
return |
|
case 0: |
|
runes := []rune(t.historyPending) |
|
t.setLine(runes, len(runes)) |
|
t.historyIndex-- |
|
default: |
|
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) |
|
if ok { |
|
t.historyIndex-- |
|
runes := []rune(entry) |
|
t.setLine(runes, len(runes)) |
|
} |
|
} |
|
case keyEnter: |
|
t.moveCursorToPos(len(t.line)) |
|
t.queue([]rune("\r\n")) |
|
line = string(t.line) |
|
ok = true |
|
t.line = t.line[:0] |
|
t.pos = 0 |
|
t.cursorX = 0 |
|
t.cursorY = 0 |
|
t.maxLine = 0 |
|
case keyDeleteWord: |
|
// Delete zero or more spaces and then one or more characters. |
|
t.eraseNPreviousChars(t.countToLeftWord()) |
|
case keyDeleteLine: |
|
// Delete everything from the current cursor position to the |
|
// end of line. |
|
for i := t.pos; i < len(t.line); i++ { |
|
t.queue(space) |
|
t.advanceCursor(1) |
|
} |
|
t.line = t.line[:t.pos] |
|
t.moveCursorToPos(t.pos) |
|
case keyCtrlD: |
|
// Erase the character under the current position. |
|
// The EOF case when the line is empty is handled in |
|
// readLine(). |
|
if t.pos < len(t.line) { |
|
t.pos++ |
|
t.eraseNPreviousChars(1) |
|
} |
|
case keyCtrlU: |
|
t.eraseNPreviousChars(t.pos) |
|
case keyClearScreen: |
|
// Erases the screen and moves the cursor to the home position. |
|
t.queue([]rune("\x1b[2J\x1b[H")) |
|
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 { |
|
prefix := string(t.line[:t.pos]) |
|
suffix := string(t.line[t.pos:]) |
|
|
|
t.lock.Unlock() |
|
newLine, newPos, completeOk := t.AutoCompleteCallback(prefix+suffix, len(prefix), key) |
|
t.lock.Lock() |
|
|
|
if completeOk { |
|
t.setLine([]rune(newLine), utf8.RuneCount([]byte(newLine)[:newPos])) |
|
return |
|
} |
|
} |
|
if !isPrintable(key) { |
|
return |
|
} |
|
if len(t.line) == maxLineLength { |
|
return |
|
} |
|
t.addKeyToLine(key) |
|
} |
|
return |
|
} |
|
|
|
// addKeyToLine inserts the given key at the current position in the current |
|
// line. |
|
func (t *Terminal) addKeyToLine(key rune) { |
|
if len(t.line) == cap(t.line) { |
|
newLine := make([]rune, len(t.line), 2*(1+len(t.line))) |
|
copy(newLine, t.line) |
|
t.line = newLine |
|
} |
|
t.line = t.line[:len(t.line)+1] |
|
copy(t.line[t.pos+1:], t.line[t.pos:]) |
|
t.line[t.pos] = key |
|
if t.echo { |
|
t.writeLine(t.line[t.pos:]) |
|
} |
|
t.pos++ |
|
t.moveCursorToPos(t.pos) |
|
} |
|
|
|
func (t *Terminal) writeLine(line []rune) { |
|
for len(line) != 0 { |
|
remainingOnLine := t.termWidth - t.cursorX |
|
todo := len(line) |
|
if todo > remainingOnLine { |
|
todo = remainingOnLine |
|
} |
|
t.queue(line[:todo]) |
|
t.advanceCursor(visualLength(line[:todo])) |
|
line = line[todo:] |
|
} |
|
} |
|
|
|
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.writeLine(t.prompt) |
|
if t.echo { |
|
t.writeLine(t.line) |
|
} |
|
|
|
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 = []rune(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) { |
|
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(t.prompt) |
|
t.c.Write(t.outBuf) |
|
t.outBuf = t.outBuf[:0] |
|
} |
|
|
|
lineIsPasted := t.pasteActive |
|
|
|
for { |
|
rest := t.remainder |
|
lineOk := false |
|
for !lineOk { |
|
var key rune |
|
key, rest = bytesToKey(rest, t.pasteActive) |
|
if key == utf8.RuneError { |
|
break |
|
} |
|
if !t.pasteActive { |
|
if key == keyCtrlD { |
|
if len(t.line) == 0 { |
|
return "", io.EOF |
|
} |
|
} |
|
if key == keyPasteStart { |
|
t.pasteActive = true |
|
if len(t.line) == 0 { |
|
lineIsPasted = true |
|
} |
|
continue |
|
} |
|
} else if key == keyPasteEnd { |
|
t.pasteActive = false |
|
continue |
|
} |
|
if !t.pasteActive { |
|
lineIsPasted = false |
|
} |
|
line, lineOk = t.handleKey(key) |
|
} |
|
if len(rest) > 0 { |
|
n := copy(t.inBuf[:], rest) |
|
t.remainder = t.inBuf[:n] |
|
} else { |
|
t.remainder = nil |
|
} |
|
t.c.Write(t.outBuf) |
|
t.outBuf = t.outBuf[:0] |
|
if lineOk { |
|
if t.echo { |
|
t.historyIndex = -1 |
|
t.history.Add(line) |
|
} |
|
if lineIsPasted { |
|
err = ErrPasteIndicator |
|
} |
|
return |
|
} |
|
|
|
// t.remainder is a slice at the beginning of t.inBuf |
|
// 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 |
|
} |
|
|
|
t.remainder = t.inBuf[:n+len(t.remainder)] |
|
} |
|
|
|
panic("unreachable") // for Go 1.0. |
|
} |
|
|
|
// SetPrompt sets the prompt to be used when reading subsequent lines. |
|
func (t *Terminal) SetPrompt(prompt string) { |
|
t.lock.Lock() |
|
defer t.lock.Unlock() |
|
|
|
t.prompt = []rune(prompt) |
|
} |
|
|
|
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() |
|
|
|
if width == 0 { |
|
width = 1 |
|
} |
|
|
|
oldWidth := t.termWidth |
|
t.termWidth, t.termHeight = width, height |
|
|
|
switch { |
|
case width == oldWidth: |
|
// If the width didn't change then nothing else needs to be |
|
// done. |
|
return nil |
|
case len(t.line) == 0 && t.cursorX == 0 && t.cursorY == 0: |
|
// If there is nothing on current line and no prompt printed, |
|
// just do nothing |
|
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. |
|
if t.cursorX >= t.termWidth { |
|
t.cursorX = t.termWidth - 1 |
|
} |
|
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 |
|
} |
|
|
|
type pasteIndicatorError struct{} |
|
|
|
func (pasteIndicatorError) Error() string { |
|
return "terminal: ErrPasteIndicator not correctly handled" |
|
} |
|
|
|
// ErrPasteIndicator may be returned from ReadLine as the error, in addition |
|
// to valid line data. It indicates that bracketed paste mode is enabled and |
|
// that the returned line consists only of pasted data. Programs may wish to |
|
// interpret pasted data more literally than typed data. |
|
var ErrPasteIndicator = pasteIndicatorError{} |
|
|
|
// SetBracketedPasteMode requests that the terminal bracket paste operations |
|
// with markers. Not all terminals support this but, if it is supported, then |
|
// enabling this mode will stop any autocomplete callback from running due to |
|
// pastes. Additionally, any lines that are completely pasted will be returned |
|
// from ReadLine with the error set to ErrPasteIndicator. |
|
func (t *Terminal) SetBracketedPasteMode(on bool) { |
|
if on { |
|
io.WriteString(t.c, "\x1b[?2004h") |
|
} else { |
|
io.WriteString(t.c, "\x1b[?2004l") |
|
} |
|
} |
|
|
|
// stRingBuffer is a ring buffer of strings. |
|
type stRingBuffer struct { |
|
// entries contains max elements. |
|
entries []string |
|
max int |
|
// head contains the index of the element most recently added to the ring. |
|
head int |
|
// size contains the number of elements in the ring. |
|
size int |
|
} |
|
|
|
func (s *stRingBuffer) Add(a string) { |
|
if s.entries == nil { |
|
const defaultNumEntries = 100 |
|
s.entries = make([]string, defaultNumEntries) |
|
s.max = defaultNumEntries |
|
} |
|
|
|
s.head = (s.head + 1) % s.max |
|
s.entries[s.head] = a |
|
if s.size < s.max { |
|
s.size++ |
|
} |
|
} |
|
|
|
// NthPreviousEntry returns the value passed to the nth previous call to Add. |
|
// If n is zero then the immediately prior value is returned, if one, then the |
|
// next most recent, and so on. If such an element doesn't exist then ok is |
|
// false. |
|
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { |
|
if n >= s.size { |
|
return "", false |
|
} |
|
index := s.head - n |
|
if index < 0 { |
|
index += s.max |
|
} |
|
return s.entries[index], true |
|
}
|
|
|