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:
Philippe Martin
2022-09-09 18:54:54 +02:00
committed by GitHub
parent 6c6f8ef5ad
commit 59f4f8348a
33 changed files with 726 additions and 2285 deletions

View File

@@ -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}

View File

@@ -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
View File

@@ -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
View 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
View 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
}

View 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
}

View 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
}

View 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

View 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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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]+)")

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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()
})
})
})
})

View File

@@ -1,5 +0,0 @@
language: go
go:
- "1.10.2"
- master

View File

@@ -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.

View File

@@ -1,9 +0,0 @@
# vt10x
[![Build Status](https://travis-ci.org/hinshun/vt10x.svg?branch=master)](https://travis-ci.org/hinshun/vt10x)
[![GoDoc](https://godoc.org/github.com/hinshun/vt10x?status.svg)](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.

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)))
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
View File

@@ -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