From 43463fdc0c94a724b3e7d0c09d761064a9187246 Mon Sep 17 00:00:00 2001 From: carmark Date: Sun, 26 Apr 2015 14:20:31 +0800 Subject: [PATCH] add the terminal module which is copied from code.google.com/golang/ssh/terminal --- terminal/terminal.go | 731 ++++++++++++++++++++++++++++++ terminal/terminal_test.go | 110 +++++ terminal/termios.go | 40 ++ terminal/termios_darwin_amd64.go | 41 ++ terminal/termios_freebsd_amd64.go | 40 ++ terminal/termios_linux_amd64.go | 42 ++ terminal/util.go | 115 +++++ 7 files changed, 1119 insertions(+) create mode 100644 terminal/terminal.go create mode 100644 terminal/terminal_test.go create mode 100644 terminal/termios.go create mode 100644 terminal/termios_darwin_amd64.go create mode 100644 terminal/termios_freebsd_amd64.go create mode 100644 terminal/termios_linux_amd64.go create mode 100644 terminal/util.go diff --git a/terminal/terminal.go b/terminal/terminal.go new file mode 100644 index 0000000..b99d5a0 --- /dev/null +++ b/terminal/terminal.go @@ -0,0 +1,731 @@ +// 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 ( + "fmt" + "io" + "os" + "sync" +) + +func max(i, j int) int { + if i > j { + return i + } + return j +} + +func min(i, j int) int { + if i < j { + return i + } + return j +} + +// historyIdxValue returns an index into a valid range of history +func historyIdxValue(idx int, history [][]byte) int { + out := idx + out = min(len(history), out) + out = max(0, out) + return out +} + +// 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 + + // line is the current line being entered. + line []byte + // history is a buffer of previously entered lines + history [][]byte + // index into the history buffer (for use in the handleKey(KeyUp) function) + historyIdx int + // 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 + // 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 +} + +// 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: prompt, + history: make([][]byte, 0, 100), + historyIdx: -1, + termWidth: 80, + termHeight: 24, + echo: true, + } +} + +const ( + KeyCtrlC = 3 + KeyCtrlD = 4 + KeyEnter = '\r' + KeyEscape = 27 + KeyBackspace = 127 + KeyUnknown = 256 + iota + KeyLeft + KeyUp + KeyRight + KeyDown + KeyAltLeft + KeyAltRight +) + +// bytesToKey tries to parse a key sequence from b. If successful, it returns +// the key and the remainder of the input. Otherwise it returns -1. +func bytesToKey(b []byte) (int, []byte) { + if len(b) == 0 { + return -1, nil + } + + if b[0] != KeyEscape { + return int(b[0]), b[1:] + } + + if 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:] + } + } + + if 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 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' { + return KeyUnknown, b[i+1:] + } + } + + return -1, b +} + +// queue appends data to the end of t.outBuf +func (t *Terminal) queue(data []byte) { + t.outBuf = append(t.outBuf, data...) +} + +var eraseUnderCursor = []byte{' ', KeyEscape, '[', 'D'} +var space = []byte{' '} + +func isPrintable(key int) bool { + return key >= 32 && key < 127 +} + +// 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 + + 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([]byte, 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 := []byte{KeyEscape, '[', 'K'} + t.queue(op) +} + +const maxLineLength = 4096 + +// handleKey processes the given key and, optionally, returns a line of text +// that the user has entered. +func (t *Terminal) handleKey(key int) (line string, ok bool) { + switch key { + case KeyBackspace: + if t.pos == 0 { + 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.queue(eraseUnderCursor) + t.moveCursorToPos(t.pos) + case KeyAltLeft: + // move left by a word. + if t.pos == 0 { + return + } + t.pos-- + for t.pos > 0 { + if t.line[t.pos] != ' ' { + break + } + t.pos-- + } + for t.pos > 0 { + if t.line[t.pos] == ' ' { + t.pos++ + break + } + t.pos-- + } + t.moveCursorToPos(t.pos) + case KeyAltRight: + // move right by a word. + for t.pos < len(t.line) { + if t.line[t.pos] == ' ' { + break + } + t.pos++ + } + for t.pos < len(t.line) { + if t.line[t.pos] != ' ' { + break + } + t.pos++ + } + 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 KeyUp: + if len(t.history) == 0 { + return + } + t.historyIdx-- + t.historyIdx = historyIdxValue(t.historyIdx, t.history) + + h := t.history[t.historyIdx] + newLine := make([]byte, len(h)) + copy(newLine, h) + newPos := len(newLine) + 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 + + case KeyDown: + if len(t.history) == 0 { + return + } + newPos := 0 + newLine := []byte{} + t.historyIdx++ + if t.historyIdx >= len(t.history) { + t.historyIdx = len(t.history) + } else { + t.historyIdx = historyIdxValue(t.historyIdx, t.history) + h := t.history[t.historyIdx] + newLine = make([]byte, len(h)) + copy(newLine, h) + newPos = len(newLine) + // fmt.Println("in") + } + 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 + + case KeyEnter: + t.moveCursorToPos(len(t.line)) + t.queue([]byte("\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 + t.historyIdx = len(t.history) + 1 + case KeyCtrlD: + // add 'exit' to the end of the line + ok = true + if len(t.line) == 0 { + if len(t.line) == maxLineLength { + return + } + if len(t.line) == cap(t.line) { + newLine := make([]byte, len(t.line), 2*(2+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+4] + copy(t.line[t.pos+4:], t.line[t.pos:]) + t.line[t.pos] = byte('e') + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + t.line[t.pos] = byte('x') + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + t.line[t.pos] = byte('i') + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + t.line[t.pos] = byte('t') + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + } + case KeyCtrlC: + // add '^C' to the end of the line + if len(t.line) == maxLineLength { + return + } + newLine := make([]byte, len(t.line), 2*(2+len(t.line))) + copy(newLine, t.line) + t.line = newLine + t.line = t.line[:len(t.line)+3] + copy(t.line[t.pos+3:], t.line[t.pos:]) + t.line[t.pos] = byte('^') + t.pos++ + t.line[t.pos] = byte('C') + if t.echo { + t.writeLine(t.line[t.pos-1:]) + } + t.pos ++ + t.moveCursorToPos(t.pos) + t.queue([]byte("\r\n")) + t.line = make([]byte, 0, 0) + t.pos = 0 + t.cursorX = 0 + t.cursorY = 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 + } + if len(t.line) == maxLineLength { + return + } + if len(t.line) == cap(t.line) { + newLine := make([]byte, 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] = byte(key) + if t.echo { + t.writeLine(t.line[t.pos:]) + } + t.pos++ + t.moveCursorToPos(t.pos) + } + return +} + +func (t *Terminal) writeLine(line []byte) { + for len(line) != 0 { + remainingOnLine := t.termWidth - t.cursorX + todo := len(line) + if todo > remainingOnLine { + todo = remainingOnLine + } + 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) { + 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] + } + + for { + rest := t.remainder + lineOk := false + for !lineOk { + var key int + key, rest = bytesToKey(rest) + if key < 0 { + break + } + + line, lineOk = t.handleKey(key) + if key == KeyCtrlD && lineOk == true { + return "", io.EOF + } + if key == KeyCtrlC { + t.remainder = nil + return "^C", fmt.Errorf("control-c break") + } + } + 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 { //&& len(line) > 0 { + // don't put passwords into history... + b := []byte(line) + h := make([]byte, len(b)) + copy(h, b) + t.history = append(t.history, h) + } + 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 "", err + } + + t.remainder = t.inBuf[:n+len(t.remainder)] + } + panic("unreachable") +} + +// 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 = prompt +} + +func (t *Terminal) SetSize(width, height int) { + t.lock.Lock() + defer t.lock.Unlock() + + t.termWidth, t.termHeight = width, height +} + +func (t *Terminal) SetHistory(h []string) { + // t.history = make([][]byte, len(h)) + // for i := range h { + // t.history[i] = []byte(h[i]) + // } + // // t.historyIdx = len(h) +} + +func (t *Terminal) GetHistory() (h []string) { + // h = make([]string, len(t.history)) + // for i := range t.history { + // h[i] = string(t.history[i]) + // } + return +} + +type shell struct { + r io.Reader + w io.Writer +} + +func (sh *shell) Read(data []byte) (n int, err error) { + return sh.r.Read(data) +} + +func (sh *shell) Write(data []byte) (n int, err error) { + return sh.w.Write(data) +} + +var oldState *State + +func (t *Terminal) ReleaseFromStdInOut() { // doesn't really need a receiver, but maybe oldState can be part of term one day + fd := int(os.Stdin.Fd()) + Restore(fd, oldState) +} + +func NewWithStdInOut() (term *Terminal, err error) { + fd := int(os.Stdin.Fd()) + oldState, err = MakeRaw(fd) + if err != nil { + panic(err) + } + sh := &shell{r: os.Stdin, w: os.Stdout} + term = NewTerminal(sh, "") + return +} diff --git a/terminal/terminal_test.go b/terminal/terminal_test.go new file mode 100644 index 0000000..a219721 --- /dev/null +++ b/terminal/terminal_test.go @@ -0,0 +1,110 @@ +// 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 ( + "io" + "testing" +) + +type MockTerminal struct { + toSend []byte + bytesPerRead int + received []byte +} + +func (c *MockTerminal) Read(data []byte) (n int, err error) { + n = len(data) + if n == 0 { + return + } + if n > len(c.toSend) { + n = len(c.toSend) + } + if n == 0 { + return 0, io.EOF + } + if c.bytesPerRead > 0 && n > c.bytesPerRead { + n = c.bytesPerRead + } + copy(data, c.toSend[:n]) + c.toSend = c.toSend[n:] + return +} + +func (c *MockTerminal) Write(data []byte) (n int, err error) { + c.received = append(c.received, data...) + return len(data), nil +} + +func TestClose(t *testing.T) { + c := &MockTerminal{} + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if line != "" { + t.Errorf("Expected empty line but got: %s", line) + } + if err != io.EOF { + t.Errorf("Error should have been EOF but got: %s", err) + } +} + +var keyPressTests = []struct { + in string + line string + err error +}{ + { + "", + "", + io.EOF, + }, + { + "\r", + "", + nil, + }, + { + "foo\r", + "foo", + nil, + }, + { + "a\x1b[Cb\r", // right + "ab", + nil, + }, + { + "a\x1b[Db\r", // left + "ba", + nil, + }, + { + "a\177b\r", // backspace + "b", + nil, + }, +} + +func TestKeyPresses(t *testing.T) { + for i, test := range keyPressTests { + for j := 0; j < len(test.in); j++ { + c := &MockTerminal{ + toSend: []byte(test.in), + bytesPerRead: j, + } + ss := NewTerminal(c, "> ") + line, err := ss.ReadLine() + if line != test.line { + t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) + break + } + if err != test.err { + t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) + break + } + } + } +} diff --git a/terminal/termios.go b/terminal/termios.go new file mode 100644 index 0000000..6ccf5eb --- /dev/null +++ b/terminal/termios.go @@ -0,0 +1,40 @@ +// +build ignore + +package terminal + +/* +#include +#include +*/ +import "C" + +type syscall_Termios C.struct_termios + +const ( + syscall_IGNBRK = C.IGNBRK + syscall_BRKINT = C.BRKINT + syscall_PARMRK = C.PARMRK + syscall_ISTRIP = C.ISTRIP + syscall_INLCR = C.INLCR + syscall_IGNCR = C.IGNCR + syscall_ICRNL = C.ICRNL + syscall_IXON = C.IXON + syscall_IXOFF = C.IXOFF + syscall_OPOST = C.OPOST + syscall_ECHO = C.ECHO + syscall_ECHONL = C.ECHONL + syscall_ICANON = C.ICANON + syscall_ISIG = C.ISIG + syscall_IEXTEN = C.IEXTEN + syscall_CSIZE = C.CSIZE + syscall_PARENB = C.PARENB + syscall_CS8 = C.CS8 + syscall_VMIN = C.VMIN + syscall_VTIME = C.VTIME + + // on darwin change these to (on *bsd too?): + // C.TIOCGETA + // C.TIOCSETA + syscall_TCGETS = C.TIOCGETA + syscall_TCSETS = C.TIOCSETA +) diff --git a/terminal/termios_darwin_amd64.go b/terminal/termios_darwin_amd64.go new file mode 100644 index 0000000..497fb1b --- /dev/null +++ b/terminal/termios_darwin_amd64.go @@ -0,0 +1,41 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs termios.go + +package terminal + +type syscall_Termios struct { + Iflag uint64 + Oflag uint64 + Cflag uint64 + Lflag uint64 + Cc [20]uint8 + Pad_cgo_0 [4]byte + Ispeed uint64 + Ospeed uint64 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_IXOFF = 0x400 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x40487413 + syscall_TCSETS = 0x80487414 +) diff --git a/terminal/termios_freebsd_amd64.go b/terminal/termios_freebsd_amd64.go new file mode 100644 index 0000000..f317859 --- /dev/null +++ b/terminal/termios_freebsd_amd64.go @@ -0,0 +1,40 @@ +// Created by cgo -godefs - DO NOT EDIT +// /usr/local/go/bin/cgo -godefs termios.go + +package terminal + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]uint8 + Ispeed uint32 + Ospeed uint32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_IXOFF = 0x400 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x402c7413 + syscall_TCSETS = 0x802c7414 +) diff --git a/terminal/termios_linux_amd64.go b/terminal/termios_linux_amd64.go new file mode 100644 index 0000000..6328a01 --- /dev/null +++ b/terminal/termios_linux_amd64.go @@ -0,0 +1,42 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs termios.go + +package terminal + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Line uint8 + Cc [32]uint8 + Pad_cgo_0 [3]byte + Ispeed uint32 + Ospeed uint32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x400 + syscall_IXOFF = 0x1000 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x40 + syscall_ICANON = 0x2 + syscall_ISIG = 0x1 + syscall_IEXTEN = 0x8000 + syscall_CSIZE = 0x30 + syscall_PARENB = 0x100 + syscall_CS8 = 0x30 + syscall_VMIN = 0x6 + syscall_VTIME = 0x5 + + syscall_TCGETS = 0x5401 + syscall_TCSETS = 0x5402 +) diff --git a/terminal/util.go b/terminal/util.go new file mode 100644 index 0000000..dc18e49 --- /dev/null +++ b/terminal/util.go @@ -0,0 +1,115 @@ +// 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. + +// +build linux darwin freebsd + +// Package terminal provides support functions for dealing with terminals, as +// commonly found on UNIX systems. +// +// Putting a terminal into raw mode is the most common requirement: +// +// oldState, err := terminal.MakeRaw(0) +// if err != nil { +// panic(err) +// } +// defer terminal.Restore(0, oldState) +package terminal + +import ( + "io" + "syscall" + "unsafe" +) + +// State contains the state of a terminal. +type State struct { + termios syscall_Termios +} + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd int) bool { + var termios syscall_Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} + +// MakeRaw put the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd int) (*State, error) { + var oldState State + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState.termios + newState.Iflag &^= syscall_ISTRIP | syscall_INLCR | syscall_ICRNL | syscall_IGNCR | syscall_IXON | syscall_IXOFF + newState.Lflag &^= syscall.ECHO | syscall_ICANON | syscall_ISIG + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + return &oldState, nil +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd int, state *State) error { + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) + 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. +func ReadPassword(fd int) ([]byte, error) { + var oldState syscall_Termios + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { + return nil, err + } + + newState := oldState + newState.Lflag &^= syscall.ECHO + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { + return nil, err + } + + defer func() { + syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) + }() + + var buf [16]byte + var ret []byte + for { + n, err := syscall.Read(fd, buf[:]) + if err != nil { + return nil, err + } + if n == 0 { + if len(ret) == 0 { + return nil, io.EOF + } + break + } + if buf[n-1] == '\n' { + n-- + } + ret = append(ret, buf[:n]...) + if n < len(buf) { + break + } + } + + return ret, nil +}