mirror of
https://github.com/redhat-developer/odo.git
synced 2025-10-19 03:06:19 +03:00
Manual sync of files pressing p (#6089)
* Set character mode for input stream * Move watchers in receiver + sync when p pressed * Integration tests manual sync * Add a console to DevSession * Vendor * Skip pressKey tests on Windows * Add interactive test for p press * Add info about pressing p key * Doc * Review * Rephrase Manul sync * Fix HotReloadCapable * Document timers * Document enableCharInput * Document geyKey and getKeyWatcher functions * Avoid to Kill in AfterEach after running Kill before
This commit is contained in:
@@ -58,6 +58,8 @@ function Run-Test {
|
||||
[Environment]::SetEnvironmentVariable("TEST_EXEC_NODES", "$TEST_EXEC_NODES")
|
||||
[Environment]::SetEnvironmentVariable("SKIP_USER_LOGIN_TESTS","true")
|
||||
[Environment]::SetEnvironmentVariable("SKIP_WELCOMING_MESSAGES","true")
|
||||
# Integration tests detecting key press when running DevSession are not working on Windows
|
||||
[Environment]::SetEnvironmentVariable("SKIP_KEY_PRESS","true")
|
||||
|
||||
Shout "Login IBMcloud"
|
||||
ibmcloud login --apikey ${API_KEY}
|
||||
|
||||
@@ -35,7 +35,9 @@ Your application is now running on the cluster
|
||||
- Forwarding from 127.0.0.1:40001 -> 3000
|
||||
|
||||
Watching for changes in the current directory /Users/user/nodejs
|
||||
Press Ctrl+c to exit `odo dev` and delete resources from the cluster
|
||||
|
||||
[Ctrl+c] - Exit and delete resources from the cluster
|
||||
[p] - Manually apply local changes to the application on the cluster
|
||||
```
|
||||
|
||||
In the above example, three things have happened:
|
||||
@@ -46,6 +48,22 @@ In the above example, three things have happened:
|
||||
You can press Ctrl-c at any time to terminate the development session. The command can take a few moment to terminate, as it
|
||||
will first delete all resources deployed into the cluster for this session before terminating.
|
||||
|
||||
### Applying local changes to the application on the cluster
|
||||
|
||||
By default, the changes made by the user to the Devfile and source files are applied directly.
|
||||
|
||||
The flag `--no-watch` can be used to change this behaviour: when the user changes the devfile or any source file, the changes
|
||||
won't be applied immediately, but the next time the user presses the `p` key.
|
||||
|
||||
Depending on the local changes, different events can occur on the cluster:
|
||||
|
||||
- if source files are modified, they are pushed to the container running the application, and:
|
||||
- if the `run` command is marked as `HotReloadCapable`, the application is responsible for applying the new changes
|
||||
- if the `run` command is not marked as `HotReloadCapable`, the application is stopped, then restarted by odo using the `build` and `run` commands again.
|
||||
- if the Devfile is modified, the deployment of the application is modified with the new changes. In some circumstances, this may
|
||||
cause the restart of the container running the application and therefore the application itself.
|
||||
|
||||
|
||||
### Running an alternative command
|
||||
|
||||
#### Running an alternative build command
|
||||
|
||||
8
go.mod
8
go.mod
@@ -4,8 +4,8 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/ActiveState/termtest v0.7.1
|
||||
github.com/ActiveState/termtest/expect v0.7.0
|
||||
github.com/AlecAivazis/survey/v2 v2.3.5
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
|
||||
github.com/Xuanwo/go-locale v1.1.0
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/devfile/api/v2 v2.0.0-20220309195345-48ebbf1e51cf
|
||||
@@ -19,9 +19,7 @@ require (
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-openapi/spec v0.19.5
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
|
||||
github.com/jedib0t/go-pretty/v6 v6.3.5
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/kubernetes-sigs/service-catalog v0.3.1
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/mattn/go-colorable v0.1.9
|
||||
@@ -68,7 +66,6 @@ require (
|
||||
require (
|
||||
cloud.google.com/go v0.81.0 // indirect
|
||||
github.com/ActiveState/termtest/conpty v0.5.0 // indirect
|
||||
github.com/ActiveState/termtest/expect v0.7.0 // indirect
|
||||
github.com/ActiveState/termtest/xpty v0.6.0 // indirect
|
||||
github.com/ActiveState/vt10x v1.3.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -81,6 +78,7 @@ require (
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.8.20 // indirect
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/RangelReale/osincli v0.0.0-20160924135400-fababb0555f2 // indirect
|
||||
@@ -129,6 +127,7 @@ require (
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.4.0 // indirect
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
@@ -138,6 +137,7 @@ require (
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/klauspost/compress v1.11.13 // indirect
|
||||
github.com/kr/pty v1.1.8 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
|
||||
137
pkg/watch/file_watcher.go
Normal file
137
pkg/watch/file_watcher.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
dfutil "github.com/devfile/library/pkg/util"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/redhat-developer/odo/pkg/util"
|
||||
gitignore "github.com/sabhiram/go-gitignore"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
func getFullSourcesWatcher(path string, fileIgnores []string) (*fsnotify.Watcher, error) {
|
||||
absIgnorePaths := dfutil.GetAbsGlobExps(path, fileIgnores)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error setting up filesystem watcher: %v", err)
|
||||
}
|
||||
|
||||
// adding watch on the root folder and the sub folders recursively
|
||||
// so directory and the path in addRecursiveWatch() are the same
|
||||
err = addRecursiveWatch(watcher, path, path, absIgnorePaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error watching source path %s: %v", path, err)
|
||||
}
|
||||
return watcher, nil
|
||||
}
|
||||
|
||||
// addRecursiveWatch handles adding watches recursively for the path provided
|
||||
// and its subdirectories. If a non-directory is specified, this call is a no-op.
|
||||
// Files matching glob pattern defined in ignores will be ignored.
|
||||
// Taken from https://github.com/openshift/origin/blob/85eb37b34f0657631592356d020cef5a58470f8e/pkg/util/fsnotification/fsnotification.go
|
||||
// rootPath is the root path of the file or directory,
|
||||
// path is the recursive path of the file or the directory,
|
||||
// ignores contains the glob rules for matching
|
||||
func addRecursiveWatch(watcher *fsnotify.Watcher, rootPath string, path string, ignores []string) error {
|
||||
|
||||
file, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error introspecting path %s: %v", path, err)
|
||||
}
|
||||
|
||||
ignoreMatcher := gitignore.CompileIgnoreLines(ignores...)
|
||||
|
||||
mode := file.Mode()
|
||||
if mode.IsRegular() {
|
||||
var rel string
|
||||
rel, err = filepath.Rel(rootPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
if !matched {
|
||||
klog.V(4).Infof("adding watch on path %s", path)
|
||||
|
||||
// checking if the file exits before adding the watcher to it
|
||||
if !util.CheckPathExists(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
klog.V(4).Infof("error adding watcher for path %s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
folders := []string{}
|
||||
err = filepath.Walk(path, func(newPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// Ignore the error if it's a 'path does not exist' error, no need to walk a non-existent path
|
||||
if !util.CheckPathExists(newPath) {
|
||||
klog.V(4).Infof("Walk func received an error for path %s, but the path doesn't exist so this is likely not an error. err: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to walk path: %s: %w", newPath, err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// If the current directory matches any of the ignore patterns, ignore them so that their contents are also not ignored
|
||||
rel, err := filepath.Rel(rootPath, newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to addRecursiveWatch on %s: %w", newPath, err)
|
||||
}
|
||||
if matched {
|
||||
klog.V(4).Infof("ignoring watch on path %s", newPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Append the folder we just walked on
|
||||
folders = append(folders, newPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, folder := range folders {
|
||||
|
||||
rel, err := filepath.Rel(rootPath, folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
|
||||
if matched {
|
||||
klog.V(4).Infof("ignoring watch for %s", folder)
|
||||
continue
|
||||
}
|
||||
|
||||
// checking if the file exits before adding the watcher to it
|
||||
if !util.CheckPathExists(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
klog.V(4).Infof("adding watch on path %s", folder)
|
||||
err = watcher.Add(folder)
|
||||
if err != nil {
|
||||
// Linux "no space left on device" issues are usually resolved via
|
||||
// $ sudo sysctl fs.inotify.max_user_watches=65536
|
||||
// BSD / OSX: "too many open files" issues are ussualy resolved via
|
||||
// $ sysctl variables "kern.maxfiles" and "kern.maxfilesperproc",
|
||||
klog.V(4).Infof("error adding watcher for path %s: %v", folder, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
74
pkg/watch/key_watcher.go
Normal file
74
pkg/watch/key_watcher.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// getKeyWatcher returns a channel which will emit
|
||||
// characters when keys are pressed on the keyboard
|
||||
func getKeyWatcher(ctx context.Context, out io.Writer) <-chan byte {
|
||||
|
||||
keyInput := make(chan byte)
|
||||
|
||||
go func() {
|
||||
stdinfd := int(os.Stdin.Fd())
|
||||
if !term.IsTerminal(stdinfd) {
|
||||
return
|
||||
}
|
||||
|
||||
// First set the terminal in character mode
|
||||
// to be able to read characters as soon as
|
||||
// they are emitted, instead of waiting
|
||||
// for newline characters
|
||||
oldState, err := term.GetState(stdinfd)
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, fmt.Errorf("getstate: %w", err))
|
||||
return
|
||||
}
|
||||
err = enableCharInput(stdinfd)
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, fmt.Errorf("enableCharInput: %w", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = term.Restore(stdinfd, oldState)
|
||||
}()
|
||||
|
||||
// Wait for the context to be cancelled
|
||||
// or a character to be emitted
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case b := <-getKey(out):
|
||||
keyInput <- b
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return keyInput
|
||||
}
|
||||
|
||||
// getKey returns a channel which will emit a character
|
||||
// when a key is pressed on the keyboard
|
||||
func getKey(out io.Writer) <-chan byte {
|
||||
|
||||
ch := make(chan byte)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 1)
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, fmt.Errorf("read: %w", err))
|
||||
return
|
||||
}
|
||||
ch <- b[0]
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
24
pkg/watch/key_watcher_unix.go
Normal file
24
pkg/watch/key_watcher_unix.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// enableCharInput is inspired from the Unix implementation of MakeRaw in golang.org/x/term
|
||||
// It enables the treatment of input stream char by char instead of line by line
|
||||
// See https://man7.org/linux/man-pages/man3/termios.3.html for reference
|
||||
func enableCharInput(fd int) error {
|
||||
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
termios.Lflag &^= unix.ICANON
|
||||
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
20
pkg/watch/key_watcher_windows.go
Normal file
20
pkg/watch/key_watcher_windows.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// enableCharInput is inspired from Windows implementation of MakeRaw in golang.org/x/term
|
||||
// It enables the treatment of input stream char by char instead of line by line
|
||||
// See https://docs.microsoft.com/en-us/windows/console/setconsolemode for reference
|
||||
func enableCharInput(fd int) error {
|
||||
var st uint32
|
||||
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||
return err
|
||||
}
|
||||
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT)
|
||||
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
13
pkg/watch/term_unix_bsd.go
Normal file
13
pkg/watch/term_unix_bsd.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2013 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.
|
||||
|
||||
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package watch
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const ioctlReadTermios = unix.TIOCGETA
|
||||
const ioctlWriteTermios = unix.TIOCSETA
|
||||
13
pkg/watch/term_unix_other.go
Normal file
13
pkg/watch/term_unix_other.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
//go:build aix || linux || solaris || zos
|
||||
// +build aix linux solaris zos
|
||||
|
||||
package watch
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const ioctlReadTermios = unix.TCGETS
|
||||
const ioctlWriteTermios = unix.TCSETS
|
||||
@@ -24,9 +24,6 @@ import (
|
||||
gitignore "github.com/sabhiram/go-gitignore"
|
||||
|
||||
"github.com/redhat-developer/odo/pkg/envinfo"
|
||||
"github.com/redhat-developer/odo/pkg/util"
|
||||
|
||||
dfutil "github.com/devfile/library/pkg/util"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -38,13 +35,26 @@ import (
|
||||
const (
|
||||
// PushErrorString is the string that is printed when an error occurs during watch's Push operation
|
||||
PushErrorString = "Error occurred on Push"
|
||||
CtrlCMessage = "Press Ctrl+c to exit `odo dev` and delete resources from the cluster"
|
||||
PromptMessage = `
|
||||
[Ctrl+c] - Exit and delete resources from the cluster
|
||||
[p] - Manually apply local changes to the application on the cluster
|
||||
`
|
||||
)
|
||||
|
||||
type WatchClient struct {
|
||||
kubeClient kclient.ClientInterface
|
||||
deleteClient _delete.Client
|
||||
stateClient state.Client
|
||||
|
||||
sourcesWatcher *fsnotify.Watcher
|
||||
deploymentWatcher watch.Interface
|
||||
devfileWatcher *fsnotify.Watcher
|
||||
podWatcher watch.Interface
|
||||
warningsWatcher watch.Interface
|
||||
keyWatcher <-chan byte
|
||||
|
||||
// true to force sync, used when manual sync
|
||||
forceSync bool
|
||||
}
|
||||
|
||||
var _ Client = (*WatchClient)(nil)
|
||||
@@ -108,138 +118,30 @@ type evaluateChangesFunc func(events []fsnotify.Event, path string, fileIgnores
|
||||
// It returns a Duration after which to recall in case of error
|
||||
type processEventsFunc func(changedFiles, deletedPaths []string, parameters WatchParameters, out io.Writer, componentStatus *ComponentStatus, backoff *ExpBackoff) (*time.Duration, error)
|
||||
|
||||
// addRecursiveWatch handles adding watches recursively for the path provided
|
||||
// and its subdirectories. If a non-directory is specified, this call is a no-op.
|
||||
// Files matching glob pattern defined in ignores will be ignored.
|
||||
// Taken from https://github.com/openshift/origin/blob/85eb37b34f0657631592356d020cef5a58470f8e/pkg/util/fsnotification/fsnotification.go
|
||||
// rootPath is the root path of the file or directory,
|
||||
// path is the recursive path of the file or the directory,
|
||||
// ignores contains the glob rules for matching
|
||||
func addRecursiveWatch(watcher *fsnotify.Watcher, rootPath string, path string, ignores []string) error {
|
||||
|
||||
file, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("error introspecting path %s: %v", path, err)
|
||||
}
|
||||
|
||||
ignoreMatcher := gitignore.CompileIgnoreLines(ignores...)
|
||||
|
||||
mode := file.Mode()
|
||||
if mode.IsRegular() {
|
||||
var rel string
|
||||
rel, err = filepath.Rel(rootPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
if !matched {
|
||||
klog.V(4).Infof("adding watch on path %s", path)
|
||||
|
||||
// checking if the file exits before adding the watcher to it
|
||||
if !util.CheckPathExists(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
klog.V(4).Infof("error adding watcher for path %s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
folders := []string{}
|
||||
err = filepath.Walk(path, func(newPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// Ignore the error if it's a 'path does not exist' error, no need to walk a non-existent path
|
||||
if !util.CheckPathExists(newPath) {
|
||||
klog.V(4).Infof("Walk func received an error for path %s, but the path doesn't exist so this is likely not an error. err: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to walk path: %s: %w", newPath, err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// If the current directory matches any of the ignore patterns, ignore them so that their contents are also not ignored
|
||||
rel, err := filepath.Rel(rootPath, newPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to addRecursiveWatch on %s: %w", newPath, err)
|
||||
}
|
||||
if matched {
|
||||
klog.V(4).Infof("ignoring watch on path %s", newPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Append the folder we just walked on
|
||||
folders = append(folders, newPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, folder := range folders {
|
||||
|
||||
rel, err := filepath.Rel(rootPath, folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := ignoreMatcher.MatchesPath(rel)
|
||||
|
||||
if matched {
|
||||
klog.V(4).Infof("ignoring watch for %s", folder)
|
||||
continue
|
||||
}
|
||||
|
||||
// checking if the file exits before adding the watcher to it
|
||||
if !util.CheckPathExists(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
klog.V(4).Infof("adding watch on path %s", folder)
|
||||
err = watcher.Add(folder)
|
||||
if err != nil {
|
||||
// Linux "no space left on device" issues are usually resolved via
|
||||
// $ sudo sysctl fs.inotify.max_user_watches=65536
|
||||
// BSD / OSX: "too many open files" issues are ussualy resolved via
|
||||
// $ sysctl variables "kern.maxfiles" and "kern.maxfilesperproc",
|
||||
klog.V(4).Infof("error adding watcher for path %s: %v", folder, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ctx context.Context, componentStatus ComponentStatus) error {
|
||||
klog.V(4).Infof("starting WatchAndPush, path: %s, component: %s, ignores %s", parameters.Path, parameters.ComponentName, parameters.FileIgnores)
|
||||
|
||||
var sourcesWatcher *fsnotify.Watcher
|
||||
var err error
|
||||
if parameters.WatchFiles {
|
||||
sourcesWatcher, err = getFullSourcesWatcher(parameters.Path, parameters.FileIgnores)
|
||||
o.sourcesWatcher, err = getFullSourcesWatcher(parameters.Path, parameters.FileIgnores)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
sourcesWatcher, err = fsnotify.NewWatcher()
|
||||
o.sourcesWatcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer sourcesWatcher.Close()
|
||||
defer o.sourcesWatcher.Close()
|
||||
|
||||
selector := labels.GetSelector(parameters.ComponentName, parameters.ApplicationName, labels.ComponentDevMode, true)
|
||||
deploymentWatcher, err := o.kubeClient.DeploymentWatcher(ctx, selector)
|
||||
o.deploymentWatcher, err = o.kubeClient.DeploymentWatcher(ctx, selector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error watching deployment: %v", err)
|
||||
}
|
||||
|
||||
devfileWatcher, err := fsnotify.NewWatcher()
|
||||
o.devfileWatcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -251,19 +153,20 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
|
||||
}
|
||||
devfileFiles = append(devfileFiles, parameters.DevfilePath)
|
||||
for _, f := range devfileFiles {
|
||||
err = devfileWatcher.Add(f)
|
||||
err = o.devfileWatcher.Add(f)
|
||||
if err != nil {
|
||||
klog.V(4).Infof("error adding watcher for path %s: %v", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
podWatcher, err := o.kubeClient.PodWatcher(ctx, selector)
|
||||
o.podWatcher, err = o.kubeClient.PodWatcher(ctx, selector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warningsWatcher, isForbidden, err := o.kubeClient.PodWarningEventWatcher(ctx)
|
||||
var isForbidden bool
|
||||
o.warningsWatcher, isForbidden, err = o.kubeClient.PodWarningEventWatcher(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -271,30 +174,21 @@ func (o *WatchClient) WatchAndPush(out io.Writer, parameters WatchParameters, ct
|
||||
log.Fwarning(out, "Unable to watch Events resource, warning Events won't be displayed")
|
||||
}
|
||||
|
||||
return o.eventWatcher(ctx, sourcesWatcher, deploymentWatcher, devfileWatcher, podWatcher, warningsWatcher, parameters, out, evaluateFileChanges, processEvents, componentStatus)
|
||||
}
|
||||
|
||||
func getFullSourcesWatcher(path string, fileIgnores []string) (*fsnotify.Watcher, error) {
|
||||
absIgnorePaths := dfutil.GetAbsGlobExps(path, fileIgnores)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error setting up filesystem watcher: %v", err)
|
||||
}
|
||||
|
||||
// adding watch on the root folder and the sub folders recursively
|
||||
// so directory and the path in addRecursiveWatch() are the same
|
||||
err = addRecursiveWatch(watcher, path, path, absIgnorePaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error watching source path %s: %v", path, err)
|
||||
}
|
||||
return watcher, nil
|
||||
o.keyWatcher = getKeyWatcher(ctx, out)
|
||||
return o.eventWatcher(ctx, parameters, out, evaluateFileChanges, processEvents, componentStatus)
|
||||
}
|
||||
|
||||
// eventWatcher loops till the context's Done channel indicates it to stop looping, at which point it performs cleanup.
|
||||
// While looping, it listens for filesystem events and processes these events using the WatchParameters to push to the remote pod.
|
||||
// It outputs any logs to the out io Writer
|
||||
func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify.Watcher, deploymentWatcher watch.Interface, devfileWatcher *fsnotify.Watcher, podWatcher watch.Interface, eventsWatcher watch.Interface, parameters WatchParameters, out io.Writer, evaluateChangesHandler evaluateChangesFunc, processEventsHandler processEventsFunc, componentStatus ComponentStatus) error {
|
||||
func (o *WatchClient) eventWatcher(
|
||||
ctx context.Context,
|
||||
parameters WatchParameters,
|
||||
out io.Writer,
|
||||
evaluateChangesHandler evaluateChangesFunc,
|
||||
processEventsHandler processEventsFunc,
|
||||
componentStatus ComponentStatus,
|
||||
) error {
|
||||
|
||||
expBackoff := NewExpBackoff()
|
||||
|
||||
@@ -309,12 +203,15 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
sourcesTimer := time.NewTimer(time.Millisecond)
|
||||
<-sourcesTimer.C
|
||||
|
||||
// devfileTimer has the same usage as sourcesTimer, for file events coming from devfileWatcher
|
||||
devfileTimer := time.NewTimer(time.Millisecond)
|
||||
<-devfileTimer.C
|
||||
|
||||
// deployTimer has the same usage as sourcesTimer, for events coming from watching Deployments, from deploymentWatcher
|
||||
deployTimer := time.NewTimer(time.Millisecond)
|
||||
<-deployTimer.C
|
||||
|
||||
// retryTimer is a timer used to retry later a sync that has failed
|
||||
retryTimer := time.NewTimer(time.Millisecond)
|
||||
<-retryTimer.C
|
||||
|
||||
@@ -322,24 +219,31 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-sourcesWatcher.Events:
|
||||
case event := <-o.sourcesWatcher.Events:
|
||||
events = append(events, event)
|
||||
// We are waiting for more events in this interval
|
||||
sourcesTimer.Reset(100 * time.Millisecond)
|
||||
|
||||
case <-sourcesTimer.C:
|
||||
// timer has fired
|
||||
if !componentCanSyncFile(componentStatus.State) {
|
||||
continue
|
||||
}
|
||||
// first find the files that have changed (also includes the ones newly created) or deleted
|
||||
changedFiles, deletedPaths := evaluateChangesHandler(events, parameters.Path, parameters.FileIgnores, sourcesWatcher)
|
||||
// process the changes and sync files with remote pod
|
||||
if len(changedFiles) == 0 && len(deletedPaths) == 0 {
|
||||
continue
|
||||
|
||||
var changedFiles, deletedPaths []string
|
||||
if !o.forceSync {
|
||||
// first find the files that have changed (also includes the ones newly created) or deleted
|
||||
changedFiles, deletedPaths = evaluateChangesHandler(events, parameters.Path, parameters.FileIgnores, o.sourcesWatcher)
|
||||
// process the changes and sync files with remote pod
|
||||
if len(changedFiles) == 0 && len(deletedPaths) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
componentStatus.State = StateSyncOutdated
|
||||
fmt.Fprintf(out, "Pushing files...\n\n")
|
||||
retry, err := processEventsHandler(changedFiles, deletedPaths, parameters, out, &componentStatus, expBackoff)
|
||||
o.forceSync = false
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,10 +259,16 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
<-retryTimer.C
|
||||
}
|
||||
|
||||
case watchErr := <-sourcesWatcher.Errors:
|
||||
case watchErr := <-o.sourcesWatcher.Errors:
|
||||
return watchErr
|
||||
|
||||
case ev := <-deploymentWatcher.ResultChan():
|
||||
case key := <-o.keyWatcher:
|
||||
if key == 'p' {
|
||||
o.forceSync = true
|
||||
sourcesTimer.Reset(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
case ev := <-o.deploymentWatcher.ResultChan():
|
||||
switch obj := ev.Object.(type) {
|
||||
case *appsv1.Deployment:
|
||||
klog.V(4).Infof("deployment watcher Event: Type: %s, name: %s, rv: %s, pods: %d\n",
|
||||
@@ -381,7 +291,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
<-retryTimer.C
|
||||
}
|
||||
|
||||
case <-devfileWatcher.Events:
|
||||
case <-o.devfileWatcher.Events:
|
||||
devfileTimer.Reset(100 * time.Millisecond)
|
||||
|
||||
case <-devfileTimer.C:
|
||||
@@ -409,7 +319,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
<-retryTimer.C
|
||||
}
|
||||
|
||||
case ev := <-podWatcher.ResultChan():
|
||||
case ev := <-o.podWatcher.ResultChan():
|
||||
switch ev.Type {
|
||||
case watch.Deleted:
|
||||
pod, ok := ev.Object.(*corev1.Pod)
|
||||
@@ -425,7 +335,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
podsPhases.Add(out, pod.GetCreationTimestamp(), pod)
|
||||
}
|
||||
|
||||
case ev := <-eventsWatcher.ResultChan():
|
||||
case ev := <-o.warningsWatcher.ResultChan():
|
||||
switch kevent := ev.Object.(type) {
|
||||
case *corev1.Event:
|
||||
podName := kevent.InvolvedObject.Name
|
||||
@@ -439,7 +349,7 @@ func (o *WatchClient) eventWatcher(ctx context.Context, sourcesWatcher *fsnotify
|
||||
}
|
||||
}
|
||||
|
||||
case watchErr := <-devfileWatcher.Errors:
|
||||
case watchErr := <-o.devfileWatcher.Errors:
|
||||
return watchErr
|
||||
|
||||
case <-ctx.Done():
|
||||
@@ -635,7 +545,8 @@ func removeDuplicates(input []string) []string {
|
||||
}
|
||||
|
||||
func printInfoMessage(out io.Writer, path string) {
|
||||
log.Finfof(out, "\nWatching for changes in the current directory %s\n"+CtrlCMessage+"\n", path)
|
||||
log.Sectionf("Dev mode")
|
||||
fmt.Fprintf(out, " %s\n Watching for changes in the current directory %s\n\n %s%s", log.Sbold("Status:"), path, log.Sbold("Keyboard Commands:"), PromptMessage)
|
||||
}
|
||||
|
||||
func isFatal(err error) bool {
|
||||
|
||||
@@ -131,8 +131,15 @@ func Test_eventWatcher(t *testing.T) {
|
||||
State: StateReady,
|
||||
}
|
||||
|
||||
o := WatchClient{}
|
||||
err := o.eventWatcher(ctx, watcher, fakeWatcher{}, fileWatcher, fakeWatcher{}, fakeWatcher{}, tt.args.parameters, out, evaluateChangesHandler, processEventsHandler, componentStatus)
|
||||
o := WatchClient{
|
||||
sourcesWatcher: watcher,
|
||||
deploymentWatcher: fakeWatcher{},
|
||||
podWatcher: fakeWatcher{},
|
||||
warningsWatcher: fakeWatcher{},
|
||||
devfileWatcher: fileWatcher,
|
||||
keyWatcher: make(chan byte),
|
||||
}
|
||||
err := o.eventWatcher(ctx, tt.args.parameters, out, evaluateChangesHandler, processEventsHandler, componentStatus)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("eventWatcher() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/ActiveState/termtest/expect"
|
||||
"github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
@@ -109,6 +111,7 @@ import (
|
||||
type DevSession struct {
|
||||
session *gexec.Session
|
||||
stopped bool
|
||||
console *expect.Console
|
||||
}
|
||||
|
||||
// StartDevMode starts a dev session with `odo dev`
|
||||
@@ -116,16 +119,28 @@ type DevSession struct {
|
||||
// and the redirections endpoints to access ports opened by component
|
||||
// when the dev mode is completely started
|
||||
func StartDevMode(envvars []string, opts ...string) (DevSession, []byte, []byte, map[string]string, error) {
|
||||
|
||||
c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
|
||||
if err != nil {
|
||||
return DevSession{}, nil, nil, nil, err
|
||||
}
|
||||
|
||||
args := []string{"dev", "--random-ports"}
|
||||
args = append(args, opts...)
|
||||
session := Cmd("odo", args...).AddEnv(envvars...).Runner().session
|
||||
WaitForOutputToContain("Press Ctrl+c to exit `odo dev` and delete resources from the cluster", 360, 10, session)
|
||||
cmd := Cmd("odo", args...)
|
||||
cmd.Cmd.Stdin = c.Tty()
|
||||
cmd.Cmd.Stdout = c.Tty()
|
||||
cmd.Cmd.Stderr = c.Tty()
|
||||
|
||||
session := cmd.AddEnv(envvars...).Runner().session
|
||||
WaitForOutputToContain("[Ctrl+c] - Exit", 360, 10, session)
|
||||
result := DevSession{
|
||||
session: session,
|
||||
console: c,
|
||||
}
|
||||
outContents := session.Out.Contents()
|
||||
errContents := session.Err.Contents()
|
||||
err := session.Out.Clear()
|
||||
err = session.Out.Clear()
|
||||
if err != nil {
|
||||
return DevSession{}, nil, nil, nil, err
|
||||
}
|
||||
@@ -139,11 +154,19 @@ func StartDevMode(envvars []string, opts ...string) (DevSession, []byte, []byte,
|
||||
|
||||
// Kill a Dev session abruptly, without handling any cleanup
|
||||
func (o DevSession) Kill() {
|
||||
if o.console != nil {
|
||||
err := o.console.Close()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
}
|
||||
o.session.Kill()
|
||||
}
|
||||
|
||||
// Stop a Dev session cleanly (equivalent as hitting Ctrl-c)
|
||||
func (o *DevSession) Stop() {
|
||||
if o.console != nil {
|
||||
err := o.console.Close()
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
}
|
||||
if o.stopped {
|
||||
return
|
||||
}
|
||||
@@ -152,6 +175,14 @@ func (o *DevSession) Stop() {
|
||||
o.stopped = true
|
||||
}
|
||||
|
||||
func (o *DevSession) PressKey(p byte) {
|
||||
if o.console == nil {
|
||||
return
|
||||
}
|
||||
_, err := o.console.Write([]byte{p})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
func (o DevSession) WaitEnd() {
|
||||
o.session.Wait(3 * time.Minute)
|
||||
}
|
||||
@@ -240,7 +271,8 @@ func DevModeShouldFail(envvars []string, substring string, opts ...string) (DevS
|
||||
}
|
||||
|
||||
// getPorts returns a map of ports redirected depending on the information in s
|
||||
// `- Forwarding from 127.0.0.1:40001 -> 3000` will return { "3000": "127.0.0.1:40001" }
|
||||
//
|
||||
// `- Forwarding from 127.0.0.1:40001 -> 3000` will return { "3000": "127.0.0.1:40001" }
|
||||
func getPorts(s string) map[string]string {
|
||||
result := map[string]string{}
|
||||
re := regexp.MustCompile("(127.0.0.1:[0-9]+) -> ([0-9]+)")
|
||||
|
||||
@@ -101,6 +101,10 @@ func SendLine(ctx InteractiveContext, line string) {
|
||||
ctx.cp.Send(line)
|
||||
}
|
||||
|
||||
func PressKey(ctx InteractiveContext, c byte) {
|
||||
ctx.cp.SendUnterminated(string(c))
|
||||
}
|
||||
|
||||
func ExpectString(ctx InteractiveContext, line string) {
|
||||
res, err := ctx.cp.Expect(line, 120*time.Second)
|
||||
fmt.Fprint(ctx.buffer, res)
|
||||
|
||||
@@ -254,6 +254,13 @@ status:
|
||||
stdout := commonVar.CliRunner.Run("get", "servicebinding", bindingName).Out.Contents()
|
||||
Expect(stdout).To(ContainSubstring("ApplicationsBound"))
|
||||
})
|
||||
})
|
||||
|
||||
When("odo dev is run", func() {
|
||||
BeforeEach(func() {
|
||||
devSession, _, _, _, err = helper.StartDevMode(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("odo dev command is stopped", func() {
|
||||
BeforeEach(func() {
|
||||
|
||||
@@ -261,7 +261,7 @@ var _ = Describe("odo delete command tests", func() {
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-valid-events.yaml")).ShouldPass()
|
||||
session := helper.CmdRunner("odo", "dev", "--random-ports")
|
||||
defer session.Kill()
|
||||
helper.WaitForOutputToContain("Press Ctrl+c to exit", 180, 10, session)
|
||||
helper.WaitForOutputToContain("[Ctrl+c] - Exit", 180, 10, session)
|
||||
// Ensure that the pod is in running state
|
||||
Eventually(string(commonVar.CliRunner.Run("get", "pods", "-n", commonVar.Project).Out.Contents()), 60, 3).Should(ContainSubstring(cmpName))
|
||||
// running in verbosity since the preStop events information is only printed in v4
|
||||
|
||||
@@ -171,10 +171,6 @@ var _ = Describe("odo dev debug command tests", func() {
|
||||
session, sessionOut, _, ports, err = helper.StartDevMode([]string{"PODMAN_CMD=echo"}, "--debug")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
AfterEach(func() {
|
||||
session.Stop()
|
||||
session.WaitEnd()
|
||||
})
|
||||
It("should execute the composite apply commands successfully", func() {
|
||||
checkDeploymentExists := func() {
|
||||
out := commonVar.CliRunner.Run("get", "deployments", deploymentName).Out.Contents()
|
||||
|
||||
@@ -255,11 +255,6 @@ ComponentSettings:
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
devSession.Kill()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
When("odo dev is stopped", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Stop()
|
||||
@@ -274,6 +269,62 @@ ComponentSettings:
|
||||
})
|
||||
})
|
||||
|
||||
When("odo dev is executed and Ephemeral is set to false", func() {
|
||||
|
||||
var devSession helper.DevSession
|
||||
BeforeEach(func() {
|
||||
helper.Cmd("odo", "preference", "set", "-f", "Ephemeral", "false").ShouldPass()
|
||||
var err error
|
||||
devSession, _, _, _, err = helper.StartDevMode(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("killing odo dev and running odo delete component --wait", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Kill()
|
||||
devSession.WaitEnd()
|
||||
helper.Cmd("odo", "delete", "component", "--wait", "-f").ShouldPass()
|
||||
})
|
||||
|
||||
It("should have deleted all resources before returning", func() {
|
||||
By("deleting the service", func() {
|
||||
services := commonVar.CliRunner.GetServices(commonVar.Project)
|
||||
Expect(services).To(BeEmpty())
|
||||
})
|
||||
By("deleting the PVC", func() {
|
||||
pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project)
|
||||
Expect(pvcs).To(BeEmpty())
|
||||
})
|
||||
By("deleting the pod", func() {
|
||||
pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project)
|
||||
Expect(pods).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("stopping odo dev normally", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should have deleted all resources before returning", func() {
|
||||
By("deleting the service", func() {
|
||||
services := commonVar.CliRunner.GetServices(commonVar.Project)
|
||||
Expect(services).To(BeEmpty())
|
||||
})
|
||||
By("deleting the PVC", func() {
|
||||
pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project)
|
||||
Expect(pvcs).To(BeEmpty())
|
||||
})
|
||||
By("deleting the pod", func() {
|
||||
pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project)
|
||||
Expect(pods).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("odo dev is executed and Ephemeral is set to false", func() {
|
||||
|
||||
var devSession helper.DevSession
|
||||
@@ -319,51 +370,6 @@ ComponentSettings:
|
||||
Expect(string(output)).To(ContainSubstring(fmt.Sprintf("Deployment/%s-app", cmpName)))
|
||||
})
|
||||
})
|
||||
|
||||
When("killing odo dev and running odo delete component --wait", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Kill()
|
||||
devSession.WaitEnd()
|
||||
helper.Cmd("odo", "delete", "component", "--wait", "-f").ShouldPass()
|
||||
})
|
||||
|
||||
It("should have deleted all resources before returning", func() {
|
||||
By("deleting the service", func() {
|
||||
services := commonVar.CliRunner.GetServices(commonVar.Project)
|
||||
Expect(services).To(BeEmpty())
|
||||
})
|
||||
By("deleting the PVC", func() {
|
||||
pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project)
|
||||
Expect(pvcs).To(BeEmpty())
|
||||
})
|
||||
By("deleting the pod", func() {
|
||||
pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project)
|
||||
Expect(pods).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("stopping odo dev normally", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should have deleted all resources before returning", func() {
|
||||
By("deleting the service", func() {
|
||||
services := commonVar.CliRunner.GetServices(commonVar.Project)
|
||||
Expect(services).To(BeEmpty())
|
||||
})
|
||||
By("deleting the PVC", func() {
|
||||
pvcs := commonVar.CliRunner.GetAllPVCNames(commonVar.Project)
|
||||
Expect(pvcs).To(BeEmpty())
|
||||
})
|
||||
By("deleting the pod", func() {
|
||||
pods := commonVar.CliRunner.GetAllPodNames(commonVar.Project)
|
||||
Expect(pods).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("odo is executed with --no-watch flag", func() {
|
||||
@@ -382,14 +388,37 @@ ComponentSettings:
|
||||
})
|
||||
|
||||
When("a file in component directory is modified", func() {
|
||||
It("should not trigger a push", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
helper.ReplaceString(filepath.Join(commonVar.Context, "server.js"), "App started", "App is super started")
|
||||
})
|
||||
|
||||
It("should not trigger a push", func() {
|
||||
podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project)
|
||||
execResult := commonVar.CliRunner.Exec(podName, commonVar.Project, "cat", "/projects/server.js")
|
||||
Expect(execResult).To(ContainSubstring("App started"))
|
||||
Expect(execResult).ToNot(ContainSubstring("App is super started"))
|
||||
|
||||
})
|
||||
|
||||
When("p is pressed", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
if os.Getenv("SKIP_KEY_PRESS") == "true" {
|
||||
Skip("This is a unix-terminal specific scenario, skipping")
|
||||
}
|
||||
|
||||
devSession.PressKey('p')
|
||||
})
|
||||
|
||||
It("should trigger a push", func() {
|
||||
_, _, _, err := devSession.WaitSync()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project)
|
||||
execResult := commonVar.CliRunner.Exec(podName, commonVar.Project, "cat", "/projects/server.js")
|
||||
Expect(execResult).To(ContainSubstring("App is super started"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -435,152 +464,113 @@ ComponentSettings:
|
||||
})
|
||||
})
|
||||
|
||||
Context("port-forwarding for the component", func() {
|
||||
When("devfile has single endpoint", func() {
|
||||
BeforeEach(func() {
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
|
||||
helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass()
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass()
|
||||
})
|
||||
|
||||
When("running odo dev", func() {
|
||||
var devSession helper.DevSession
|
||||
var ports map[string]string
|
||||
for _, manual := range []bool{false, true} {
|
||||
manual := manual
|
||||
Context("port-forwarding for the component", func() {
|
||||
When("devfile has single endpoint", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
devSession, _, _, ports, err = helper.StartDevMode(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
|
||||
helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass()
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should expose the endpoint on localhost", func() {
|
||||
url := fmt.Sprintf("http://%s", ports["3000"])
|
||||
resp, err := http.Get(url)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("modifying memoryLimit for container in Devfile", func() {
|
||||
When("running odo dev", func() {
|
||||
var devSession helper.DevSession
|
||||
var ports map[string]string
|
||||
BeforeEach(func() {
|
||||
src := "memoryLimit: 1024Mi"
|
||||
dst := "memoryLimit: 1023Mi"
|
||||
helper.ReplaceString("devfile.yaml", src, dst)
|
||||
var err error
|
||||
_, _, ports, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
opts := []string{}
|
||||
if manual {
|
||||
opts = append(opts, "--no-watch")
|
||||
}
|
||||
devSession, _, _, ports, err = helper.StartDevMode(nil, opts...)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should expose the endpoint on localhost", func() {
|
||||
By("updating the pod", func() {
|
||||
podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project)
|
||||
bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents()
|
||||
output := string(bufferOutput)
|
||||
Expect(output).To(ContainSubstring("1023Mi"))
|
||||
url := fmt.Sprintf("http://%s", ports["3000"])
|
||||
resp, err := http.Get(url)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("modifying memoryLimit for container in Devfile", func() {
|
||||
BeforeEach(func() {
|
||||
src := "memoryLimit: 1024Mi"
|
||||
dst := "memoryLimit: 1023Mi"
|
||||
helper.ReplaceString("devfile.yaml", src, dst)
|
||||
if manual {
|
||||
if os.Getenv("SKIP_KEY_PRESS") == "true" {
|
||||
Skip("This is a unix-terminal specific scenario, skipping")
|
||||
}
|
||||
|
||||
devSession.PressKey('p')
|
||||
}
|
||||
var err error
|
||||
_, _, ports, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
By("exposing the endpoint", func() {
|
||||
url := fmt.Sprintf("http://%s", ports["3000"])
|
||||
resp, err := http.Get(url)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp.Body.Close()
|
||||
It("should expose the endpoint on localhost", func() {
|
||||
By("updating the pod", func() {
|
||||
podName := commonVar.CliRunner.GetRunningPodNameByComponent(cmpName, commonVar.Project)
|
||||
bufferOutput := commonVar.CliRunner.Run("get", "pods", podName, "-o", "jsonpath='{.spec.containers[0].resources.requests.memory}'").Out.Contents()
|
||||
output := string(bufferOutput)
|
||||
Expect(output).To(ContainSubstring("1023Mi"))
|
||||
})
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
By("exposing the endpoint", func() {
|
||||
url := fmt.Sprintf("http://%s", ports["3000"])
|
||||
resp, err := http.Get(url)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("devfile has multiple endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context)
|
||||
helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass()
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass()
|
||||
})
|
||||
|
||||
When("running odo dev", func() {
|
||||
var devSession helper.DevSession
|
||||
var ports map[string]string
|
||||
When("devfile has multiple endpoints", func() {
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
devSession, _, _, ports, err = helper.StartDevMode(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context)
|
||||
helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass()
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should expose two endpoints on localhost", func() {
|
||||
url1 := fmt.Sprintf("http://%s", ports["3000"])
|
||||
url2 := fmt.Sprintf("http://%s", ports["4567"])
|
||||
|
||||
resp1, err := http.Get(url1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp1.Body.Close()
|
||||
|
||||
resp2, err := http.Get(url2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp2.Body.Close()
|
||||
|
||||
body1, _ := io.ReadAll(resp1.Body)
|
||||
helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js")
|
||||
|
||||
_, _, _, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
Eventually(func() bool {
|
||||
resp3, err := http.Get(url1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp3.Body.Close()
|
||||
|
||||
resp4, err := http.Get(url2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp4.Body.Close()
|
||||
|
||||
body3, _ := io.ReadAll(resp3.Body)
|
||||
if string(body3) != "H3110 from Node.js Starter Application!" {
|
||||
return false
|
||||
}
|
||||
|
||||
body4, _ := io.ReadAll(resp4.Body)
|
||||
return string(body4) == "H3110 from Node.js Starter Application!"
|
||||
}, 180, 10).Should(Equal(true))
|
||||
})
|
||||
|
||||
When("an endpoint is added after first run of odo dev", func() {
|
||||
|
||||
When("running odo dev", func() {
|
||||
var devSession helper.DevSession
|
||||
var ports map[string]string
|
||||
BeforeEach(func() {
|
||||
helper.ReplaceString("devfile.yaml", "exposure: none", "exposure: public")
|
||||
opts := []string{}
|
||||
if manual {
|
||||
opts = append(opts, "--no-watch")
|
||||
}
|
||||
var err error
|
||||
_, _, ports, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
devSession, _, _, ports, err = helper.StartDevMode(nil, opts...)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
It("should expose three endpoints on localhost", func() {
|
||||
|
||||
AfterEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should expose two endpoints on localhost", func() {
|
||||
url1 := fmt.Sprintf("http://%s", ports["3000"])
|
||||
url2 := fmt.Sprintf("http://%s", ports["4567"])
|
||||
url3 := fmt.Sprintf("http://%s", ports["7890"])
|
||||
|
||||
resp1, err := http.Get(url1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@@ -590,24 +580,98 @@ ComponentSettings:
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp2.Body.Close()
|
||||
|
||||
resp3, err := http.Get(url3)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp3.Body.Close()
|
||||
|
||||
body1, _ := io.ReadAll(resp1.Body)
|
||||
helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
body3, _ := io.ReadAll(resp3.Body)
|
||||
helper.MatchAllInOutput(string(body3), []string{"Hello from Node.js Starter Application!"})
|
||||
helper.ReplaceString("server.js", "Hello from Node.js", "H3110 from Node.js")
|
||||
|
||||
if manual {
|
||||
if os.Getenv("SKIP_KEY_PRESS") == "true" {
|
||||
Skip("This is a unix-terminal specific scenario, skipping")
|
||||
}
|
||||
|
||||
devSession.PressKey('p')
|
||||
}
|
||||
|
||||
_, _, _, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
Eventually(func() bool {
|
||||
resp3, err := http.Get(url1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp3.Body.Close()
|
||||
|
||||
resp4, err := http.Get(url2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp4.Body.Close()
|
||||
|
||||
body3, _ := io.ReadAll(resp3.Body)
|
||||
if string(body3) != "H3110 from Node.js Starter Application!" {
|
||||
return false
|
||||
}
|
||||
|
||||
body4, _ := io.ReadAll(resp4.Body)
|
||||
return string(body4) == "H3110 from Node.js Starter Application!"
|
||||
}, 180, 10).Should(Equal(true))
|
||||
})
|
||||
|
||||
When("an endpoint is added after first run of odo dev", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
helper.ReplaceString("devfile.yaml", "exposure: none", "exposure: public")
|
||||
|
||||
if manual {
|
||||
if os.Getenv("SKIP_KEY_PRESS") == "true" {
|
||||
Skip("This is a unix-terminal specific scenario, skipping")
|
||||
}
|
||||
|
||||
devSession.PressKey('p')
|
||||
}
|
||||
|
||||
var err error
|
||||
_, _, ports, err = devSession.WaitSync()
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
It("should expose three endpoints on localhost", func() {
|
||||
url1 := fmt.Sprintf("http://%s", ports["3000"])
|
||||
url2 := fmt.Sprintf("http://%s", ports["4567"])
|
||||
url3 := fmt.Sprintf("http://%s", ports["7890"])
|
||||
|
||||
resp1, err := http.Get(url1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp1.Body.Close()
|
||||
|
||||
resp2, err := http.Get(url2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp2.Body.Close()
|
||||
|
||||
resp3, err := http.Get(url3)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer resp3.Body.Close()
|
||||
|
||||
body1, _ := io.ReadAll(resp1.Body)
|
||||
helper.MatchAllInOutput(string(body1), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
body2, _ := io.ReadAll(resp2.Body)
|
||||
helper.MatchAllInOutput(string(body2), []string{"Hello from Node.js Starter Application!"})
|
||||
|
||||
body3, _ := io.ReadAll(resp3.Body)
|
||||
helper.MatchAllInOutput(string(body3), []string{"Hello from Node.js Starter Application!"})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for _, devfileHandlerCtx := range []struct {
|
||||
name string
|
||||
@@ -1292,10 +1356,6 @@ ComponentSettings:
|
||||
session, sessionOut, sessionErr, ports, err = helper.StartDevMode([]string{"PODMAN_CMD=echo"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
AfterEach(func() {
|
||||
session.Stop()
|
||||
session.WaitEnd()
|
||||
})
|
||||
It("should execute the composite apply commands successfully", func() {
|
||||
checkDeploymentExists := func() {
|
||||
out := commonVar.CliRunner.Run("get", "deployments", deploymentName).Out.Contents()
|
||||
@@ -2341,6 +2401,34 @@ CMD ["npm", "start"]
|
||||
})
|
||||
}
|
||||
|
||||
When("a component with multiple endpoints is run", func() {
|
||||
stateFile := ".odo/devstate.json"
|
||||
var devSession helper.DevSession
|
||||
BeforeEach(func() {
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project-with-multiple-endpoints"), commonVar.Context)
|
||||
helper.Cmd("odo", "set", "project", commonVar.Project).ShouldPass()
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile-with-multiple-endpoints.yaml")).ShouldPass()
|
||||
Expect(helper.VerifyFileExists(".odo/devstate.json")).To(BeFalse())
|
||||
var err error
|
||||
devSession, _, _, _, err = helper.StartDevMode(nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("odo dev is stopped", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should remove forwarded ports from state file", func() {
|
||||
Expect(helper.VerifyFileExists(stateFile)).To(BeTrue())
|
||||
contentJSON, err := ioutil.ReadFile(stateFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
helper.JsonPathContentIs(string(contentJSON), "forwardedPorts", "")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("a component with multiple endpoints is run", func() {
|
||||
stateFile := ".odo/devstate.json"
|
||||
var devSession helper.DevSession
|
||||
@@ -2373,20 +2461,6 @@ CMD ["npm", "start"]
|
||||
helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.0.localPort")
|
||||
helper.JsonPathContentIsValidUserPort(string(contentJSON), "forwardedPorts.1.localPort")
|
||||
})
|
||||
|
||||
When("odo dev is stopped", func() {
|
||||
BeforeEach(func() {
|
||||
devSession.Stop()
|
||||
devSession.WaitEnd()
|
||||
})
|
||||
|
||||
It("should remove forwarded ports from state file", func() {
|
||||
Expect(helper.VerifyFileExists(stateFile)).To(BeTrue())
|
||||
contentJSON, err := ioutil.ReadFile(stateFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
helper.JsonPathContentIs(string(contentJSON), "forwardedPorts", "")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("a devfile with a local parent is used for odo dev and the parent is not synced", func() {
|
||||
|
||||
@@ -60,7 +60,7 @@ var _ = Describe("odo dev interactive command tests", func() {
|
||||
helper.ExpectString(ctx, "Enter component name")
|
||||
helper.SendLine(ctx, "my-app")
|
||||
|
||||
helper.ExpectString(ctx, "Press Ctrl+c to exit")
|
||||
helper.ExpectString(ctx, "[Ctrl+c] - Exit")
|
||||
ctx.StopCommand()
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ var _ = Describe("odo dev interactive command tests", func() {
|
||||
helper.ExpectString(ctx, "Enter component name")
|
||||
helper.SendLine(ctx, "my-app")
|
||||
|
||||
helper.ExpectString(ctx, "Press Ctrl+c to exit")
|
||||
helper.ExpectString(ctx, "[Ctrl+c] - Exit")
|
||||
ctx.StopCommand()
|
||||
})
|
||||
|
||||
@@ -129,7 +129,7 @@ var _ = Describe("odo dev interactive command tests", func() {
|
||||
helper.ExpectString(ctx, "Enter component name")
|
||||
helper.SendLine(ctx, "my-app")
|
||||
|
||||
helper.ExpectString(ctx, "Press Ctrl+c to exit")
|
||||
helper.ExpectString(ctx, "[Ctrl+c] - Exit")
|
||||
ctx.StopCommand()
|
||||
})
|
||||
|
||||
@@ -140,4 +140,28 @@ var _ = Describe("odo dev interactive command tests", func() {
|
||||
"odo will try to autodetect the language and project type in order to select the best suited Devfile for your project."))
|
||||
})
|
||||
})
|
||||
|
||||
When("a component is bootstrapped", func() {
|
||||
|
||||
var cmpName string
|
||||
|
||||
BeforeEach(func() {
|
||||
cmpName = helper.RandString(6)
|
||||
helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context)
|
||||
helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", helper.GetExamplePath("source", "devfiles", "nodejs", "devfile.yaml")).ShouldPass()
|
||||
})
|
||||
|
||||
It("should sync files when p is pressed", func() {
|
||||
_, _ = helper.RunInteractive([]string{"odo", "dev", "--random-ports", "--no-watch"},
|
||||
nil,
|
||||
func(ctx helper.InteractiveContext) {
|
||||
helper.ExpectString(ctx, "[p] - Manually apply")
|
||||
|
||||
helper.PressKey(ctx, 'p')
|
||||
|
||||
helper.ExpectString(ctx, "Pushing files")
|
||||
ctx.StopCommand()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
5
vendor/github.com/hinshun/vt10x/.travis.yml
generated
vendored
5
vendor/github.com/hinshun/vt10x/.travis.yml
generated
vendored
@@ -1,5 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.10.2"
|
||||
- master
|
||||
19
vendor/github.com/hinshun/vt10x/LICENSE
generated
vendored
19
vendor/github.com/hinshun/vt10x/LICENSE
generated
vendored
@@ -1,19 +0,0 @@
|
||||
Copyright (C) 2013 James Gray
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without liitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and thismssion notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
vendor/github.com/hinshun/vt10x/README.md
generated
vendored
9
vendor/github.com/hinshun/vt10x/README.md
generated
vendored
@@ -1,9 +0,0 @@
|
||||
# vt10x
|
||||
|
||||
[](https://travis-ci.org/hinshun/vt10x)
|
||||
[](https://godoc.org/github.com/hinshun/vt10x)
|
||||
|
||||
Package vt10x is a vt10x terminal emulation backend, influenced
|
||||
largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal
|
||||
muxing, a terminal emulation frontend, or wherever else you need
|
||||
terminal emulation.
|
||||
38
vendor/github.com/hinshun/vt10x/color.go
generated
vendored
38
vendor/github.com/hinshun/vt10x/color.go
generated
vendored
@@ -1,38 +0,0 @@
|
||||
package vt10x
|
||||
|
||||
// ANSI color values
|
||||
const (
|
||||
Black Color = iota
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
LightGrey
|
||||
DarkGrey
|
||||
LightRed
|
||||
LightGreen
|
||||
LightYellow
|
||||
LightBlue
|
||||
LightMagenta
|
||||
LightCyan
|
||||
White
|
||||
)
|
||||
|
||||
// Default colors are potentially distinct to allow for special behavior.
|
||||
// For example, a transparent background. Otherwise, the simple case is to
|
||||
// map default colors to another color.
|
||||
const (
|
||||
DefaultFG Color = 1<<24 + iota
|
||||
DefaultBG
|
||||
DefaultCursor
|
||||
)
|
||||
|
||||
// Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256).
|
||||
type Color uint32
|
||||
|
||||
// ANSI returns true if Color is within [0, 16).
|
||||
func (c Color) ANSI() bool {
|
||||
return (c < 16)
|
||||
}
|
||||
189
vendor/github.com/hinshun/vt10x/csi.go
generated
vendored
189
vendor/github.com/hinshun/vt10x/csi.go
generated
vendored
@@ -1,189 +0,0 @@
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CSI (Control Sequence Introducer)
|
||||
// ESC+[
|
||||
type csiEscape struct {
|
||||
buf []byte
|
||||
args []int
|
||||
mode byte
|
||||
priv bool
|
||||
}
|
||||
|
||||
func (c *csiEscape) reset() {
|
||||
c.buf = c.buf[:0]
|
||||
c.args = c.args[:0]
|
||||
c.mode = 0
|
||||
c.priv = false
|
||||
}
|
||||
|
||||
func (c *csiEscape) put(b byte) bool {
|
||||
c.buf = append(c.buf, b)
|
||||
if b >= 0x40 && b <= 0x7E || len(c.buf) >= 256 {
|
||||
c.parse()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *csiEscape) parse() {
|
||||
c.mode = c.buf[len(c.buf)-1]
|
||||
if len(c.buf) == 1 {
|
||||
return
|
||||
}
|
||||
s := string(c.buf)
|
||||
c.args = c.args[:0]
|
||||
if s[0] == '?' {
|
||||
c.priv = true
|
||||
s = s[1:]
|
||||
}
|
||||
s = s[:len(s)-1]
|
||||
ss := strings.Split(s, ";")
|
||||
for _, p := range ss {
|
||||
i, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
//t.logf("invalid CSI arg '%s'\n", p)
|
||||
break
|
||||
}
|
||||
c.args = append(c.args, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *csiEscape) arg(i, def int) int {
|
||||
if i >= len(c.args) || i < 0 {
|
||||
return def
|
||||
}
|
||||
return c.args[i]
|
||||
}
|
||||
|
||||
// maxarg takes the maximum of arg(i, def) and def
|
||||
func (c *csiEscape) maxarg(i, def int) int {
|
||||
return max(c.arg(i, def), def)
|
||||
}
|
||||
|
||||
func (t *State) handleCSI() {
|
||||
c := &t.csi
|
||||
switch c.mode {
|
||||
default:
|
||||
goto unknown
|
||||
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))
|
||||
case 'B', 'e': // CUD, VPR - cursor <n> down
|
||||
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)
|
||||
case 'D': // CUB - cursor <n> backward
|
||||
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))
|
||||
case 'F': // CPL - cursor <n> up and first col
|
||||
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
|
||||
// clear all tabs
|
||||
case 3:
|
||||
for i := range t.tabs {
|
||||
t.tabs[i] = false
|
||||
}
|
||||
default:
|
||||
goto unknown
|
||||
}
|
||||
case 'G', '`': // CHA, HPA - Move to <col>
|
||||
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
|
||||
n := c.arg(0, 1)
|
||||
for i := 0; i < n; i++ {
|
||||
t.putTab(true)
|
||||
}
|
||||
case 'J': // ED - clear screen
|
||||
// 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)
|
||||
}
|
||||
case 1: // above
|
||||
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)
|
||||
case 2: // all
|
||||
t.clear(0, 0, t.cols-1, t.rows-1)
|
||||
default:
|
||||
goto unknown
|
||||
}
|
||||
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)
|
||||
case 1: // left
|
||||
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)
|
||||
}
|
||||
case 'S': // SU - scroll <n> lines up
|
||||
t.scrollUp(t.top, c.arg(0, 1))
|
||||
case 'T': // SD - scroll <n> lines down
|
||||
t.scrollDown(t.top, c.arg(0, 1))
|
||||
case 'L': // IL - insert <n> blank lines
|
||||
t.insertBlankLines(c.arg(0, 1))
|
||||
case 'l': // RM - reset mode
|
||||
t.setMode(c.priv, false, c.args)
|
||||
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)
|
||||
case 'P': // DCH - delete <n> chars
|
||||
t.deleteChars(c.arg(0, 1))
|
||||
case 'Z': // CBT - cursor backward tabulation <n> tab stops
|
||||
n := c.arg(0, 1)
|
||||
for i := 0; i < n; i++ {
|
||||
t.putTab(false)
|
||||
}
|
||||
case 'd': // VPA - move to <row>
|
||||
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)
|
||||
t.setAttr(c.args)
|
||||
case 'n':
|
||||
switch c.arg(0, 0) {
|
||||
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)))
|
||||
}
|
||||
case 'r': // DECSTBM - set scrolling region
|
||||
if c.priv {
|
||||
goto unknown
|
||||
} else {
|
||||
t.setScroll(c.arg(0, 1)-1, c.arg(1, t.rows)-1)
|
||||
t.moveAbsTo(0, 0)
|
||||
}
|
||||
case 's': // DECSC - save cursor position (ANSI.SYS)
|
||||
t.saveCursor()
|
||||
case 'u': // DECRC - restore cursor position (ANSI.SYS)
|
||||
t.restoreCursor()
|
||||
}
|
||||
return
|
||||
unknown: // TODO: get rid of this goto
|
||||
t.logf("unknown CSI sequence '%c'\n", c.mode)
|
||||
// TODO: c.dump()
|
||||
}
|
||||
9
vendor/github.com/hinshun/vt10x/doc.go
generated
vendored
9
vendor/github.com/hinshun/vt10x/doc.go
generated
vendored
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
Package terminal is a vt10x terminal emulation backend, influenced
|
||||
largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal
|
||||
muxing, a terminal emulation frontend, or wherever else you need
|
||||
terminal emulation.
|
||||
|
||||
In development, but very usable.
|
||||
*/
|
||||
package vt10x
|
||||
15
vendor/github.com/hinshun/vt10x/ioctl_other.go
generated
vendored
15
vendor/github.com/hinshun/vt10x/ioctl_other.go
generated
vendored
@@ -1,15 +0,0 @@
|
||||
// +build plan9 nacl windows
|
||||
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func ioctl(f *os.File, cmd, p uintptr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResizePty(*os.File) error {
|
||||
return nil
|
||||
}
|
||||
31
vendor/github.com/hinshun/vt10x/ioctl_posix.go
generated
vendored
31
vendor/github.com/hinshun/vt10x/ioctl_posix.go
generated
vendored
@@ -1,31 +0,0 @@
|
||||
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
|
||||
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func ioctl(f *os.File, cmd, p uintptr) error {
|
||||
_, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
f.Fd(),
|
||||
syscall.TIOCSWINSZ,
|
||||
p)
|
||||
if errno != 0 {
|
||||
return syscall.Errno(errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResizePty(pty *os.File, cols, rows int) error {
|
||||
var w struct{ row, col, xpix, ypix uint16 }
|
||||
w.row = uint16(rows)
|
||||
w.col = uint16(cols)
|
||||
w.xpix = 16 * uint16(cols)
|
||||
w.ypix = 16 * uint16(rows)
|
||||
return ioctl(pty, syscall.TIOCSWINSZ,
|
||||
uintptr(unsafe.Pointer(&w)))
|
||||
}
|
||||
203
vendor/github.com/hinshun/vt10x/parse.go
generated
vendored
203
vendor/github.com/hinshun/vt10x/parse.go
generated
vendored
@@ -1,203 +0,0 @@
|
||||
package vt10x
|
||||
|
||||
func isControlCode(c rune) bool {
|
||||
return c < 0x20 || c == 0177
|
||||
}
|
||||
|
||||
func (t *State) parse(c rune) {
|
||||
t.logf("%q", string(c))
|
||||
if isControlCode(c) {
|
||||
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
|
||||
t.newline(true)
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
t.cur.State |= cursorWrapNext
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) parseEsc(c rune) {
|
||||
if t.handleControlCodes(c) {
|
||||
return
|
||||
}
|
||||
next := t.parse
|
||||
t.logf("%q", string(c))
|
||||
switch c {
|
||||
case '[':
|
||||
next = t.parseEscCSI
|
||||
case '#':
|
||||
next = t.parseEscTest
|
||||
case 'P', // DCS - Device Control String
|
||||
'_', // APC - Application Program Command
|
||||
'^', // PM - Privacy Message
|
||||
']', // OSC - Operating System Command
|
||||
'k': // old title set compatibility
|
||||
t.str.reset()
|
||||
t.str.typ = c
|
||||
next = t.parseEscStr
|
||||
case '(': // set primary charset G0
|
||||
next = t.parseEscAltCharset
|
||||
case ')', // set secondary charset G1 (ignored)
|
||||
'*', // set tertiary charset G2 (ignored)
|
||||
'+': // set quaternary charset G3 (ignored)
|
||||
case 'D': // IND - linefeed
|
||||
if t.cur.Y == t.bottom {
|
||||
t.scrollUp(t.top, 1)
|
||||
} else {
|
||||
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
|
||||
case 'M': // RI - reverse index
|
||||
if t.cur.Y == t.top {
|
||||
t.scrollDown(t.top, 1)
|
||||
} else {
|
||||
t.moveTo(t.cur.X, t.cur.Y-1)
|
||||
}
|
||||
case 'Z': // DECID - identify terminal
|
||||
// TODO: write to our writer our id
|
||||
case 'c': // RIS - reset to initial state
|
||||
t.reset()
|
||||
case '=': // DECPAM - application keypad
|
||||
t.mode |= ModeAppKeypad
|
||||
case '>': // DECPNM - normal keypad
|
||||
t.mode &^= ModeAppKeypad
|
||||
case '7': // DECSC - save cursor
|
||||
t.saveCursor()
|
||||
case '8': // DECRC - restore cursor
|
||||
t.restoreCursor()
|
||||
case '\\': // ST - stop
|
||||
default:
|
||||
t.logf("unknown ESC sequence '%c'\n", c)
|
||||
}
|
||||
t.state = next
|
||||
}
|
||||
|
||||
func (t *State) parseEscCSI(c rune) {
|
||||
if t.handleControlCodes(c) {
|
||||
return
|
||||
}
|
||||
t.logf("%q", string(c))
|
||||
if t.csi.put(byte(c)) {
|
||||
t.state = t.parse
|
||||
t.handleCSI()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) parseEscStr(c rune) {
|
||||
t.logf("%q", string(c))
|
||||
switch c {
|
||||
case '\033':
|
||||
t.state = t.parseEscStrEnd
|
||||
case '\a': // backwards compatiblity to xterm
|
||||
t.state = t.parse
|
||||
t.handleSTR()
|
||||
default:
|
||||
t.str.put(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) parseEscStrEnd(c rune) {
|
||||
if t.handleControlCodes(c) {
|
||||
return
|
||||
}
|
||||
t.logf("%q", string(c))
|
||||
t.state = t.parse
|
||||
if c == '\\' {
|
||||
t.handleSTR()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) parseEscAltCharset(c rune) {
|
||||
if t.handleControlCodes(c) {
|
||||
return
|
||||
}
|
||||
t.logf("%q", string(c))
|
||||
switch c {
|
||||
case '0': // line drawing set
|
||||
t.cur.Attr.Mode |= attrGfx
|
||||
case 'B': // USASCII
|
||||
t.cur.Attr.Mode &^= attrGfx
|
||||
case 'A', // UK (ignored)
|
||||
'<', // multinational (ignored)
|
||||
'5', // Finnish (ignored)
|
||||
'C', // Finnish (ignored)
|
||||
'K': // German (ignored)
|
||||
default:
|
||||
t.logf("unknown alt. charset '%c'\n", c)
|
||||
}
|
||||
t.state = t.parse
|
||||
}
|
||||
|
||||
func (t *State) parseEscTest(c rune) {
|
||||
if t.handleControlCodes(c) {
|
||||
return
|
||||
}
|
||||
// DEC screen alignment test
|
||||
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.state = t.parse
|
||||
}
|
||||
|
||||
func (t *State) handleControlCodes(c rune) bool {
|
||||
if !isControlCode(c) {
|
||||
return false
|
||||
}
|
||||
switch c {
|
||||
// HT
|
||||
case '\t':
|
||||
t.putTab(true)
|
||||
// BS
|
||||
case '\b':
|
||||
t.moveTo(t.cur.X-1, t.cur.Y)
|
||||
// CR
|
||||
case '\r':
|
||||
t.moveTo(0, t.cur.Y)
|
||||
// LF, VT, LF
|
||||
case '\f', '\v', '\n':
|
||||
// go to first col if mode is set
|
||||
t.newline(t.mode&ModeCRLF != 0)
|
||||
// BEL
|
||||
case '\a':
|
||||
// TODO: emit sound
|
||||
// TODO: window alert if not focused
|
||||
// ESC
|
||||
case 033:
|
||||
t.csi.reset()
|
||||
t.state = t.parseEsc
|
||||
// SO, SI
|
||||
case 016, 017:
|
||||
// different charsets not supported. apps should use the correct
|
||||
// alt charset escapes, probably for line drawing
|
||||
// SUB, CAN
|
||||
case 032, 030:
|
||||
t.csi.reset()
|
||||
// ignore ENQ, NUL, XON, XOFF, DEL
|
||||
case 005, 000, 021, 023, 0177:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
762
vendor/github.com/hinshun/vt10x/state.go
generated
vendored
762
vendor/github.com/hinshun/vt10x/state.go
generated
vendored
@@ -1,762 +0,0 @@
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
tabspaces = 8
|
||||
)
|
||||
|
||||
const (
|
||||
attrReverse = 1 << iota
|
||||
attrUnderline
|
||||
attrBold
|
||||
attrGfx
|
||||
attrItalic
|
||||
attrBlink
|
||||
attrWrap
|
||||
)
|
||||
|
||||
const (
|
||||
cursorDefault = 1 << iota
|
||||
cursorWrapNext
|
||||
cursorOrigin
|
||||
)
|
||||
|
||||
// ModeFlag represents various terminal mode states.
|
||||
type ModeFlag uint32
|
||||
|
||||
// Terminal modes
|
||||
const (
|
||||
ModeWrap ModeFlag = 1 << iota
|
||||
ModeInsert
|
||||
ModeAppKeypad
|
||||
ModeAltScreen
|
||||
ModeCRLF
|
||||
ModeMouseButton
|
||||
ModeMouseMotion
|
||||
ModeReverse
|
||||
ModeKeyboardLock
|
||||
ModeHide
|
||||
ModeEcho
|
||||
ModeAppCursor
|
||||
ModeMouseSgr
|
||||
Mode8bit
|
||||
ModeBlink
|
||||
ModeFBlink
|
||||
ModeFocus
|
||||
ModeMouseX10
|
||||
ModeMouseMany
|
||||
ModeMouseMask = ModeMouseButton | ModeMouseMotion | ModeMouseX10 | ModeMouseMany
|
||||
)
|
||||
|
||||
// ChangeFlag represents possible state changes of the terminal.
|
||||
type ChangeFlag uint32
|
||||
|
||||
// Terminal changes to occur in VT.ReadState
|
||||
const (
|
||||
ChangedScreen ChangeFlag = 1 << iota
|
||||
ChangedTitle
|
||||
)
|
||||
|
||||
type Glyph struct {
|
||||
Char rune
|
||||
Mode int16
|
||||
FG, BG Color
|
||||
}
|
||||
|
||||
type line []Glyph
|
||||
|
||||
type Cursor struct {
|
||||
Attr Glyph
|
||||
X, Y int
|
||||
State uint8
|
||||
}
|
||||
|
||||
type parseState func(c rune)
|
||||
|
||||
// State represents the terminal emulation state. Use Lock/Unlock
|
||||
// methods to synchronize data access with VT.
|
||||
type State struct {
|
||||
DebugLogger *log.Logger
|
||||
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
changed ChangeFlag
|
||||
cols, rows int
|
||||
lines []line
|
||||
altLines []line
|
||||
dirty []bool // line dirtiness
|
||||
anydirty bool
|
||||
cur, curSaved Cursor
|
||||
top, bottom int // scroll limits
|
||||
mode ModeFlag
|
||||
state parseState
|
||||
str strEscape
|
||||
csi csiEscape
|
||||
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{}) {
|
||||
if t.DebugLogger != nil {
|
||||
t.DebugLogger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) logln(s string) {
|
||||
if t.DebugLogger != nil {
|
||||
t.DebugLogger.Println(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) lock() {
|
||||
t.mu.Lock()
|
||||
}
|
||||
|
||||
func (t *State) unlock() {
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// Lock locks the state object's mutex.
|
||||
func (t *State) Lock() {
|
||||
t.mu.Lock()
|
||||
}
|
||||
|
||||
// Unlock resets change flags and unlocks the state object's mutex.
|
||||
func (t *State) Unlock() {
|
||||
t.resetChanges()
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// 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() Cursor {
|
||||
return t.cur
|
||||
}
|
||||
|
||||
// CursorVisible returns the visible state of the cursor.
|
||||
func (t *State) CursorVisible() bool {
|
||||
return t.mode&ModeHide == 0
|
||||
}
|
||||
|
||||
// Mode returns the current terminal mode.
|
||||
func (t *State) Mode() ModeFlag {
|
||||
return t.mode
|
||||
}
|
||||
|
||||
// Title returns the current title set via the tty.
|
||||
func (t *State) Title() string {
|
||||
return t.title
|
||||
}
|
||||
|
||||
/*
|
||||
// ChangeMask returns a bitfield of changes that have occured by VT.
|
||||
func (t *State) ChangeMask() ChangeFlag {
|
||||
return t.changed
|
||||
}
|
||||
*/
|
||||
|
||||
// Changed returns true if change has occured.
|
||||
func (t *State) Changed(change ChangeFlag) bool {
|
||||
return t.changed&change != 0
|
||||
}
|
||||
|
||||
// resetChanges resets the change mask and dirtiness.
|
||||
func (t *State) resetChanges() {
|
||||
for i := range t.dirty {
|
||||
t.dirty[i] = false
|
||||
}
|
||||
t.anydirty = false
|
||||
t.changed = 0
|
||||
}
|
||||
|
||||
func (t *State) saveCursor() {
|
||||
t.curSaved = t.cur
|
||||
}
|
||||
|
||||
func (t *State) restoreCursor() {
|
||||
t.cur = t.curSaved
|
||||
t.moveTo(t.cur.X, t.cur.Y)
|
||||
}
|
||||
|
||||
func (t *State) put(c rune) {
|
||||
t.state(c)
|
||||
}
|
||||
|
||||
func (t *State) putTab(forward bool) {
|
||||
x := t.cur.X
|
||||
if forward {
|
||||
if x == t.cols {
|
||||
return
|
||||
}
|
||||
for x++; x < t.cols && !t.tabs[x]; x++ {
|
||||
}
|
||||
} else {
|
||||
if x == 0 {
|
||||
return
|
||||
}
|
||||
for x--; x > 0 && !t.tabs[x]; x-- {
|
||||
}
|
||||
}
|
||||
t.moveTo(x, t.cur.Y)
|
||||
}
|
||||
|
||||
func (t *State) newline(firstCol bool) {
|
||||
y := t.cur.Y
|
||||
if y == t.bottom {
|
||||
cur := t.cur
|
||||
t.cur = t.defaultCursor()
|
||||
t.scrollUp(t.top, 1)
|
||||
t.cur = cur
|
||||
} else {
|
||||
y++
|
||||
}
|
||||
if firstCol {
|
||||
t.moveTo(0, y)
|
||||
} else {
|
||||
t.moveTo(t.cur.X, y)
|
||||
}
|
||||
}
|
||||
|
||||
// table from st, which in turn is from rxvt :)
|
||||
var gfxCharTable = [62]rune{
|
||||
'↑', '↓', '→', '←', '█', '▚', '☃', // A - G
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // H - O
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // P - W
|
||||
0, 0, 0, 0, 0, 0, 0, ' ', // X - _
|
||||
'◆', '▒', '␉', '␌', '␍', '␊', '°', '±', // ` - g
|
||||
'', '␋', '┘', '┐', '┌', '└', '┼', '⎺', // h - o
|
||||
'⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', // p - w
|
||||
'│', '≤', '≥', 'π', '≠', '£', '·', // x - ~
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
t.changed |= ChangedScreen
|
||||
t.dirty[y] = true
|
||||
t.lines[y][x] = *attr
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) defaultCursor() Cursor {
|
||||
c := Cursor{}
|
||||
c.Attr.FG = DefaultFG
|
||||
c.Attr.BG = DefaultBG
|
||||
return c
|
||||
}
|
||||
|
||||
func (t *State) reset() {
|
||||
t.cur = t.defaultCursor()
|
||||
t.saveCursor()
|
||||
for i := range t.tabs {
|
||||
t.tabs[i] = false
|
||||
}
|
||||
for i := tabspaces; i < len(t.tabs); i += tabspaces {
|
||||
t.tabs[i] = true
|
||||
}
|
||||
t.top = 0
|
||||
t.bottom = t.rows - 1
|
||||
t.mode = ModeWrap
|
||||
t.clear(0, 0, t.rows-1, t.cols-1)
|
||||
t.moveTo(0, 0)
|
||||
}
|
||||
|
||||
// TODO: definitely can improve allocs
|
||||
func (t *State) resize(cols, rows int) bool {
|
||||
if cols == t.cols && rows == t.rows {
|
||||
return false
|
||||
}
|
||||
if cols < 1 || rows < 1 {
|
||||
return false
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
lines, altLines, tabs := t.lines, t.altLines, t.tabs
|
||||
t.lines = make([]line, rows)
|
||||
t.altLines = make([]line, rows)
|
||||
t.dirty = make([]bool, rows)
|
||||
t.tabs = make([]bool, cols)
|
||||
|
||||
minrows := min(rows, t.rows)
|
||||
mincols := min(cols, t.cols)
|
||||
t.changed |= ChangedScreen
|
||||
for i := 0; i < rows; i++ {
|
||||
t.dirty[i] = true
|
||||
t.lines[i] = make(line, cols)
|
||||
t.altLines[i] = make(line, cols)
|
||||
}
|
||||
for i := 0; i < minrows; i++ {
|
||||
copy(t.lines[i], lines[i])
|
||||
copy(t.altLines[i], altLines[i])
|
||||
}
|
||||
copy(t.tabs, tabs)
|
||||
if cols > t.cols {
|
||||
i := t.cols - 1
|
||||
for i > 0 && !tabs[i] {
|
||||
i--
|
||||
}
|
||||
for i += tabspaces; i < len(tabs); i += tabspaces {
|
||||
tabs[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
t.cols = cols
|
||||
t.rows = rows
|
||||
t.setScroll(0, rows-1)
|
||||
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)
|
||||
}
|
||||
if cols > 0 && minrows < rows {
|
||||
t.clear(0, minrows, cols-1, rows-1)
|
||||
}
|
||||
t.swapScreen()
|
||||
}
|
||||
return slide > 0
|
||||
}
|
||||
|
||||
func (t *State) clear(x0, y0, x1, y1 int) {
|
||||
if x0 > x1 {
|
||||
x0, x1 = x1, x0
|
||||
}
|
||||
if y0 > y1 {
|
||||
y0, y1 = y1, y0
|
||||
}
|
||||
x0 = clamp(x0, 0, t.cols-1)
|
||||
x1 = clamp(x1, 0, t.cols-1)
|
||||
y0 = clamp(y0, 0, t.rows-1)
|
||||
y1 = clamp(y1, 0, t.rows-1)
|
||||
t.changed |= ChangedScreen
|
||||
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].Char = ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) clearAll() {
|
||||
t.clear(0, 0, t.cols-1, t.rows-1)
|
||||
}
|
||||
|
||||
func (t *State) moveAbsTo(x, y int) {
|
||||
if t.cur.State&cursorOrigin != 0 {
|
||||
y += t.top
|
||||
}
|
||||
t.moveTo(x, y)
|
||||
}
|
||||
|
||||
func (t *State) moveTo(x, y int) {
|
||||
var miny, maxy int
|
||||
if t.cur.State&cursorOrigin != 0 {
|
||||
miny = t.top
|
||||
maxy = t.bottom
|
||||
} else {
|
||||
miny = 0
|
||||
maxy = t.rows - 1
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (t *State) swapScreen() {
|
||||
t.lines, t.altLines = t.altLines, t.lines
|
||||
t.mode ^= ModeAltScreen
|
||||
t.dirtyAll()
|
||||
}
|
||||
|
||||
func (t *State) dirtyAll() {
|
||||
t.changed |= ChangedScreen
|
||||
for y := 0; y < t.rows; y++ {
|
||||
t.dirty[y] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) setScroll(top, bottom int) {
|
||||
top = clamp(top, 0, t.rows-1)
|
||||
bottom = clamp(bottom, 0, t.rows-1)
|
||||
if top > bottom {
|
||||
top, bottom = bottom, top
|
||||
}
|
||||
t.top = top
|
||||
t.bottom = bottom
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func clamp(val, min, max int) int {
|
||||
if val < min {
|
||||
return min
|
||||
} else if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func between(val, min, max int) bool {
|
||||
if val < min || val > max {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *State) scrollDown(orig, n int) {
|
||||
n = clamp(n, 0, t.bottom-orig+1)
|
||||
t.clear(0, t.bottom-n+1, t.cols-1, t.bottom)
|
||||
t.changed |= ChangedScreen
|
||||
for i := t.bottom; i >= orig+n; i-- {
|
||||
t.lines[i], t.lines[i-n] = t.lines[i-n], t.lines[i]
|
||||
t.dirty[i] = true
|
||||
t.dirty[i-n] = true
|
||||
}
|
||||
|
||||
// TODO: selection scroll
|
||||
}
|
||||
|
||||
func (t *State) scrollUp(orig, n int) {
|
||||
n = clamp(n, 0, t.bottom-orig+1)
|
||||
t.clear(0, orig, t.cols-1, orig+n-1)
|
||||
t.changed |= ChangedScreen
|
||||
for i := orig; i <= t.bottom-n; i++ {
|
||||
t.lines[i], t.lines[i+n] = t.lines[i+n], t.lines[i]
|
||||
t.dirty[i] = true
|
||||
t.dirty[i+n] = true
|
||||
}
|
||||
|
||||
// TODO: selection scroll
|
||||
}
|
||||
|
||||
func (t *State) modMode(set bool, bit ModeFlag) {
|
||||
if set {
|
||||
t.mode |= bit
|
||||
} else {
|
||||
t.mode &^= bit
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) setMode(priv bool, set bool, args []int) {
|
||||
if priv {
|
||||
for _, a := range args {
|
||||
switch a {
|
||||
case 1: // DECCKM - cursor key
|
||||
t.modMode(set, ModeAppCursor)
|
||||
case 5: // DECSCNM - reverse video
|
||||
mode := t.mode
|
||||
t.modMode(set, ModeReverse)
|
||||
if mode != t.mode {
|
||||
// TODO: redraw
|
||||
}
|
||||
case 6: // DECOM - origin
|
||||
if set {
|
||||
t.cur.State |= cursorOrigin
|
||||
} else {
|
||||
t.cur.State &^= cursorOrigin
|
||||
}
|
||||
t.moveAbsTo(0, 0)
|
||||
case 7: // DECAWM - auto wrap
|
||||
t.modMode(set, ModeWrap)
|
||||
// IGNORED:
|
||||
case 0, // error
|
||||
2, // DECANM - ANSI/VT52
|
||||
3, // DECCOLM - column
|
||||
4, // DECSCLM - scroll
|
||||
8, // DECARM - auto repeat
|
||||
18, // DECPFF - printer feed
|
||||
19, // DECPEX - printer extent
|
||||
42, // DECNRCM - national characters
|
||||
12: // att610 - start blinking cursor
|
||||
break
|
||||
case 25: // DECTCEM - text cursor enable mode
|
||||
t.modMode(!set, ModeHide)
|
||||
case 9: // X10 mouse compatibility mode
|
||||
t.modMode(false, ModeMouseMask)
|
||||
t.modMode(set, ModeMouseX10)
|
||||
case 1000: // report button press
|
||||
t.modMode(false, ModeMouseMask)
|
||||
t.modMode(set, ModeMouseButton)
|
||||
case 1002: // report motion on button press
|
||||
t.modMode(false, ModeMouseMask)
|
||||
t.modMode(set, ModeMouseMotion)
|
||||
case 1003: // enable all mouse motions
|
||||
t.modMode(false, ModeMouseMask)
|
||||
t.modMode(set, ModeMouseMany)
|
||||
case 1004: // send focus events to tty
|
||||
t.modMode(set, ModeFocus)
|
||||
case 1006: // extended reporting mode
|
||||
t.modMode(set, ModeMouseSgr)
|
||||
case 1034:
|
||||
t.modMode(set, Mode8bit)
|
||||
case 1049, // = 1047 and 1048
|
||||
47, 1047:
|
||||
alt := t.mode&ModeAltScreen != 0
|
||||
if alt {
|
||||
t.clear(0, 0, t.cols-1, t.rows-1)
|
||||
}
|
||||
if !set || !alt {
|
||||
t.swapScreen()
|
||||
}
|
||||
if a != 1049 {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case 1048:
|
||||
if set {
|
||||
t.saveCursor()
|
||||
} else {
|
||||
t.restoreCursor()
|
||||
}
|
||||
case 1001:
|
||||
// mouse highlight mode; can hang the terminal by design when
|
||||
// implemented
|
||||
case 1005:
|
||||
// utf8 mouse mode; will confuse applications not supporting
|
||||
// utf8 and luit
|
||||
case 1015:
|
||||
// urxvt mangled mouse mode; incompatiblt and can be mistaken
|
||||
// for other control codes
|
||||
default:
|
||||
t.logf("unknown private set/reset mode %d\n", a)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, a := range args {
|
||||
switch a {
|
||||
case 0: // Error (ignored)
|
||||
case 2: // KAM - keyboard action
|
||||
t.modMode(set, ModeKeyboardLock)
|
||||
case 4: // IRM - insertion-replacement
|
||||
t.modMode(set, ModeInsert)
|
||||
t.logln("insert mode not implemented")
|
||||
case 12: // SRM - send/receive
|
||||
t.modMode(set, ModeEcho)
|
||||
case 20: // LNM - linefeed/newline
|
||||
t.modMode(set, ModeCRLF)
|
||||
case 34:
|
||||
t.logln("right-to-left mode not implemented")
|
||||
case 96:
|
||||
t.logln("right-to-left copy mode not implemented")
|
||||
default:
|
||||
t.logf("unknown set/reset mode %d\n", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) setAttr(attr []int) {
|
||||
if len(attr) == 0 {
|
||||
attr = []int{0}
|
||||
}
|
||||
for i := 0; i < len(attr); i++ {
|
||||
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
|
||||
case 1:
|
||||
t.cur.Attr.Mode |= attrBold
|
||||
case 3:
|
||||
t.cur.Attr.Mode |= attrItalic
|
||||
case 4:
|
||||
t.cur.Attr.Mode |= attrUnderline
|
||||
case 5, 6: // slow, rapid blink
|
||||
t.cur.Attr.Mode |= attrBlink
|
||||
case 7:
|
||||
t.cur.Attr.Mode |= attrReverse
|
||||
case 21, 22:
|
||||
t.cur.Attr.Mode &^= attrBold
|
||||
case 23:
|
||||
t.cur.Attr.Mode &^= attrItalic
|
||||
case 24:
|
||||
t.cur.Attr.Mode &^= attrUnderline
|
||||
case 25, 26:
|
||||
t.cur.Attr.Mode &^= attrBlink
|
||||
case 27:
|
||||
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])
|
||||
} 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
|
||||
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])
|
||||
} 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
|
||||
default:
|
||||
if between(a, 30, 37) {
|
||||
t.cur.Attr.FG = Color(a - 30)
|
||||
} else if between(a, 40, 47) {
|
||||
t.cur.Attr.BG = Color(a - 40)
|
||||
} else if between(a, 90, 97) {
|
||||
t.cur.Attr.FG = Color(a - 90 + 8)
|
||||
} else if between(a, 100, 107) {
|
||||
t.cur.Attr.BG = Color(a - 100 + 8)
|
||||
} else {
|
||||
t.logf("gfx attr %d unknown\n", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) insertBlanks(n int) {
|
||||
src := t.cur.X
|
||||
dst := src + n
|
||||
size := t.cols - dst
|
||||
t.changed |= ChangedScreen
|
||||
t.dirty[t.cur.Y] = true
|
||||
|
||||
if dst >= t.cols {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) insertBlankLines(n int) {
|
||||
if t.cur.Y < t.top || t.cur.Y > t.bottom {
|
||||
return
|
||||
}
|
||||
t.scrollDown(t.cur.Y, n)
|
||||
}
|
||||
|
||||
func (t *State) deleteLines(n int) {
|
||||
if t.cur.Y < t.top || t.cur.Y > t.bottom {
|
||||
return
|
||||
}
|
||||
t.scrollUp(t.cur.Y, n)
|
||||
}
|
||||
|
||||
func (t *State) deleteChars(n int) {
|
||||
src := t.cur.X + n
|
||||
dst := t.cur.X
|
||||
size := t.cols - src
|
||||
t.changed |= ChangedScreen
|
||||
t.dirty[t.cur.Y] = true
|
||||
|
||||
if src >= t.cols {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *State) setTitle(title string) {
|
||||
t.changed |= ChangedTitle
|
||||
t.title = title
|
||||
}
|
||||
|
||||
func (t *State) Size() (cols, rows int) {
|
||||
return t.cols, t.rows
|
||||
}
|
||||
|
||||
func (t *State) String() string {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
var view []rune
|
||||
for y := 0; y < t.rows; y++ {
|
||||
for x := 0; x < t.cols; x++ {
|
||||
attr := t.Cell(x, y)
|
||||
view = append(view, attr.Char)
|
||||
}
|
||||
view = append(view, '\n')
|
||||
}
|
||||
|
||||
return string(view)
|
||||
}
|
||||
330
vendor/github.com/hinshun/vt10x/str.go
generated
vendored
330
vendor/github.com/hinshun/vt10x/str.go
generated
vendored
@@ -1,330 +0,0 @@
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// STR sequences are similar to CSI sequences, but have string arguments (and
|
||||
// as far as I can tell, don't really have a name; STR is the name I took from
|
||||
// suckless which I imagine comes from rxvt or xterm).
|
||||
type strEscape struct {
|
||||
typ rune
|
||||
buf []rune
|
||||
args []string
|
||||
}
|
||||
|
||||
func (s *strEscape) reset() {
|
||||
s.typ = 0
|
||||
s.buf = s.buf[:0]
|
||||
s.args = nil
|
||||
}
|
||||
|
||||
func (s *strEscape) put(c rune) {
|
||||
// TODO: improve allocs with an array backed slice; bench first
|
||||
if len(s.buf) < 256 {
|
||||
s.buf = append(s.buf, c)
|
||||
}
|
||||
// Going by st, it is better to remain silent when the STR sequence is not
|
||||
// ended so that it is apparent to users something is wrong. The length sanity
|
||||
// check ensures we don't absorb the entire stream into memory.
|
||||
// TODO: see what rxvt or xterm does
|
||||
}
|
||||
|
||||
func (s *strEscape) parse() {
|
||||
s.args = strings.Split(string(s.buf), ";")
|
||||
}
|
||||
|
||||
func (s *strEscape) arg(i, def int) int {
|
||||
if i >= len(s.args) || i < 0 {
|
||||
return def
|
||||
}
|
||||
i, err := strconv.Atoi(s.args[i])
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (s *strEscape) argString(i int, def string) string {
|
||||
if i >= len(s.args) || i < 0 {
|
||||
return def
|
||||
}
|
||||
return s.args[i]
|
||||
}
|
||||
|
||||
func (t *State) handleSTR() {
|
||||
s := &t.str
|
||||
s.parse()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c := s.argString(2, "")
|
||||
p = &c
|
||||
fallthrough
|
||||
case 104: // color reset
|
||||
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()
|
||||
}
|
||||
case 'k': // old title set compatibility
|
||||
title := s.argString(0, "")
|
||||
if title != "" {
|
||||
t.setTitle(title)
|
||||
}
|
||||
default:
|
||||
// TODO: Ignore these codes instead of complain?
|
||||
// 'P': // DSC - device control string
|
||||
// '_': // APC - application program command
|
||||
// '^': // PM - privacy message
|
||||
|
||||
t.logf("unhandled STR sequence '%c'\n", s.typ)
|
||||
// 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
89
vendor/github.com/hinshun/vt10x/vt.go
generated
vendored
@@ -1,89 +0,0 @@
|
||||
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)
|
||||
}
|
||||
107
vendor/github.com/hinshun/vt10x/vt_other.go
generated
vendored
107
vendor/github.com/hinshun/vt10x/vt_other.go
generated
vendored
@@ -1,107 +0,0 @@
|
||||
// +build plan9 nacl windows
|
||||
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type terminal struct {
|
||||
*State
|
||||
}
|
||||
|
||||
func newTerminal(info TerminalInfo) *terminal {
|
||||
t := &terminal{newState(info.w)}
|
||||
t.init(info.cols, info.rows)
|
||||
return t
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (t *terminal) Write(p []byte) (int, error) {
|
||||
var written int
|
||||
r := bytes.NewReader(p)
|
||||
t.lock()
|
||||
defer t.unlock()
|
||||
for {
|
||||
c, sz, err := r.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
written += sz
|
||||
if c == unicode.ReplacementChar && sz == 1 {
|
||||
if r.Len() == 0 {
|
||||
// not enough bytes for a full rune
|
||||
return written - 1, nil
|
||||
}
|
||||
t.logln("invalid utf8 sequence")
|
||||
continue
|
||||
}
|
||||
t.put(c)
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// TODO: add tests for expected blocking behavior
|
||||
func (t *terminal) Parse(br *bufio.Reader) error {
|
||||
var locked bool
|
||||
defer func() {
|
||||
if locked {
|
||||
t.unlock()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
c, sz, err := br.ReadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c == unicode.ReplacementChar && sz == 1 {
|
||||
t.logln("invalid utf8 sequence")
|
||||
break
|
||||
}
|
||||
if !locked {
|
||||
t.lock()
|
||||
locked = true
|
||||
}
|
||||
|
||||
// put rune for parsing and update state
|
||||
t.put(c)
|
||||
|
||||
// break if our buffer is empty, or if buffer contains an
|
||||
// incomplete rune.
|
||||
n := br.Buffered()
|
||||
if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fullRuneBuffered(br *bufio.Reader) bool {
|
||||
n := br.Buffered()
|
||||
buf, err := br.Peek(n)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return utf8.FullRune(buf)
|
||||
}
|
||||
|
||||
func (t *terminal) Resize(cols, rows int) {
|
||||
t.lock()
|
||||
defer t.unlock()
|
||||
_ = t.resize(cols, rows)
|
||||
}
|
||||
108
vendor/github.com/hinshun/vt10x/vt_posix.go
generated
vendored
108
vendor/github.com/hinshun/vt10x/vt_posix.go
generated
vendored
@@ -1,108 +0,0 @@
|
||||
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
|
||||
|
||||
package vt10x
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type terminal struct {
|
||||
*State
|
||||
}
|
||||
|
||||
func newTerminal(info TerminalInfo) *terminal {
|
||||
t := &terminal{newState(info.w)}
|
||||
t.init(info.cols, info.rows)
|
||||
return t
|
||||
}
|
||||
|
||||
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 *terminal) Write(p []byte) (int, error) {
|
||||
var written int
|
||||
r := bytes.NewReader(p)
|
||||
t.lock()
|
||||
defer t.unlock()
|
||||
for {
|
||||
c, sz, err := r.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
written += sz
|
||||
if c == unicode.ReplacementChar && sz == 1 {
|
||||
if r.Len() == 0 {
|
||||
// not enough bytes for a full rune
|
||||
return written - 1, nil
|
||||
}
|
||||
t.logln("invalid utf8 sequence")
|
||||
continue
|
||||
}
|
||||
t.put(c)
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// TODO: add tests for expected blocking behavior
|
||||
func (t *terminal) Parse(br *bufio.Reader) error {
|
||||
var locked bool
|
||||
defer func() {
|
||||
if locked {
|
||||
t.unlock()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
c, sz, err := br.ReadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c == unicode.ReplacementChar && sz == 1 {
|
||||
t.logln("invalid utf8 sequence")
|
||||
break
|
||||
}
|
||||
if !locked {
|
||||
t.lock()
|
||||
locked = true
|
||||
}
|
||||
|
||||
// put rune for parsing and update state
|
||||
t.put(c)
|
||||
|
||||
// break if our buffer is empty, or if buffer contains an
|
||||
// incomplete rune.
|
||||
n := br.Buffered()
|
||||
if n == 0 || (n < 4 && !fullRuneBuffered(br)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fullRuneBuffered(br *bufio.Reader) bool {
|
||||
n := br.Buffered()
|
||||
buf, err := br.Peek(n)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return utf8.FullRune(buf)
|
||||
}
|
||||
|
||||
func (t *terminal) Resize(cols, rows int) {
|
||||
t.lock()
|
||||
defer t.unlock()
|
||||
_ = t.resize(cols, rows)
|
||||
}
|
||||
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@@ -385,7 +385,6 @@ github.com/hashicorp/go-multierror
|
||||
github.com/hashicorp/go-version
|
||||
# github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
|
||||
## explicit; go 1.14
|
||||
github.com/hinshun/vt10x
|
||||
# github.com/imdario/mergo v0.3.12
|
||||
## explicit; go 1.13
|
||||
github.com/imdario/mergo
|
||||
|
||||
Reference in New Issue
Block a user