Skip to content

Commit

Permalink
add termutil package
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Castillo <[email protected]>
  • Loading branch information
peterctl committed Feb 26, 2019
1 parent 696c82b commit e259f03
Show file tree
Hide file tree
Showing 13 changed files with 815 additions and 1 deletion.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/google/go-cmp v0.2.0
github.com/moby/moby v1.13.1
github.com/pkg/errors v0.8.0 // indirect
github.com/sirupsen/logrus v1.1.0
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4
golang.org/x/net v0.0.0-20181003013248-f5e5bdd77824
golang.org/x/sys v0.0.0-20181003145944-af653ce8b74f // indirect
golang.org/x/sys v0.0.0-20181003145944-af653ce8b74f
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8
gopkg.in/yaml.v2 v2.2.1
gotest.tools v2.2.0+incompatible
)
66 changes: 66 additions & 0 deletions pkg/termutil/ascii.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package termutil

import (
"fmt"
"strings"
)

// ASCII list the possible supported ASCII key sequence
var ASCII = []string{
"ctrl-@",
"ctrl-a",
"ctrl-b",
"ctrl-c",
"ctrl-d",
"ctrl-e",
"ctrl-f",
"ctrl-g",
"ctrl-h",
"ctrl-i",
"ctrl-j",
"ctrl-k",
"ctrl-l",
"ctrl-m",
"ctrl-n",
"ctrl-o",
"ctrl-p",
"ctrl-q",
"ctrl-r",
"ctrl-s",
"ctrl-t",
"ctrl-u",
"ctrl-v",
"ctrl-w",
"ctrl-x",
"ctrl-y",
"ctrl-z",
"ctrl-[",
"ctrl-\\",
"ctrl-]",
"ctrl-^",
"ctrl-_",
}

// ToBytes converts a string representing a suite of key-sequence to the corresponding ASCII code.
func ToBytes(keys string) ([]byte, error) {
codes := []byte{}
next:
for _, key := range strings.Split(keys, ",") {
if len(key) != 1 {
for code, ctrl := range ASCII {
if ctrl == key {
codes = append(codes, byte(code))
continue next
}
}
if key == "DEL" {
codes = append(codes, 127)
} else {
return nil, fmt.Errorf("Unknown character: '%s'", key)
}
} else {
codes = append(codes, key[0])
}
}
return codes, nil
}
25 changes: 25 additions & 0 deletions pkg/termutil/ascii_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package termutil

import (
"testing"

"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)

func TestToBytes(t *testing.T) {
codes, err := ToBytes("ctrl-a,a")
assert.NilError(t, err)
assert.Check(t, is.DeepEqual([]byte{1, 97}, codes))

_, err = ToBytes("shift-z")
assert.Check(t, is.ErrorContains(err, ""))

codes, err = ToBytes("ctrl-@,ctrl-[,~,ctrl-o")
assert.NilError(t, err)
assert.Check(t, is.DeepEqual([]byte{0, 27, 126, 15}, codes))

codes, err = ToBytes("DEL,+")
assert.NilError(t, err)
assert.Check(t, is.DeepEqual([]byte{127, 43}, codes))
}
78 changes: 78 additions & 0 deletions pkg/termutil/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package termutil

import (
"io"
)

// EscapeError is special error which returned by a TTY proxy reader's Read()
// method in case its detach escape sequence is read.
type EscapeError struct{}

func (EscapeError) Error() string {
return "read escape sequence"
}

// escapeProxy is used only for attaches with a TTY. It is used to proxy
// stdin keypresses from the underlying reader and look for the passed in
// escape key sequence to signal a detach.
type escapeProxy struct {
escapeKeys []byte
escapeKeyPos int
r io.Reader
}

// NewEscapeProxy returns a new TTY proxy reader which wraps the given reader
// and detects when the specified escape keys are read, in which case the Read
// method will return an error of type EscapeError.
func NewEscapeProxy(r io.Reader, escapeKeys []byte) io.Reader {
return &escapeProxy{
escapeKeys: escapeKeys,
r: r,
}
}

func (r *escapeProxy) Read(buf []byte) (int, error) {
nr, err := r.r.Read(buf)

if len(r.escapeKeys) == 0 {
return nr, err
}

preserve := func() {
// this preserves the original key presses in the passed in buffer
nr += r.escapeKeyPos
preserve := make([]byte, 0, r.escapeKeyPos+len(buf))
preserve = append(preserve, r.escapeKeys[:r.escapeKeyPos]...)
preserve = append(preserve, buf...)
r.escapeKeyPos = 0
copy(buf[0:nr], preserve)
}

if nr != 1 || err != nil {
if r.escapeKeyPos > 0 {
preserve()
}
return nr, err
}

if buf[0] != r.escapeKeys[r.escapeKeyPos] {
if r.escapeKeyPos > 0 {
preserve()
}
return nr, nil
}

if r.escapeKeyPos == len(r.escapeKeys)-1 {
return 0, EscapeError{}
}

// Looks like we've got an escape key, but we need to match again on the next
// read.
// Store the current escape key we found so we can look for the next one on
// the next read.
// Since this is an escape key, make sure we don't let the caller read it
// If later on we find that this is not the escape sequence, we'll add the
// keys back
r.escapeKeyPos++
return nr - r.escapeKeyPos, nil
}
115 changes: 115 additions & 0 deletions pkg/termutil/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package termutil

import (
"bytes"
"fmt"
"testing"

"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)

func TestEscapeProxyRead(t *testing.T) {
escapeKeys, _ := ToBytes("")
keys, _ := ToBytes("a")
reader := NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf := make([]byte, len(keys))
nr, err := reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, len(keys), fmt.Sprintf("nr %d should be equal to the number of %d", nr, len(keys)))
assert.DeepEqual(t, keys, buf)

keys, _ = ToBytes("a,b,c")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, len(keys), fmt.Sprintf("nr %d should be equal to the number of %d", nr, len(keys)))
assert.DeepEqual(t, keys, buf)

keys, _ = ToBytes("")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.Assert(t, is.ErrorContains(err, ""), "Should throw error when no keys are to read")
assert.Equal(t, nr, 0, "nr should be zero")
assert.Check(t, is.Len(keys, 0))
assert.Check(t, is.Len(buf, 0))

escapeKeys, _ = ToBytes("DEL")
keys, _ = ToBytes("a,b,c,+")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, len(keys), fmt.Sprintf("nr %d should be equal to the number of %d", nr, len(keys)))
assert.DeepEqual(t, keys, buf)

keys, _ = ToBytes("")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.Assert(t, is.ErrorContains(err, ""), "Should throw error when no keys are to read")
assert.Equal(t, nr, 0, "nr should be zero")
assert.Check(t, is.Len(keys, 0))
assert.Check(t, is.Len(buf, 0))

escapeKeys, _ = ToBytes("ctrl-x,ctrl-@")
keys, _ = ToBytes("DEL")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, 1, fmt.Sprintf("nr %d should be equal to the number of 1", nr))
assert.DeepEqual(t, keys, buf)

escapeKeys, _ = ToBytes("ctrl-c")
keys, _ = ToBytes("ctrl-c")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.Error(t, err, "read escape sequence")
assert.Equal(t, nr, 0, "nr should be equal to 0")
assert.DeepEqual(t, keys, buf)

escapeKeys, _ = ToBytes("ctrl-c,ctrl-z")
keys, _ = ToBytes("ctrl-c,ctrl-z")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, 1)
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, 0, "nr should be equal to 0")
assert.DeepEqual(t, keys[0:1], buf)
nr, err = reader.Read(buf)
assert.Error(t, err, "read escape sequence")
assert.Equal(t, nr, 0, "nr should be equal to 0")
assert.DeepEqual(t, keys[1:], buf)

escapeKeys, _ = ToBytes("ctrl-c,ctrl-z")
keys, _ = ToBytes("ctrl-c,DEL,+")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, 1)
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, 0, "nr should be equal to 0")
assert.DeepEqual(t, keys[0:1], buf)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, len(keys), fmt.Sprintf("nr should be equal to %d", len(keys)))
assert.DeepEqual(t, keys, buf)

escapeKeys, _ = ToBytes("ctrl-c,ctrl-z")
keys, _ = ToBytes("ctrl-c,DEL")
reader = NewEscapeProxy(bytes.NewReader(keys), escapeKeys)
buf = make([]byte, 1)
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, 0, "nr should be equal to 0")
assert.DeepEqual(t, keys[0:1], buf)
buf = make([]byte, len(keys))
nr, err = reader.Read(buf)
assert.NilError(t, err)
assert.Equal(t, nr, len(keys), fmt.Sprintf("nr should be equal to %d", len(keys)))
assert.DeepEqual(t, keys, buf)
}
47 changes: 47 additions & 0 deletions pkg/termutil/std_terminal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package termutil

import (
"os"
)

var stdTerm = NewTerminal(os.Stdin, os.Stdout, os.Stderr)

func StdTerminal() *Terminal {
return stdTerm
}

func In() *os.File {
return StdTerminal().In()
}

func Out() *os.File {
return StdTerminal().Out()
}

func Err() *os.File {
return StdTerminal().Err()
}

func IsTTY() bool {
return StdTerminal().IsTTY()
}

func MakeRaw() error {
return StdTerminal().MakeRaw()
}

func Restore() error {
return StdTerminal().Restore()
}

func GetWinsize() (*Winsize, error) {
return StdTerminal().GetWinsize()
}

func SetWinsize(ws *Winsize) error {
return StdTerminal().SetWinsize(ws)
}

func GetState() (*Termios, error) {
return StdTerminal().GetState()
}
20 changes: 20 additions & 0 deletions pkg/termutil/tc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// +build !windows

package termutil

import (
"syscall"
"unsafe"

"golang.org/x/sys/unix"
)

func tcget(fd uintptr, p *Termios) syscall.Errno {
_, _, err := unix.Syscall(unix.SYS_IOCTL, fd, uintptr(getTermiosOp), uintptr(unsafe.Pointer(p)))
return err
}

func tcset(fd uintptr, p *Termios) syscall.Errno {
_, _, err := unix.Syscall(unix.SYS_IOCTL, fd, setTermiosOp, uintptr(unsafe.Pointer(p)))
return err
}
Loading

0 comments on commit e259f03

Please sign in to comment.