POC for odo interactive testing (#5466)

* POC for odo interactive testing

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* add interactive tests to make target test-integration

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* [WIP] use func to pass test instructions

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* cleanup comments/test

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* cleanup minor changes

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* fix unit test failure

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* skip for windows

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* skip for windows by not compiling, cleanup

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* cleanup, and add the make target to test files

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* incorporate review, adding comments, cleanup

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* fix failing make validate

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* update test dependency, and possible fix for windows

Signed-off-by: anandrkskd <anandrkskd@gmail.com>

* Revert "update test dependency, and possible fix for windows"

This reverts commit 55580b7dc5.

* Final cleanup

Signed-off-by: anandrkskd <anandrkskd@gmail.com>
This commit is contained in:
Anand Kumar Singh
2022-03-03 00:20:26 +05:30
committed by GitHub
parent fbdacb2dde
commit 3eb92b6a47
32 changed files with 712 additions and 3917 deletions

View File

@@ -14,6 +14,7 @@ cleanup_namespaces
set -e
make install
make test-integration-devfile
make test-integration-interactive
make test-e2e-devfile
make test-cmd-project
) |& tee "/tmp/${LOGFILE}"

View File

@@ -12,6 +12,7 @@ cleanup_namespaces
set -e
make install
make test-integration
make test-integration-interactive
make test-integration-devfile
make test-cmd-login-logout
make test-cmd-project

View File

@@ -266,6 +266,11 @@ test-cmd-debug: ## Run odo debug command tests
test-integration: ## Run command's integration tests irrespective of service catalog status in the cluster.
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/
## Run integration interactive tests
.PHONY: test-interactive test
test-integration-interactive:
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/interactive/
.PHONY: test-integration-devfile
test-integration-devfile: ## Run devfile integration tests
$(RUN_GINKGO) $(GINKGO_FLAGS) tests/integration/devfile/

4
go.mod
View File

@@ -18,7 +18,8 @@ require (
github.com/go-git/go-git/v5 v5.3.0
github.com/go-openapi/spec v0.19.5
github.com/golang/mock v1.5.0
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c
github.com/hinshun/vt10x v0.0.0-20220127042424-3ca73d0126d7
github.com/kr/pty v1.1.5
github.com/kubernetes-sigs/service-catalog v0.3.1
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-colorable v0.1.8
@@ -42,7 +43,6 @@ require (
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.9.3
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/zalando/go-keyring v0.1.1

4
go.sum
View File

@@ -642,8 +642,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20220127042424-3ca73d0126d7 h1:PoerlCqzob3t6b5/8mjCPkX4QSTYR4/+kB8IzqZE3ug=
github.com/hinshun/vt10x v0.0.0-20220127042424-3ca73d0126d7/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=

View File

@@ -1,81 +0,0 @@
package testingutil
import (
"bytes"
"testing"
"github.com/Netflix/go-expect"
"github.com/hinshun/vt10x"
"github.com/stretchr/testify/require"
"gopkg.in/AlecAivazis/survey.v1"
"gopkg.in/AlecAivazis/survey.v1/terminal"
)
// This whole file copies the testing infrastructure from survey lib since it cannot be imported. This mixes elements from:
// vendor/gopkg.in/AlecAivazis/survey.v1/survey_posix_test.go
// vendor/gopkg.in/AlecAivazis/survey.v1/survey_test.go
// vendor/gopkg.in/AlecAivazis/survey.v1/survey.go
type wantsStdio interface {
WithStdio(terminal.Stdio)
}
// Stdio converts an expect.Console into a survey terminal.Stdio
func Stdio(c *expect.Console) terminal.Stdio {
return terminal.Stdio{In: c.Tty(), Out: c.Tty(), Err: c.Tty()}
}
// PromptTest encapsulates a survey prompt test
type PromptTest struct {
// Name of the test
Name string
// Prompt to test
Prompt survey.Prompt
// Procedure defines the list of interaction with the console simulating user actions
Procedure func(*expect.Console)
// Expected result
Expected interface{}
}
// RunPromptTest runs the specified PromptTest in the given testing context
func RunPromptTest(t *testing.T, test PromptTest) {
var answer interface{}
RunTest(t, test.Procedure, func(stdio terminal.Stdio) error {
var err error
if p, ok := test.Prompt.(wantsStdio); ok {
p.WithStdio(stdio)
}
answer, err = test.Prompt.Prompt()
return err
})
require.Equal(t, test.Expected, answer)
}
// RunTest runs the given test using the specified procedure simulating the user interaction with the console
func RunTest(t *testing.T, procedure func(*expect.Console), test func(terminal.Stdio) error) {
t.Parallel()
// Multiplex output to a buffer as well for the raw bytes.
buf := new(bytes.Buffer)
c, state, err := vt10x.NewVT10XConsole(expect.WithStdout(buf))
require.Nil(t, err)
defer c.Close()
donec := make(chan struct{})
go func() {
defer close(donec)
procedure(c)
}()
err = test(Stdio(c))
require.Nil(t, err)
// Close the slave end of the pty, and read the remaining bytes from the master end.
c.Tty().Close()
<-donec
t.Logf("Raw output: %q", buf.String())
// Dump the terminal's screen.
t.Logf("\n%s", expect.StripTrailingEmptyLines(state.String()))
}

View File

@@ -29,6 +29,7 @@ oc whoami
# Integration tests
make test-integration || error=true
make test-integration-devfile || error=true
make test-integration-interactive || error=true
make test-cmd-login-logout || error=true
make test-cmd-project || error=true

View File

@@ -52,6 +52,7 @@ elif [ "${ARCH}" == "ppc64le" ]; then
else
# Integration tests
make test-integration || error=true
make test-integration-interactive || error=true
make test-integration-devfile || error=true
make test-cmd-login-logout || error=true
make test-cmd-project || error=true

View File

@@ -1,3 +1,4 @@
//go:build tools
// +build tools
package tools

View File

@@ -0,0 +1,66 @@
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
package helper
import (
"bytes"
"log"
"os/exec"
"github.com/Netflix/go-expect"
"github.com/hinshun/vt10x"
"github.com/kr/pty"
. "github.com/onsi/gomega"
)
// RunInteractive runs the command in interactive mode and returns the output, and error
// It takes command as array of strings, and a function `test` that contains steps to run the test as an argument
func RunInteractive(command []string, test func(*expect.Console, *bytes.Buffer)) (string, error) {
ptm, pts, err := pty.Open()
if err != nil {
log.Fatal(err)
}
term := vt10x.New(vt10x.WithWriter(pts))
c, err := expect.NewConsole(expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(pts, ptm))
if err != nil {
log.Fatal(err)
}
defer c.Close()
// execute the command
cmd := exec.Command(command[0], command[1:]...)
// setup stdin, stdout and stderr
cmd.Stdin = c.Tty()
cmd.Stdout = c.Tty()
cmd.Stderr = c.Tty()
err = cmd.Start()
if err != nil {
log.Fatal(err)
}
buf := new(bytes.Buffer)
test(c, buf)
err = cmd.Wait()
if err != nil {
log.Fatal(err)
}
// Close the slave end of the pty, and read the remaining bytes from the master end.
c.Tty().Close()
return buf.String(), err
}
func SendLine(c *expect.Console, line string) {
_, err := c.SendLine(line)
Expect(err).ShouldNot(HaveOccurred())
}
func ExpectString(c *expect.Console, line string) string {
res, err := c.ExpectString(line)
Expect(err).ShouldNot(HaveOccurred())
return res
}

View File

@@ -0,0 +1,62 @@
//go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
package interactive
import (
"bytes"
"fmt"
"github.com/Netflix/go-expect"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/tests/helper"
)
var _ = Describe("odo init interactive command tests", func() {
var commonVar helper.CommonVar
// This is run before every Spec (It)
var _ = BeforeEach(func() {
commonVar = helper.CommonBeforeEach()
helper.Chdir(commonVar.Context)
})
// Clean up after the test
// This is run after every Spec (It)
var _ = AfterEach(func() {
helper.CommonAfterEach(commonVar)
})
It("should download correct devfile", func() {
command := []string{"odo", "init"}
output, err := helper.RunInteractive(command, func(c *expect.Console, output *bytes.Buffer) {
res := helper.ExpectString(c, "Select language")
fmt.Fprintln(output, res)
helper.SendLine(c, "go")
res = helper.ExpectString(c, "Select project type")
fmt.Fprintln(output, res)
helper.SendLine(c, "\n")
res = helper.ExpectString(c, "Which starter project do you want to use")
fmt.Fprintln(output, res)
helper.SendLine(c, "\n")
res = helper.ExpectString(c, "Enter component name")
fmt.Fprintln(output, res)
helper.SendLine(c, "my-go-app")
res = helper.ExpectString(c, "Your new component \"my-go-app\" is ready in the current directory.")
fmt.Fprintln(output, res)
})
Expect(err).To(BeNil())
Expect(output).To(ContainSubstring("Your new component \"my-go-app\" is ready in the current directory."))
Expect(helper.ListFilesInDir(commonVar.Context)).To(ContainElements("devfile.yaml"))
})
})

View File

@@ -0,0 +1,15 @@
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
//go:build !race
// +build !race
package interactive
import (
"testing"
"github.com/redhat-developer/odo/tests/helper"
)
func TestInteractive(t *testing.T) {
helper.RunTestSpecs(t, "Interactive Suite")
}

View File

@@ -1,96 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Netflix/go-expect"
packages = ["."]
revision = "c93bf25de8e869da25cf26bcd2932b36141f61ae"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/gdamore/encoding"
packages = ["."]
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
[[projects]]
branch = "master"
name = "github.com/gdamore/tcell"
packages = [
".",
"terminfo"
]
revision = "b3cebc399d6f98536af845ed8a5144ab586f6759"
[[projects]]
name = "github.com/kr/pty"
packages = ["."]
revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
version = "v1.1.1"
[[projects]]
name = "github.com/lucasb-eyer/go-colorful"
packages = ["."]
revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
version = "v1.0"
[[projects]]
name = "github.com/mattn/go-runewidth"
packages = ["."]
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"require"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
[[projects]]
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/internal/identifier",
"internal/gen",
"transform",
"unicode/cldr"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "bd24f29ad753e2f861768c734d71fbbc4e0380b5d7b43b6b101c01274cabcedb"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,38 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/kr/pty"
version = "1.1.1"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@@ -24,12 +24,13 @@ const (
// For example, a transparent background. Otherwise, the simple case is to
// map default colors to another color.
const (
DefaultFG Color = 0xff80 + iota
DefaultFG Color = 1<<24 + iota
DefaultBG
DefaultCursor
)
// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256).
type Color uint16
type Color uint32
// ANSI returns true if Color is within [0, 16).
func (c Color) ANSI() bool {

View File

@@ -74,26 +74,26 @@ func (t *State) handleCSI() {
case '@': // ICH - insert <n> blank char
t.insertBlanks(c.arg(0, 1))
case 'A': // CUU - cursor <n> up
t.moveTo(t.cur.x, t.cur.y-c.maxarg(0, 1))
t.moveTo(t.cur.X, t.cur.Y-c.maxarg(0, 1))
case 'B', 'e': // CUD, VPR - cursor <n> down
t.moveTo(t.cur.x, t.cur.y+c.maxarg(0, 1))
t.moveTo(t.cur.X, t.cur.Y+c.maxarg(0, 1))
case 'c': // DA - device attributes
if c.arg(0, 0) == 0 {
// TODO: write vt102 id
}
case 'C', 'a': // CUF, HPR - cursor <n> forward
t.moveTo(t.cur.x+c.maxarg(0, 1), t.cur.y)
t.moveTo(t.cur.X+c.maxarg(0, 1), t.cur.Y)
case 'D': // CUB - cursor <n> backward
t.moveTo(t.cur.x-c.maxarg(0, 1), t.cur.y)
t.moveTo(t.cur.X-c.maxarg(0, 1), t.cur.Y)
case 'E': // CNL - cursor <n> down and first col
t.moveTo(0, t.cur.y+c.arg(0, 1))
t.moveTo(0, t.cur.Y+c.arg(0, 1))
case 'F': // CPL - cursor <n> up and first col
t.moveTo(0, t.cur.y-c.arg(0, 1))
t.moveTo(0, t.cur.Y-c.arg(0, 1))
case 'g': // TBC - tabulation clear
switch c.arg(0, 0) {
// clear current tab stop
case 0:
t.tabs[t.cur.x] = false
t.tabs[t.cur.X] = false
// clear all tabs
case 3:
for i := range t.tabs {
@@ -103,7 +103,7 @@ func (t *State) handleCSI() {
goto unknown
}
case 'G', '`': // CHA, HPA - Move to <col>
t.moveTo(c.arg(0, 1)-1, t.cur.y)
t.moveTo(c.arg(0, 1)-1, t.cur.Y)
case 'H', 'f': // CUP, HVP - move to <row> <col>
t.moveAbsTo(c.arg(1, 1)-1, c.arg(0, 1)-1)
case 'I': // CHT - cursor forward tabulation <n> tab stops
@@ -115,15 +115,15 @@ func (t *State) handleCSI() {
// TODO: sel.ob.x = -1
switch c.arg(0, 0) {
case 0: // below
t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
if t.cur.y < t.rows-1 {
t.clear(0, t.cur.y+1, t.cols-1, t.rows-1)
t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
if t.cur.Y < t.rows-1 {
t.clear(0, t.cur.Y+1, t.cols-1, t.rows-1)
}
case 1: // above
if t.cur.y > 1 {
t.clear(0, 0, t.cols-1, t.cur.y-1)
if t.cur.Y > 1 {
t.clear(0, 0, t.cols-1, t.cur.Y-1)
}
t.clear(0, t.cur.y, t.cur.x, t.cur.y)
t.clear(0, t.cur.Y, t.cur.X, t.cur.Y)
case 2: // all
t.clear(0, 0, t.cols-1, t.rows-1)
default:
@@ -132,11 +132,11 @@ func (t *State) handleCSI() {
case 'K': // EL - clear line
switch c.arg(0, 0) {
case 0: // right
t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
case 1: // left
t.clear(0, t.cur.y, t.cur.x, t.cur.y)
t.clear(0, t.cur.Y, t.cur.X, t.cur.Y)
case 2: // all
t.clear(0, t.cur.y, t.cols-1, t.cur.y)
t.clear(0, t.cur.Y, t.cols-1, t.cur.Y)
}
case 'S': // SU - scroll <n> lines up
t.scrollUp(t.top, c.arg(0, 1))
@@ -149,7 +149,7 @@ func (t *State) handleCSI() {
case 'M': // DL - delete <n> lines
t.deleteLines(c.arg(0, 1))
case 'X': // ECH - erase <n> chars
t.clear(t.cur.x, t.cur.y, t.cur.x+c.arg(0, 1)-1, t.cur.y)
t.clear(t.cur.X, t.cur.Y, t.cur.X+c.arg(0, 1)-1, t.cur.Y)
case 'P': // DCH - delete <n> chars
t.deleteChars(c.arg(0, 1))
case 'Z': // CBT - cursor backward tabulation <n> tab stops
@@ -158,7 +158,7 @@ func (t *State) handleCSI() {
t.putTab(false)
}
case 'd': // VPA - move to <row>
t.moveAbsTo(t.cur.x, c.arg(0, 1)-1)
t.moveAbsTo(t.cur.X, c.arg(0, 1)-1)
case 'h': // SM - set terminal mode
t.setMode(c.priv, true, c.args)
case 'm': // SGR - terminal attribute (color)
@@ -168,7 +168,7 @@ func (t *State) handleCSI() {
case 5: // DSR - device status report
t.w.Write([]byte("\033[0n"))
case 6: // CPR - cursor position report
t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.y+1, t.cur.x+1)))
t.w.Write([]byte(fmt.Sprintf("\033[%d;%dR", t.cur.Y+1, t.cur.X+1)))
}
case 'r': // DECSTBM - set scrolling region
if c.priv {

View File

@@ -1,29 +0,0 @@
package vt10x
import (
expect "github.com/Netflix/go-expect"
"github.com/kr/pty"
)
// NewVT10XConsole returns a new expect.Console that multiplexes the
// Stdin/Stdout to a VT10X terminal, allowing Console to interact with an
// application sending ANSI escape sequences.
func NewVT10XConsole(opts ...expect.ConsoleOpt) (*expect.Console, *State, error) {
ptm, pts, err := pty.Open()
if err != nil {
return nil, nil, err
}
var state State
term, err := Create(&state, pts)
if err != nil {
return nil, nil, err
}
c, err := expect.NewConsole(append(opts, expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(pts, ptm, term))...)
if err != nil {
return nil, nil, err
}
return c, &state, nil
}

3
vendor/github.com/hinshun/vt10x/go.mod generated vendored Normal file
View File

@@ -0,0 +1,3 @@
module github.com/hinshun/vt10x
go 1.14

View File

@@ -7,27 +7,27 @@ func isControlCode(c rune) bool {
func (t *State) parse(c rune) {
t.logf("%q", string(c))
if isControlCode(c) {
if t.handleControlCodes(c) || t.cur.attr.mode&attrGfx == 0 {
if t.handleControlCodes(c) || t.cur.Attr.Mode&attrGfx == 0 {
return
}
}
// TODO: update selection; see st.c:2450
if t.mode&ModeWrap != 0 && t.cur.state&cursorWrapNext != 0 {
t.lines[t.cur.y][t.cur.x].mode |= attrWrap
if t.mode&ModeWrap != 0 && t.cur.State&cursorWrapNext != 0 {
t.lines[t.cur.Y][t.cur.X].Mode |= attrWrap
t.newline(true)
}
if t.mode&ModeInsert != 0 && t.cur.x+1 < t.cols {
if t.mode&ModeInsert != 0 && t.cur.X+1 < t.cols {
// TODO: move shiz, look at st.c:2458
t.logln("insert mode not implemented")
}
t.setChar(c, &t.cur.attr, t.cur.x, t.cur.y)
if t.cur.x+1 < t.cols {
t.moveTo(t.cur.x+1, t.cur.y)
t.setChar(c, &t.cur.Attr, t.cur.X, t.cur.Y)
if t.cur.X+1 < t.cols {
t.moveTo(t.cur.X+1, t.cur.Y)
} else {
t.cur.state |= cursorWrapNext
t.cur.State |= cursorWrapNext
}
}
@@ -56,20 +56,20 @@ func (t *State) parseEsc(c rune) {
'*', // set tertiary charset G2 (ignored)
'+': // set quaternary charset G3 (ignored)
case 'D': // IND - linefeed
if t.cur.y == t.bottom {
if t.cur.Y == t.bottom {
t.scrollUp(t.top, 1)
} else {
t.moveTo(t.cur.x, t.cur.y+1)
t.moveTo(t.cur.X, t.cur.Y+1)
}
case 'E': // NEL - next line
t.newline(true)
case 'H': // HTS - horizontal tab stop
t.tabs[t.cur.x] = true
t.tabs[t.cur.X] = true
case 'M': // RI - reverse index
if t.cur.y == t.top {
if t.cur.Y == t.top {
t.scrollDown(t.top, 1)
} else {
t.moveTo(t.cur.x, t.cur.y-1)
t.moveTo(t.cur.X, t.cur.Y-1)
}
case 'Z': // DECID - identify terminal
// TODO: write to our writer our id
@@ -132,9 +132,9 @@ func (t *State) parseEscAltCharset(c rune) {
t.logf("%q", string(c))
switch c {
case '0': // line drawing set
t.cur.attr.mode |= attrGfx
t.cur.Attr.Mode |= attrGfx
case 'B': // USASCII
t.cur.attr.mode &^= attrGfx
t.cur.Attr.Mode &^= attrGfx
case 'A', // UK (ignored)
'<', // multinational (ignored)
'5', // Finnish (ignored)
@@ -154,7 +154,7 @@ func (t *State) parseEscTest(c rune) {
if c == '8' {
for y := 0; y < t.rows; y++ {
for x := 0; x < t.cols; x++ {
t.setChar('E', &t.cur.attr, x, y)
t.setChar('E', &t.cur.Attr, x, y)
}
}
}
@@ -171,10 +171,10 @@ func (t *State) handleControlCodes(c rune) bool {
t.putTab(true)
// BS
case '\b':
t.moveTo(t.cur.x-1, t.cur.y)
t.moveTo(t.cur.X-1, t.cur.Y)
// CR
case '\r':
t.moveTo(0, t.cur.y)
t.moveTo(0, t.cur.Y)
// LF, VT, LF
case '\f', '\v', '\n':
// go to first col if mode is set

View File

@@ -62,18 +62,18 @@ const (
ChangedTitle
)
type glyph struct {
c rune
mode int16
fg, bg Color
type Glyph struct {
Char rune
Mode int16
FG, BG Color
}
type line []glyph
type line []Glyph
type cursor struct {
attr glyph
x, y int
state uint8
type Cursor struct {
Attr Glyph
X, Y int
State uint8
}
type parseState func(c rune)
@@ -91,7 +91,7 @@ type State struct {
altLines []line
dirty []bool // line dirtiness
anydirty bool
cur, curSaved cursor
cur, curSaved Cursor
top, bottom int // scroll limits
mode ModeFlag
state parseState
@@ -100,6 +100,14 @@ type State struct {
numlock bool
tabs []bool
title string
colorOverride map[Color]Color
}
func newState(w io.Writer) *State {
return &State{
w: w,
colorOverride: make(map[Color]Color),
}
}
func (t *State) logf(format string, args ...interface{}) {
@@ -133,15 +141,24 @@ func (t *State) Unlock() {
t.mu.Unlock()
}
// Cell returns the character code, foreground color, and background
// color at position (x, y) relative to the top left of the terminal.
func (t *State) Cell(x, y int) (ch rune, fg Color, bg Color) {
return t.lines[y][x].c, Color(t.lines[y][x].fg), Color(t.lines[y][x].bg)
// Cell returns the glyph containing the character code, foreground color, and
// background color at position (x, y) relative to the top left of the terminal.
func (t *State) Cell(x, y int) Glyph {
cell := t.lines[y][x]
fg, ok := t.colorOverride[cell.FG]
if ok {
cell.FG = fg
}
bg, ok := t.colorOverride[cell.BG]
if ok {
cell.BG = bg
}
return cell
}
// Cursor returns the current position of the cursor.
func (t *State) Cursor() (int, int) {
return t.cur.x, t.cur.y
func (t *State) Cursor() Cursor {
return t.cur
}
// CursorVisible returns the visible state of the cursor.
@@ -149,9 +166,9 @@ func (t *State) CursorVisible() bool {
return t.mode&ModeHide == 0
}
// Mode tests if mode is currently set.
func (t *State) Mode(mode ModeFlag) bool {
return t.mode&mode != 0
// Mode returns the current terminal mode.
func (t *State) Mode() ModeFlag {
return t.mode
}
// Title returns the current title set via the tty.
@@ -186,7 +203,7 @@ func (t *State) saveCursor() {
func (t *State) restoreCursor() {
t.cur = t.curSaved
t.moveTo(t.cur.x, t.cur.y)
t.moveTo(t.cur.X, t.cur.Y)
}
func (t *State) put(c rune) {
@@ -194,7 +211,7 @@ func (t *State) put(c rune) {
}
func (t *State) putTab(forward bool) {
x := t.cur.x
x := t.cur.X
if forward {
if x == t.cols {
return
@@ -208,11 +225,11 @@ func (t *State) putTab(forward bool) {
for x--; x > 0 && !t.tabs[x]; x-- {
}
}
t.moveTo(x, t.cur.y)
t.moveTo(x, t.cur.Y)
}
func (t *State) newline(firstCol bool) {
y := t.cur.y
y := t.cur.Y
if y == t.bottom {
cur := t.cur
t.cur = t.defaultCursor()
@@ -224,7 +241,7 @@ func (t *State) newline(firstCol bool) {
if firstCol {
t.moveTo(0, y)
} else {
t.moveTo(t.cur.x, y)
t.moveTo(t.cur.X, y)
}
}
@@ -240,8 +257,8 @@ var gfxCharTable = [62]rune{
'│', '≤', '≥', 'π', '≠', '£', '·', // x - ~
}
func (t *State) setChar(c rune, attr *glyph, x, y int) {
if attr.mode&attrGfx != 0 {
func (t *State) setChar(c rune, attr *Glyph, x, y int) {
if attr.Mode&attrGfx != 0 {
if c >= 0x41 && c <= 0x7e && gfxCharTable[c-0x41] != 0 {
c = gfxCharTable[c-0x41]
}
@@ -249,21 +266,21 @@ func (t *State) setChar(c rune, attr *glyph, x, y int) {
t.changed |= ChangedScreen
t.dirty[y] = true
t.lines[y][x] = *attr
t.lines[y][x].c = c
//if t.options.BrightBold && attr.mode&attrBold != 0 && attr.fg < 8 {
if attr.mode&attrBold != 0 && attr.fg < 8 {
t.lines[y][x].fg = attr.fg + 8
t.lines[y][x].Char = c
//if t.options.BrightBold && attr.Mode&attrBold != 0 && attr.FG < 8 {
if attr.Mode&attrBold != 0 && attr.FG < 8 {
t.lines[y][x].FG = attr.FG + 8
}
if attr.mode&attrReverse != 0 {
t.lines[y][x].fg = attr.bg
t.lines[y][x].bg = attr.fg
if attr.Mode&attrReverse != 0 {
t.lines[y][x].FG = attr.BG
t.lines[y][x].BG = attr.FG
}
}
func (t *State) defaultCursor() cursor {
c := cursor{}
c.attr.fg = DefaultFG
c.attr.bg = DefaultBG
func (t *State) defaultCursor() Cursor {
c := Cursor{}
c.Attr.FG = DefaultFG
c.Attr.BG = DefaultBG
return c
}
@@ -291,7 +308,7 @@ func (t *State) resize(cols, rows int) bool {
if cols < 1 || rows < 1 {
return false
}
slide := t.cur.y - rows + 1
slide := t.cur.Y - rows + 1
if slide > 0 {
copy(t.lines, t.lines[slide:slide+rows])
copy(t.altLines, t.altLines[slide:slide+rows])
@@ -329,7 +346,7 @@ func (t *State) resize(cols, rows int) bool {
t.cols = cols
t.rows = rows
t.setScroll(0, rows-1)
t.moveTo(t.cur.x, t.cur.y)
t.moveTo(t.cur.X, t.cur.Y)
for i := 0; i < 2; i++ {
if mincols < cols && minrows > 0 {
t.clear(mincols, 0, cols-1, minrows-1)
@@ -357,8 +374,8 @@ func (t *State) clear(x0, y0, x1, y1 int) {
for y := y0; y <= y1; y++ {
t.dirty[y] = true
for x := x0; x <= x1; x++ {
t.lines[y][x] = t.cur.attr
t.lines[y][x].c = ' '
t.lines[y][x] = t.cur.Attr
t.lines[y][x].Char = ' '
}
}
}
@@ -368,7 +385,7 @@ func (t *State) clearAll() {
}
func (t *State) moveAbsTo(x, y int) {
if t.cur.state&cursorOrigin != 0 {
if t.cur.State&cursorOrigin != 0 {
y += t.top
}
t.moveTo(x, y)
@@ -376,7 +393,7 @@ func (t *State) moveAbsTo(x, y int) {
func (t *State) moveTo(x, y int) {
var miny, maxy int
if t.cur.state&cursorOrigin != 0 {
if t.cur.State&cursorOrigin != 0 {
miny = t.top
maxy = t.bottom
} else {
@@ -386,9 +403,9 @@ func (t *State) moveTo(x, y int) {
x = clamp(x, 0, t.cols-1)
y = clamp(y, miny, maxy)
t.changed |= ChangedScreen
t.cur.state &^= cursorWrapNext
t.cur.x = x
t.cur.y = y
t.cur.State &^= cursorWrapNext
t.cur.X = x
t.cur.Y = y
}
func (t *State) swapScreen() {
@@ -492,9 +509,9 @@ func (t *State) setMode(priv bool, set bool, args []int) {
}
case 6: // DECOM - origin
if set {
t.cur.state |= cursorOrigin
t.cur.State |= cursorOrigin
} else {
t.cur.state &^= cursorOrigin
t.cur.State &^= cursorOrigin
}
t.moveAbsTo(0, 0)
case 7: // DECAWM - auto wrap
@@ -594,64 +611,80 @@ func (t *State) setAttr(attr []int) {
a := attr[i]
switch a {
case 0:
t.cur.attr.mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink
t.cur.attr.fg = DefaultFG
t.cur.attr.bg = DefaultBG
t.cur.Attr.Mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink
t.cur.Attr.FG = DefaultFG
t.cur.Attr.BG = DefaultBG
case 1:
t.cur.attr.mode |= attrBold
t.cur.Attr.Mode |= attrBold
case 3:
t.cur.attr.mode |= attrItalic
t.cur.Attr.Mode |= attrItalic
case 4:
t.cur.attr.mode |= attrUnderline
t.cur.Attr.Mode |= attrUnderline
case 5, 6: // slow, rapid blink
t.cur.attr.mode |= attrBlink
t.cur.Attr.Mode |= attrBlink
case 7:
t.cur.attr.mode |= attrReverse
t.cur.Attr.Mode |= attrReverse
case 21, 22:
t.cur.attr.mode &^= attrBold
t.cur.Attr.Mode &^= attrBold
case 23:
t.cur.attr.mode &^= attrItalic
t.cur.Attr.Mode &^= attrItalic
case 24:
t.cur.attr.mode &^= attrUnderline
t.cur.Attr.Mode &^= attrUnderline
case 25, 26:
t.cur.attr.mode &^= attrBlink
t.cur.Attr.Mode &^= attrBlink
case 27:
t.cur.attr.mode &^= attrReverse
t.cur.Attr.Mode &^= attrReverse
case 38:
if i+2 < len(attr) && attr[i+1] == 5 {
i += 2
if between(attr[i], 0, 255) {
t.cur.attr.fg = Color(attr[i])
t.cur.Attr.FG = Color(attr[i])
} else {
t.logf("bad fgcolor %d\n", attr[i])
}
} else if i+4 < len(attr) && attr[i+1] == 2 {
i += 4
r, g, b := attr[i-2], attr[i-1], attr[i]
if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) {
t.logf("bad fg rgb color (%d,%d,%d)\n", r, g, b)
} else {
t.cur.Attr.FG = Color(r<<16 | g<<8 | b)
}
} else {
t.logf("gfx attr %d unknown\n", a)
}
case 39:
t.cur.attr.fg = DefaultFG
t.cur.Attr.FG = DefaultFG
case 48:
if i+2 < len(attr) && attr[i+1] == 5 {
i += 2
if between(attr[i], 0, 255) {
t.cur.attr.bg = Color(attr[i])
t.cur.Attr.BG = Color(attr[i])
} else {
t.logf("bad bgcolor %d\n", attr[i])
}
} else if i+4 < len(attr) && attr[i+1] == 2 {
i += 4
r, g, b := attr[i-2], attr[i-1], attr[i]
if !between(r, 0, 255) || !between(g, 0, 255) || !between(b, 0, 255) {
t.logf("bad bg rgb color (%d,%d,%d)\n", r, g, b)
} else {
t.cur.Attr.BG = Color(r<<16 | g<<8 | b)
}
} else {
t.logf("gfx attr %d unknown\n", a)
}
case 49:
t.cur.attr.bg = DefaultBG
t.cur.Attr.BG = DefaultBG
default:
if between(a, 30, 37) {
t.cur.attr.fg = Color(a - 30)
t.cur.Attr.FG = Color(a - 30)
} else if between(a, 40, 47) {
t.cur.attr.bg = Color(a - 40)
t.cur.Attr.BG = Color(a - 40)
} else if between(a, 90, 97) {
t.cur.attr.fg = Color(a - 90 + 8)
t.cur.Attr.FG = Color(a - 90 + 8)
} else if between(a, 100, 107) {
t.cur.attr.bg = Color(a - 100 + 8)
t.cur.Attr.BG = Color(a - 100 + 8)
} else {
t.logf("gfx attr %d unknown\n", a)
}
@@ -660,46 +693,46 @@ func (t *State) setAttr(attr []int) {
}
func (t *State) insertBlanks(n int) {
src := t.cur.x
src := t.cur.X
dst := src + n
size := t.cols - dst
t.changed |= ChangedScreen
t.dirty[t.cur.y] = true
t.dirty[t.cur.Y] = true
if dst >= t.cols {
t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
} else {
copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size])
t.clear(src, t.cur.y, dst-1, t.cur.y)
copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size])
t.clear(src, t.cur.Y, dst-1, t.cur.Y)
}
}
func (t *State) insertBlankLines(n int) {
if t.cur.y < t.top || t.cur.y > t.bottom {
if t.cur.Y < t.top || t.cur.Y > t.bottom {
return
}
t.scrollDown(t.cur.y, n)
t.scrollDown(t.cur.Y, n)
}
func (t *State) deleteLines(n int) {
if t.cur.y < t.top || t.cur.y > t.bottom {
if t.cur.Y < t.top || t.cur.Y > t.bottom {
return
}
t.scrollUp(t.cur.y, n)
t.scrollUp(t.cur.Y, n)
}
func (t *State) deleteChars(n int) {
src := t.cur.x + n
dst := t.cur.x
src := t.cur.X + n
dst := t.cur.X
size := t.cols - src
t.changed |= ChangedScreen
t.dirty[t.cur.y] = true
t.dirty[t.cur.Y] = true
if src >= t.cols {
t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y)
t.clear(t.cur.X, t.cur.Y, t.cols-1, t.cur.Y)
} else {
copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size])
t.clear(t.cols-n, t.cur.y, t.cols-1, t.cur.y)
copy(t.lines[t.cur.Y][dst:dst+size], t.lines[t.cur.Y][src:src+size])
t.clear(t.cols-n, t.cur.Y, t.cols-1, t.cur.Y)
}
}
@@ -708,8 +741,8 @@ func (t *State) setTitle(title string) {
t.title = title
}
func (t *State) Size() (rows int, cols int) {
return t.rows, t.cols
func (t *State) Size() (cols, rows int) {
return t.cols, t.rows
}
func (t *State) String() string {
@@ -719,8 +752,8 @@ func (t *State) String() string {
var view []rune
for y := 0; y < t.rows; y++ {
for x := 0; x < t.cols; x++ {
c, _, _ := t.Cell(x, y)
view = append(view, c)
attr := t.Cell(x, y)
view = append(view, attr.Char)
}
view = append(view, '\n')
}

View File

@@ -1,6 +1,9 @@
package vt10x
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)
@@ -59,20 +62,77 @@ func (t *State) handleSTR() {
switch s.typ {
case ']': // OSC - operating system command
var p *string
switch d := s.arg(0, 0); d {
case 0, 1, 2:
title := s.argString(1, "")
if title != "" {
t.setTitle(title)
}
case 10:
if len(s.args) < 2 {
break
}
c := s.argString(1, "")
p := &c
if p != nil && *p == "?" {
t.oscColorResponse(int(DefaultFG), 10)
} else if err := t.setColorName(int(DefaultFG), p); err != nil {
t.logf("invalid foreground color: %s\n", maybe(p))
} else {
// TODO: redraw
}
case 11:
if len(s.args) < 2 {
break
}
c := s.argString(1, "")
p := &c
if p != nil && *p == "?" {
t.oscColorResponse(int(DefaultBG), 11)
} else if err := t.setColorName(int(DefaultBG), p); err != nil {
t.logf("invalid cursor color: %s\n", maybe(p))
} else {
// TODO: redraw
}
// case 12:
// if len(s.args) < 2 {
// break
// }
// c := s.argString(1, "")
// p := &c
// if p != nil && *p == "?" {
// t.oscColorResponse(int(DefaultCursor), 12)
// } else if err := t.setColorName(int(DefaultCursor), p); err != nil {
// t.logf("invalid background color: %s\n", p)
// } else {
// // TODO: redraw
// }
case 4: // color set
if len(s.args) < 3 {
break
}
// setcolorname(s.arg(1, 0), s.argString(2, ""))
c := s.argString(2, "")
p = &c
fallthrough
case 104: // color reset
// TODO: complain about invalid color, redraw, etc.
// setcolorname(s.arg(1, 0), nil)
j := -1
if len(s.args) > 1 {
j = s.arg(1, 0)
}
if p != nil && *p == "?" { // report
t.osc4ColorResponse(j)
} else if err := t.setColorName(j, p); err != nil {
if !(d == 104 && len(s.args) <= 1) {
t.logf("invalid color j=%d, p=%s\n", j, maybe(p))
}
} else {
// TODO: redraw
}
default:
t.logf("unknown OSC command %d\n", d)
// TODO: s.dump()
@@ -92,3 +152,179 @@ func (t *State) handleSTR() {
// t.str.dump()
}
}
func (t *State) setColorName(j int, p *string) error {
if !between(j, 0, 1<<24) {
return fmt.Errorf("invalid color value %d", j)
}
if p == nil {
// restore color
delete(t.colorOverride, Color(j))
} else {
// set color
r, g, b, err := parseColor(*p)
if err != nil {
return err
}
t.colorOverride[Color(j)] = Color(r<<16 | g<<8 | b)
}
return nil
}
func (t *State) oscColorResponse(j, num int) {
if j < 0 {
t.logf("failed to fetch osc color %d\n", j)
return
}
k, ok := t.colorOverride[Color(j)]
if ok {
j = int(k)
}
r, g, b := rgb(j)
t.w.Write([]byte(fmt.Sprintf("\033]%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", num, r, r, g, g, b, b)))
}
func (t *State) osc4ColorResponse(j int) {
if j < 0 {
t.logf("failed to fetch osc4 color %d\n", j)
return
}
k, ok := t.colorOverride[Color(j)]
if ok {
j = int(k)
}
r, g, b := rgb(j)
t.w.Write([]byte(fmt.Sprintf("\033]4;%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", j, r, r, g, g, b, b)))
}
func rgb(j int) (r, g, b int) {
return (j >> 16) & 0xff, (j >> 8) & 0xff, j & 0xff
}
var (
RGBPattern = regexp.MustCompile(`^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$`)
HashPattern = regexp.MustCompile(`[\da-f]`)
)
func parseColor(p string) (r, g, b int, err error) {
if len(p) == 0 {
err = fmt.Errorf("empty color spec")
return
}
low := strings.ToLower(p)
if strings.HasPrefix(low, "rgb:") {
low = low[4:]
sm := RGBPattern.FindAllStringSubmatch(low, -1)
if len(sm) != 1 || len(sm[0]) == 0 {
err = fmt.Errorf("invalid rgb color spec: %s", p)
return
}
m := sm[0]
var base float64
if len(m[1]) > 0 {
base = 15
} else if len(m[4]) > 0 {
base = 255
} else if len(m[7]) > 0 {
base = 4095
} else {
base = 65535
}
r64, err := strconv.ParseInt(firstNonEmpty(m[1], m[4], m[7], m[10]), 16, 0)
if err != nil {
return r, g, b, err
}
g64, err := strconv.ParseInt(firstNonEmpty(m[2], m[5], m[8], m[11]), 16, 0)
if err != nil {
return r, g, b, err
}
b64, err := strconv.ParseInt(firstNonEmpty(m[3], m[6], m[9], m[12]), 16, 0)
if err != nil {
return r, g, b, err
}
r = int(math.Round(float64(r64) / base * 255))
g = int(math.Round(float64(g64) / base * 255))
b = int(math.Round(float64(b64) / base * 255))
return r, g, b, nil
} else if strings.HasPrefix(low, "#") {
low = low[1:]
m := HashPattern.FindAllString(low, -1)
if !oneOf(len(m), []int{3, 6, 9, 12}) {
err = fmt.Errorf("invalid hash color spec: %s", p)
return
}
adv := len(low) / 3
for i := 0; i < 3; i++ {
c, err := strconv.ParseInt(low[adv*i:adv*i+adv], 16, 0)
if err != nil {
return r, g, b, err
}
var v int64
switch adv {
case 1:
v = c << 4
case 2:
v = c
case 3:
v = c >> 4
default:
v = c >> 8
}
switch i {
case 0:
r = int(v)
case 1:
g = int(v)
case 2:
b = int(v)
}
}
return
} else {
err = fmt.Errorf("invalid color spec: %s", p)
return
}
}
func maybe(p *string) string {
if p == nil {
return "<nil>"
}
return *p
}
func firstNonEmpty(strs ...string) string {
if len(strs) == 0 {
return ""
}
for _, str := range strs {
if len(str) > 0 {
return str
}
}
return strs[len(strs)-1]
}
func oneOf(v int, is []int) bool {
for _, i := range is {
if v == i {
return true
}
}
return false
}

89
vendor/github.com/hinshun/vt10x/vt.go generated vendored Normal file
View File

@@ -0,0 +1,89 @@
package vt10x
import (
"bufio"
"fmt"
"io"
"io/ioutil"
)
// Terminal represents the virtual terminal emulator.
type Terminal interface {
// View displays the virtual terminal.
View
// Write parses input and writes terminal changes to state.
io.Writer
// Parse blocks on read on pty or io.Reader, then parses sequences until
// buffer empties. State is locked as soon as first rune is read, and unlocked
// when buffer is empty.
Parse(bf *bufio.Reader) error
}
// View represents the view of the virtual terminal emulator.
type View interface {
// String dumps the virtual terminal contents.
fmt.Stringer
// Size returns the size of the virtual terminal.
Size() (cols, rows int)
// Resize changes the size of the virtual terminal.
Resize(cols, rows int)
// Mode returns the current terminal mode.//
Mode() ModeFlag
// Title represents the title of the console window.
Title() string
// Cell returns the glyph containing the character code, foreground color, and
// background color at position (x, y) relative to the top left of the terminal.
Cell(x, y int) Glyph
// Cursor returns the current position of the cursor.
Cursor() Cursor
// CursorVisible returns the visible state of the cursor.
CursorVisible() bool
// Lock locks the state object's mutex.
Lock()
// Unlock resets change flags and unlocks the state object's mutex.
Unlock()
}
type TerminalOption func(*TerminalInfo)
type TerminalInfo struct {
w io.Writer
cols, rows int
}
func WithWriter(w io.Writer) TerminalOption {
return func(info *TerminalInfo) {
info.w = w
}
}
func WithSize(cols, rows int) TerminalOption {
return func(info *TerminalInfo) {
info.cols = cols
info.rows = rows
}
}
// New returns a new virtual terminal emulator.
func New(opts ...TerminalOption) Terminal {
info := TerminalInfo{
w: ioutil.Discard,
cols: 80,
rows: 24,
}
for _, opt := range opts {
opt(&info)
}
return newTerminal(info)
}

View File

@@ -6,47 +6,34 @@ import (
"bufio"
"bytes"
"io"
"os"
"unicode"
"unicode/utf8"
)
// VT represents the virtual terminal emulator.
type VT struct {
dest *State
rwc io.ReadWriteCloser
br *bufio.Reader
pty *os.File
type terminal struct {
*State
}
// Create initializes a virtual terminal emulator with the target state
// and io.ReadWriteCloser input.
func Create(state *State, rwc io.ReadWriteCloser) (*VT, error) {
t := &VT{
dest: state,
rwc: rwc,
}
t.init()
return t, nil
func newTerminal(info TerminalInfo) *terminal {
t := &terminal{newState(info.w)}
t.init(info.cols, info.rows)
return t
}
func (t *VT) init() {
t.br = bufio.NewReader(t.rwc)
t.dest.w = t.rwc
t.dest.numlock = true
t.dest.state = t.dest.parse
t.dest.cur.attr.fg = DefaultFG
t.dest.cur.attr.bg = DefaultBG
t.Resize(80, 24)
t.dest.reset()
func (t *terminal) init(cols, rows int) {
t.numlock = true
t.state = t.parse
t.cur.attr.fg = DefaultFG
t.cur.attr.bg = DefaultBG
t.Resize(cols, rows)
t.reset()
}
// Write parses input and writes terminal changes to state.
func (t *VT) Write(p []byte) (int, error) {
func (t *terminal) Write(p []byte) (int, error) {
var written int
r := bytes.NewReader(p)
t.dest.lock()
defer t.dest.unlock()
t.lock()
defer t.unlock()
for {
c, sz, err := r.ReadRune()
if err != nil {
@@ -61,51 +48,43 @@ func (t *VT) Write(p []byte) (int, error) {
// not enough bytes for a full rune
return written - 1, nil
}
t.dest.logln("invalid utf8 sequence")
t.logln("invalid utf8 sequence")
continue
}
t.dest.put(c)
t.put(c)
}
return written, nil
}
// Close closes the io.ReadWriteCloser.
func (t *VT) Close() error {
return t.rwc.Close()
}
// Parse blocks on read on io.ReadWriteCloser, then parses sequences until
// buffer empties. State is locked as soon as first rune is read, and unlocked
// when buffer is empty.
// TODO: add tests for expected blocking behavior
func (t *VT) Parse() error {
func (t *terminal) Parse(br *bufio.Reader) error {
var locked bool
defer func() {
if locked {
t.dest.unlock()
t.unlock()
}
}()
for {
c, sz, err := t.br.ReadRune()
c, sz, err := br.ReadRune()
if err != nil {
return err
}
if c == unicode.ReplacementChar && sz == 1 {
t.dest.logln("invalid utf8 sequence")
t.logln("invalid utf8 sequence")
break
}
if !locked {
t.dest.lock()
t.lock()
locked = true
}
// put rune for parsing and update state
t.dest.put(c)
t.put(c)
// break if our buffer is empty, or if buffer contains an
// incomplete rune.
n := t.br.Buffered()
if n == 0 || (n < 4 && !fullRuneBuffered(t.br)) {
n := br.Buffered()
if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
break
}
}
@@ -121,9 +100,8 @@ func fullRuneBuffered(br *bufio.Reader) bool {
return utf8.FullRune(buf)
}
// Resize reports new size to pty and updates state.
func (t *VT) Resize(cols, rows int) {
t.dest.lock()
defer t.dest.unlock()
_ = t.dest.resize(cols, rows)
func (t *terminal) Resize(cols, rows int) {
t.lock()
defer t.unlock()
_ = t.resize(cols, rows)
}

View File

@@ -10,41 +10,31 @@ import (
"unicode/utf8"
)
// VT represents the virtual terminal emulator.
type VT struct {
dest *State
rwc io.ReadWriteCloser
br *bufio.Reader
type terminal struct {
*State
}
// Create initializes a virtual terminal emulator with the target state
// and io.ReadWriteCloser input.
func Create(state *State, rwc io.ReadWriteCloser) (*VT, error) {
t := &VT{
dest: state,
rwc: rwc,
}
t.init()
return t, nil
func newTerminal(info TerminalInfo) *terminal {
t := &terminal{newState(info.w)}
t.init(info.cols, info.rows)
return t
}
func (t *VT) init() {
t.br = bufio.NewReader(t.rwc)
t.dest.w = t.rwc
t.dest.numlock = true
t.dest.state = t.dest.parse
t.dest.cur.attr.fg = DefaultFG
t.dest.cur.attr.bg = DefaultBG
t.Resize(80, 24)
t.dest.reset()
func (t *terminal) init(cols, rows int) {
t.numlock = true
t.state = t.parse
t.cur.Attr.FG = DefaultFG
t.cur.Attr.BG = DefaultBG
t.Resize(cols, rows)
t.reset()
}
// Write parses input and writes terminal changes to state.
func (t *VT) Write(p []byte) (int, error) {
func (t *terminal) Write(p []byte) (int, error) {
var written int
r := bytes.NewReader(p)
t.dest.lock()
defer t.dest.unlock()
t.lock()
defer t.unlock()
for {
c, sz, err := r.ReadRune()
if err != nil {
@@ -59,51 +49,43 @@ func (t *VT) Write(p []byte) (int, error) {
// not enough bytes for a full rune
return written - 1, nil
}
t.dest.logln("invalid utf8 sequence")
t.logln("invalid utf8 sequence")
continue
}
t.dest.put(c)
t.put(c)
}
return written, nil
}
// Close closes the io.ReadWriteCloser.
func (t *VT) Close() error {
return t.rwc.Close()
}
// Parse blocks on read on pty or io.ReadCloser, then parses sequences until
// buffer empties. State is locked as soon as first rune is read, and unlocked
// when buffer is empty.
// TODO: add tests for expected blocking behavior
func (t *VT) Parse() error {
func (t *terminal) Parse(br *bufio.Reader) error {
var locked bool
defer func() {
if locked {
t.dest.unlock()
t.unlock()
}
}()
for {
c, sz, err := t.br.ReadRune()
c, sz, err := br.ReadRune()
if err != nil {
return err
}
if c == unicode.ReplacementChar && sz == 1 {
t.dest.logln("invalid utf8 sequence")
t.logln("invalid utf8 sequence")
break
}
if !locked {
t.dest.lock()
t.lock()
locked = true
}
// put rune for parsing and update state
t.dest.put(c)
t.put(c)
// break if our buffer is empty, or if buffer contains an
// incomplete rune.
n := t.br.Buffered()
if n == 0 || (n < 4 && !fullRuneBuffered(t.br)) {
n := br.Buffered()
if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
break
}
}
@@ -119,9 +101,8 @@ func fullRuneBuffered(br *bufio.Reader) bool {
return utf8.FullRune(buf)
}
// Resize reports new size to pty and updates state.
func (t *VT) Resize(cols, rows int) {
t.dest.lock()
defer t.dest.unlock()
_ = t.dest.resize(cols, rows)
func (t *terminal) Resize(cols, rows int) {
t.lock()
defer t.unlock()
_ = t.resize(cols, rows)
}

View File

@@ -1,28 +0,0 @@
// Package require implements the same assertions as the `assert` package but
// stops test execution when a test fails.
//
// Example Usage
//
// The following is a complete example using require in a standard test function:
// import (
// "testing"
// "github.com/stretchr/testify/require"
// )
//
// func TestSomething(t *testing.T) {
//
// var a string = "Hello"
// var b string = "Hello"
//
// require.Equal(t, a, b, "The two words should be the same.")
//
// }
//
// Assertions
//
// The `require` package have same global functions as in the `assert` package,
// but instead of returning a boolean result they call `t.FailNow()`.
//
// Every assertion function also takes an optional string message as the final argument,
// allowing custom error messages to be appended to the message the assertion method outputs.
package require

View File

@@ -1,16 +0,0 @@
package require
// Assertions provides assertion methods around the
// TestingT interface.
type Assertions struct {
t TestingT
}
// New makes a new Assertions object for the specified TestingT.
func New(t TestingT) *Assertions {
return &Assertions{
t: t,
}
}
//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require_forward.go.tmpl -include-format-funcs"

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
{{.Comment}}
func {{.DocInfo.Name}}(t TestingT, {{.Params}}) {
if h, ok := t.(tHelper); ok { h.Helper() }
if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { return }
t.FailNow()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
{{.CommentWithoutT "a"}}
func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) {
if h, ok := a.t.(tHelper); ok { h.Helper() }
{{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
}

View File

@@ -1,29 +0,0 @@
package require
// TestingT is an interface wrapper around *testing.T
type TestingT interface {
Errorf(format string, args ...interface{})
FailNow()
}
type tHelper interface {
Helper()
}
// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful
// for table driven tests.
type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{})
// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful
// for table driven tests.
type ValueAssertionFunc func(TestingT, interface{}, ...interface{})
// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful
// for table driven tests.
type BoolAssertionFunc func(TestingT, bool, ...interface{})
// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful
// for table driven tests.
type ErrorAssertionFunc func(TestingT, error, ...interface{})
//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs"

5
vendor/modules.txt vendored
View File

@@ -286,7 +286,7 @@ github.com/hashicorp/errwrap
github.com/hashicorp/go-multierror
# github.com/hashicorp/go-version v1.3.0
github.com/hashicorp/go-version
# github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c
# github.com/hinshun/vt10x v0.0.0-20220127042424-3ca73d0126d7
## explicit
github.com/hinshun/vt10x
# github.com/hpcloud/tail v1.0.0
@@ -312,6 +312,7 @@ github.com/kballard/go-shellquote
# github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351
github.com/kevinburke/ssh_config
# github.com/kr/pty v1.1.5
## explicit
github.com/kr/pty
# github.com/kubernetes-sigs/service-catalog v0.3.1
## explicit
@@ -585,9 +586,7 @@ github.com/spf13/cobra
## explicit
github.com/spf13/pflag
# github.com/stretchr/testify v1.7.0
## explicit
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/tidwall/gjson v1.9.3
## explicit
github.com/tidwall/gjson