// 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 termimport ()// EscapeCodes contains escape sequences that can be written to the terminal in// order to achieve different styles of text.typeEscapeCodesstruct {// 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'},}// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine].typeHistoryinterface {// Add will be called by [Terminal.ReadLine] to add // a new, most recent entry to the history. // It is allowed to drop any entry, including // the entry being added (e.g., if it's deemed an invalid entry), // the least-recent entry (e.g., to keep the history bounded), // or any other entry.Add(entry string)// Len returns the number of entries in the history.Len() int// At returns an entry from the history. // Index 0 is the most-recently added entry and // index Len()-1 is the least-recently added entry. // If index is < 0 or >= Len(), it panics.At(idx int) string}// Terminal contains the state for running a VT100 terminal that is capable of// reading lines of input.typeTerminalstruct {// 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. // // This will be disabled during ReadPassword. 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 records and retrieves lines of input read by [ReadLine] which // a user can retrieve and navigate using the up and down arrow keys. // // It is not safe to call ReadLine concurrently with any methods on History. // // [NewTerminal] sets this to a default implementation that records the // last 100 lines of input. History History// 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 ( io.ReadWriter, string) *Terminal {return &Terminal{Escape: &vt100EscapeCodes,c: ,prompt: []rune(),termWidth: 80,termHeight: 24,echo: true,historyIndex: -1,History: &stRingBuffer{}, }}const ( keyCtrlC = 3 keyCtrlD = 4 keyCtrlU = 21 keyEnter = '\r' keyLF = '\n' 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 ( crlf = []byte{'\r', '\n'} pasteStart = []byte{keyEscape, '[', '2', '0', '0', '~'} 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( []byte, bool) (rune, []byte) {iflen() == 0 {returnutf8.RuneError, nil }if ! {switch [0] {case1: // ^AreturnkeyHome, [1:]case2: // ^BreturnkeyLeft, [1:]case5: // ^EreturnkeyEnd, [1:]case6: // ^FreturnkeyRight, [1:]case8: // ^HreturnkeyBackspace, [1:]case11: // ^KreturnkeyDeleteLine, [1:]case12: // ^LreturnkeyClearScreen, [1:]case23: // ^WreturnkeyDeleteWord, [1:]case14: // ^NreturnkeyDown, [1:]case16: // ^PreturnkeyUp, [1:] } }if [0] != keyEscape {if !utf8.FullRune() {returnutf8.RuneError, } , := utf8.DecodeRune()return , [:] }if ! && len() >= 3 && [0] == keyEscape && [1] == '[' {switch [2] {case'A':returnkeyUp, [3:]case'B':returnkeyDown, [3:]case'C':returnkeyRight, [3:]case'D':returnkeyLeft, [3:]case'H':returnkeyHome, [3:]case'F':returnkeyEnd, [3:] } }if ! && len() >= 6 && [0] == keyEscape && [1] == '[' && [2] == '1' && [3] == ';' && [4] == '3' {switch [5] {case'C':returnkeyAltRight, [6:]case'D':returnkeyAltLeft, [6:] } }if ! && len() >= 6 && bytes.Equal([:6], pasteStart) {returnkeyPasteStart, [6:] }if && len() >= 6 && bytes.Equal([:6], pasteEnd) {returnkeyPasteEnd, [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 , := range [0:] {if >= 'a' && <= 'z' || >= 'A' && <= 'Z' || == '~' {returnkeyUnknown, [+1:] } }returnutf8.RuneError, }// queue appends data to the end of t.outBuffunc ( *Terminal) ( []rune) { .outBuf = append(.outBuf, []byte(string())...)}var space = []rune{' '}func isPrintable( rune) bool { := >= 0xd800 && <= 0xdbffreturn >= 32 && !}// moveCursorToPos appends data to t.outBuf which will move the cursor to the// given, logical position in the text.func ( *Terminal) ( int) {if !.echo {return } := visualLength(.prompt) + := / .termWidth = % .termWidth := 0if < .cursorY { = .cursorY - } := 0if > .cursorY { = - .cursorY } := 0if < .cursorX { = .cursorX - } := 0if > .cursorX { = - .cursorX } .cursorX = .cursorY = .move(, , , )}func ( *Terminal) (, , , int) { := []rune{}// 1 unit up can be expressed as ^[[A or ^[A // 5 units up can be expressed as ^[[5Aif == 1 { = append(, keyEscape, '[', 'A') } elseif > 1 { = append(, keyEscape, '[') = append(, []rune(strconv.Itoa())...) = append(, 'A') }if == 1 { = append(, keyEscape, '[', 'B') } elseif > 1 { = append(, keyEscape, '[') = append(, []rune(strconv.Itoa())...) = append(, 'B') }if == 1 { = append(, keyEscape, '[', 'C') } elseif > 1 { = append(, keyEscape, '[') = append(, []rune(strconv.Itoa())...) = append(, 'C') }if == 1 { = append(, keyEscape, '[', 'D') } elseif > 1 { = append(, keyEscape, '[') = append(, []rune(strconv.Itoa())...) = append(, 'D') } .queue()}func ( *Terminal) () { := []rune{keyEscape, '[', 'K'} .queue()}const maxLineLength = 4096func ( *Terminal) ( []rune, int) {if .echo { .moveCursorToPos(0) .writeLine()for := len(); < len(.line); ++ { .writeLine(space) } .moveCursorToPos() } .line = .pos = }func ( *Terminal) ( int) { .cursorX += .cursorY += .cursorX / .termWidthif .cursorY > .maxLine { .maxLine = .cursorY } .cursorX = .cursorX % .termWidthif > 0 && .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. .outBuf = append(.outBuf, '\r', '\n') }}func ( *Terminal) ( int) {if == 0 {return }if .pos < { = .pos } .pos -= .moveCursorToPos(.pos)copy(.line[.pos:], .line[+.pos:]) .line = .line[:len(.line)-]if .echo { .writeLine(.line[.pos:])for := 0; < ; ++ { .queue(space) } .advanceCursor() .moveCursorToPos(.pos) }}// countToLeftWord returns then number of characters from the cursor to the// start of the previous word.func ( *Terminal) () int {if .pos == 0 {return0 } := .pos - 1for > 0 {if .line[] != ' ' {break } -- }for > 0 {if .line[] == ' ' { ++break } -- }return .pos - }// countToRightWord returns then number of characters from the cursor to the// start of the next word.func ( *Terminal) () int { := .posfor < len(.line) {if .line[] == ' ' {break } ++ }for < len(.line) {if .line[] != ' ' {break } ++ }return - .pos}// visualLength returns the number of visible glyphs in s.func visualLength( []rune) int { := false := 0for , := range {switch {case :if ( >= 'a' && <= 'z') || ( >= 'A' && <= 'Z') { = false }case == '\x1b': = truedefault: ++ } }return}// histroryAt unlocks the terminal and relocks it while calling History.At.func ( *Terminal) ( int) (string, bool) { .lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.defer .lock.Lock() // panic in At (or Len) protection.if < 0 || >= .History.Len() {return"", false }return .History.At(), true}// historyAdd unlocks the terminal and relocks it while calling History.Add.func ( *Terminal) ( string) { .lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.defer .lock.Lock() // panic in Add protection. .History.Add()}// handleKey processes the given key and, optionally, returns a line of text// that the user has entered.func ( *Terminal) ( rune) ( string, bool) {if .pasteActive && != keyEnter && != keyLF { .addKeyToLine()return }switch {casekeyBackspace:if .pos == 0 {return } .eraseNPreviousChars(1)casekeyAltLeft:// move left by a word. .pos -= .countToLeftWord() .moveCursorToPos(.pos)casekeyAltRight:// move right by a word. .pos += .countToRightWord() .moveCursorToPos(.pos)casekeyLeft:if .pos == 0 {return } .pos-- .moveCursorToPos(.pos)casekeyRight:if .pos == len(.line) {return } .pos++ .moveCursorToPos(.pos)casekeyHome:if .pos == 0 {return } .pos = 0 .moveCursorToPos(.pos)casekeyEnd:if .pos == len(.line) {return } .pos = len(.line) .moveCursorToPos(.pos)casekeyUp: , := .historyAt(.historyIndex + 1)if ! {return"", false }if .historyIndex == -1 { .historyPending = string(.line) } .historyIndex++ := []rune() .setLine(, len())casekeyDown:switch .historyIndex {case -1:returncase0: := []rune(.historyPending) .setLine(, len()) .historyIndex--default: , := .historyAt(.historyIndex - 1)if { .historyIndex-- := []rune() .setLine(, len()) } }casekeyEnter, keyLF: .moveCursorToPos(len(.line)) .queue([]rune("\r\n")) = string(.line) = true .line = .line[:0] .pos = 0 .cursorX = 0 .cursorY = 0 .maxLine = 0casekeyDeleteWord:// Delete zero or more spaces and then one or more characters. .eraseNPreviousChars(.countToLeftWord())casekeyDeleteLine:// Delete everything from the current cursor position to the // end of line.for := .pos; < len(.line); ++ { .queue(space) .advanceCursor(1) } .line = .line[:.pos] .moveCursorToPos(.pos)casekeyCtrlD:// Erase the character under the current position. // The EOF case when the line is empty is handled in // readLine().if .pos < len(.line) { .pos++ .eraseNPreviousChars(1) }casekeyCtrlU: .eraseNPreviousChars(.pos)casekeyClearScreen:// Erases the screen and moves the cursor to the home position. .queue([]rune("\x1b[2J\x1b[H")) .queue(.prompt) .cursorX, .cursorY = 0, 0 .advanceCursor(visualLength(.prompt)) .setLine(.line, .pos)default:if .AutoCompleteCallback != nil { := string(.line[:.pos]) := string(.line[.pos:]) .lock.Unlock() , , := .AutoCompleteCallback(+, len(), ) .lock.Lock()if { .setLine([]rune(), utf8.RuneCount([]byte()[:]))return } }if !isPrintable() {return }iflen(.line) == maxLineLength {return } .addKeyToLine() }return}// addKeyToLine inserts the given key at the current position in the current// line.func ( *Terminal) ( rune) {iflen(.line) == cap(.line) { := make([]rune, len(.line), 2*(1+len(.line)))copy(, .line) .line = } .line = .line[:len(.line)+1]copy(.line[.pos+1:], .line[.pos:]) .line[.pos] = if .echo { .writeLine(.line[.pos:]) } .pos++ .moveCursorToPos(.pos)}func ( *Terminal) ( []rune) {forlen() != 0 { := .termWidth - .cursorX := len()if > { = } .queue([:]) .advanceCursor(visualLength([:])) = [:] }}// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n.func writeWithCRLF( io.Writer, []byte) ( int, error) {forlen() > 0 { := bytes.IndexByte(, '\n') := len()if >= 0 { = }varint , = .Write([:]) += if != nil {return , } = [:]if >= 0 {if _, = .Write(crlf); != nil {return , } ++ = [1:] } }return , nil}func ( *Terminal) ( []byte) ( int, error) { .lock.Lock()defer .lock.Unlock()if .cursorX == 0 && .cursorY == 0 {// This is the easy case: there's nothing on the screen that we // have to move out of the way.returnwriteWithCRLF(.c, ) }// We have a prompt and possibly user input on the screen. We // have to clear it first. .move(0/* up */, 0/* down */, .cursorX/* left */, 0/* right */) .cursorX = 0 .clearLineToRight()for .cursorY > 0 { .move(1/* up */, 0, 0, 0) .cursorY-- .clearLineToRight() }if _, = .c.Write(.outBuf); != nil {return } .outBuf = .outBuf[:0]if , = writeWithCRLF(.c, ); != nil {return } .writeLine(.prompt)if .echo { .writeLine(.line) } .moveCursorToPos(.pos)if _, = .c.Write(.outBuf); != nil {return } .outBuf = .outBuf[:0]return}// ReadPassword temporarily changes the prompt and reads a password, without// echo, from the terminal.//// The AutoCompleteCallback is disabled during this call.func ( *Terminal) ( string) ( string, error) { .lock.Lock()defer .lock.Unlock() := .prompt .prompt = []rune() .echo = false := .AutoCompleteCallback .AutoCompleteCallback = nildeferfunc() { .AutoCompleteCallback = }() , = .readLine() .prompt = .echo = truereturn}// ReadLine returns a line of input from the terminal.func ( *Terminal) () ( string, error) { .lock.Lock()defer .lock.Unlock()return .readLine()}func ( *Terminal) () ( string, error) {// t.lock must be held at this pointif .cursorX == 0 && .cursorY == 0 { .writeLine(.prompt) .c.Write(.outBuf) .outBuf = .outBuf[:0] } := .pasteActivefor { := .remainder := falsefor ! {varrune , = bytesToKey(, .pasteActive)if == utf8.RuneError {break }if !.pasteActive {if == keyCtrlD {iflen(.line) == 0 {return"", io.EOF } }if == keyCtrlC {return"", io.EOF }if == keyPasteStart { .pasteActive = trueiflen(.line) == 0 { = true }continue } } elseif == keyPasteEnd { .pasteActive = falsecontinue }if !.pasteActive { = false }// If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line.if == keyEnter && len() > 0 && [0] == keyLF { = [1:] } , = .handleKey() }iflen() > 0 { := copy(.inBuf[:], ) .remainder = .inBuf[:] } else { .remainder = nil } .c.Write(.outBuf) .outBuf = .outBuf[:0]if {if .echo { .historyIndex = -1 .historyAdd() }if { = ErrPasteIndicator }return }// t.remainder is a slice at the beginning of t.inBuf // containing a partial key sequence := .inBuf[len(.remainder):]varint .lock.Unlock() , = .c.Read() .lock.Lock()if != nil {return } .remainder = .inBuf[:+len(.remainder)] }}// SetPrompt sets the prompt to be used when reading subsequent lines.func ( *Terminal) ( string) { .lock.Lock()defer .lock.Unlock() .prompt = []rune()}func ( *Terminal) ( int) {// Move cursor to column zero at the start of the line. .move(.cursorY, 0, .cursorX, 0) .cursorX, .cursorY = 0, 0 .clearLineToRight()for .cursorY < {// Move down a line .move(0, 1, 0, 0) .cursorY++ .clearLineToRight() }// Move back to beginning. .move(.cursorY, 0, 0, 0) .cursorX, .cursorY = 0, 0 .queue(.prompt) .advanceCursor(visualLength(.prompt)) .writeLine(.line) .moveCursorToPos(.pos)}func ( *Terminal) (, int) error { .lock.Lock()defer .lock.Unlock()if == 0 { = 1 } := .termWidth .termWidth, .termHeight = , switch {case == :// If the width didn't change then nothing else needs to be // done.returnnilcaselen(.line) == 0 && .cursorX == 0 && .cursorY == 0:// If there is nothing on current line and no prompt printed, // just do nothingreturnnilcase < :// 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 .cursorX >= .termWidth { .cursorX = .termWidth - 1 } .cursorY *= 2 .clearAndRepaintLinePlusNPrevious(.maxLine * 2)case > :// 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. .clearAndRepaintLinePlusNPrevious(.maxLine) } , := .c.Write(.outBuf) .outBuf = .outBuf[:0]return}type pasteIndicatorError struct{}func (pasteIndicatorError) () 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.varErrPasteIndicator = 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 ( *Terminal) ( bool) {if {io.WriteString(.c, "\x1b[?2004h") } else {io.WriteString(.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 ( *stRingBuffer) ( string) {if .entries == nil {const = 100 .entries = make([]string, ) .max = } .head = (.head + 1) % .max .entries[.head] = if .size < .max { .size++ }}func ( *stRingBuffer) () int {return .size}// At 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 ( *stRingBuffer) ( int) string {if < 0 || >= .size {panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", , .size)) } := .head - if < 0 { += .max }return .entries[]}// readPasswordLine reads from reader until it finds \n or io.EOF.// The slice returned does not include the \n.// readPasswordLine also ignores any \r it finds.// Windows uses \r as end of line. So, on Windows, readPasswordLine// reads until it finds \r and ignores any \n it finds during processing.func readPasswordLine( io.Reader) ([]byte, error) {var [1]bytevar []bytefor { , := .Read([:])if > 0 {switch [0] {case'\b':iflen() > 0 { = [:len()-1] }case'\n':ifruntime.GOOS != "windows" {return , nil }// otherwise ignore \ncase'\r':ifruntime.GOOS == "windows" {return , nil }// otherwise ignore \rdefault: = append(, [0]) }continue }if != nil {if == io.EOF && len() > 0 {return , nil }return , } }}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.