Skip to content

Commit ba6a88d

Browse files
committed
Experimental terminal UI
Only for `skaffold dev` Signed-off-by: David Gageot <[email protected]>
1 parent 093b209 commit ba6a88d

File tree

7 files changed

+147
-17
lines changed

7 files changed

+147
-17
lines changed

cmd/skaffold/app/cmd/dev.go

+106-4
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ package cmd
1919
import (
2020
"context"
2121
"io"
22+
"strings"
2223

24+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
25+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2326
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
2427
"github.com/pkg/errors"
28+
"github.com/rivo/tview"
2529
"github.com/sirupsen/logrus"
2630
"github.com/spf13/cobra"
2731
)
@@ -34,7 +38,7 @@ func NewCmdDev(out io.Writer) *cobra.Command {
3438
Args: cobra.NoArgs,
3539
RunE: func(cmd *cobra.Command, args []string) error {
3640
opts.Command = "dev"
37-
return dev(out)
41+
return dev(out, opts.ExperimentalGUI)
3842
},
3943
}
4044
AddRunDevFlags(cmd)
@@ -45,13 +49,17 @@ func NewCmdDev(out io.Writer) *cobra.Command {
4549
cmd.Flags().IntVarP(&opts.WatchPollInterval, "watch-poll-interval", "i", 1000, "Interval (in ms) between two checks for file changes")
4650
cmd.Flags().BoolVar(&opts.PortForward, "port-forward", true, "Port-forward exposed container ports within pods")
4751
cmd.Flags().StringArrayVarP(&opts.CustomLabels, "label", "l", nil, "Add custom labels to deployed objects. Set multiple times for multiple labels")
52+
cmd.Flags().BoolVar(&opts.ExperimentalGUI, "experimental-gui", false, "Experimental Graphical User Interface")
53+
4854
return cmd
4955
}
5056

51-
func dev(out io.Writer) error {
57+
func dev(out io.Writer, ui bool) error {
5258
ctx, cancel := context.WithCancel(context.Background())
5359
defer cancel()
54-
catchCtrlC(cancel)
60+
if !ui {
61+
catchCtrlC(cancel)
62+
}
5563

5664
cleanup := func() {}
5765
if opts.Cleanup {
@@ -60,6 +68,25 @@ func dev(out io.Writer) error {
6068
}()
6169
}
6270

71+
var (
72+
app *tview.Application
73+
output *config.Output
74+
)
75+
if ui {
76+
app, output = createApp()
77+
defer app.Stop()
78+
79+
go func() {
80+
app.Run()
81+
cancel()
82+
}()
83+
} else {
84+
output = &config.Output{
85+
Main: out,
86+
Logs: out,
87+
}
88+
}
89+
6390
for {
6491
select {
6592
case <-ctx.Done():
@@ -70,7 +97,7 @@ func dev(out io.Writer) error {
7097
return errors.Wrap(err, "creating runner")
7198
}
7299

73-
err = r.Dev(ctx, out, config.Build.Artifacts)
100+
err = r.Dev(ctx, output, config.Build.Artifacts)
74101
if r.HasDeployed() {
75102
cleanup = func() {
76103
if err := r.Cleanup(context.Background(), out); err != nil {
@@ -86,3 +113,78 @@ func dev(out io.Writer) error {
86113
}
87114
}
88115
}
116+
117+
func createApp() (*tview.Application, *config.Output) {
118+
app := tview.NewApplication()
119+
120+
mainView := tview.NewTextView()
121+
mainView.
122+
SetChangedFunc(func() {
123+
app.Draw()
124+
}).
125+
SetDynamicColors(true).
126+
SetBorder(true).
127+
SetTitle("Build")
128+
129+
logsView := tview.NewTextView()
130+
logsView.
131+
SetChangedFunc(func() {
132+
app.Draw()
133+
}).
134+
SetDynamicColors(true).
135+
SetBorder(true).
136+
SetTitle("Logs")
137+
138+
grid := tview.NewGrid()
139+
grid.
140+
SetRows(0, 0).
141+
SetColumns(0).
142+
SetBorders(false).
143+
AddItem(mainView, 0, 0, 1, 1, 0, 0, false).
144+
AddItem(logsView, 1, 0, 1, 1, 0, 0, false)
145+
146+
app.
147+
SetRoot(grid, true).
148+
SetFocus(grid)
149+
150+
output := &config.Output{
151+
Main: color.ColoredWriter{Writer: ansiWriter(mainView)},
152+
Logs: color.ColoredWriter{Writer: ansiWriter(logsView)},
153+
}
154+
155+
return app, output
156+
}
157+
158+
func ansiWriter(writer io.Writer) io.Writer {
159+
return &ansi{
160+
Writer: writer,
161+
replacer: strings.NewReplacer(
162+
"\033[31m", "[maroon]",
163+
"\033[32m", "[green]",
164+
"\033[33m", "[olive]",
165+
"\033[34m", "[navy]",
166+
"\033[35m", "[purple]",
167+
"\033[36m", "[teal]",
168+
"\033[37m", "[silver]",
169+
170+
"\033[91m", "[red]",
171+
"\033[92m", "[lime]",
172+
"\033[93m", "[yellow]",
173+
"\033[94m", "[blue]",
174+
"\033[95m", "[fuchsia]",
175+
"\033[96m", "[aqua]",
176+
"\033[97m", "[white]",
177+
178+
"\033[0m", "",
179+
),
180+
}
181+
}
182+
183+
type ansi struct {
184+
io.Writer
185+
replacer *strings.Replacer
186+
}
187+
188+
func (a *ansi) Write(text []byte) (int, error) {
189+
return a.replacer.WriteString(a.Writer, string(text))
190+
}

docs/content/en/docs/references/cli/_index.md

+2
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ Usage:
207207
Flags:
208208
--cleanup Delete deployments after dev mode is interrupted (default true)
209209
-d, --default-repo string Default repository value (overrides global config)
210+
--experimental-gui Experimental Graphical User Interface
210211
-f, --filename string Filename or URL to the pipeline file (default "skaffold.yaml")
211212
-l, --label stringArray Add custom labels to deployed objects. Set multiple times for multiple labels
212213
-n, --namespace string Run deployments in the specified namespace
@@ -228,6 +229,7 @@ Env vars:
228229

229230
* `SKAFFOLD_CLEANUP` (same as --cleanup)
230231
* `SKAFFOLD_DEFAULT_REPO` (same as --default-repo)
232+
* `SKAFFOLD_EXPERIMENTAL_GUI` (same as --experimental-gui)
231233
* `SKAFFOLD_FILENAME` (same as --filename)
232234
* `SKAFFOLD_LABEL` (same as --label)
233235
* `SKAFFOLD_NAMESPACE` (same as --namespace)

pkg/skaffold/color/formatter.go

+10
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ var (
5656
Purple = Color(35)
5757
// Cyan can format text to be displayed to the terminal in cyan, using ANSI escape codes.
5858
Cyan = Color(36)
59+
// White can format text to be displayed to the terminal in white, using ANSI escape codes.
60+
White = Color(37)
5961
// None uses ANSI escape codes to reset all formatting.
6062
None = Color(0)
6163

@@ -99,12 +101,20 @@ type ColoredWriteCloser struct {
99101
io.WriteCloser
100102
}
101103

104+
// ColoredWriter forces printing with colors to an io.Writer.
105+
type ColoredWriter struct {
106+
io.Writer
107+
}
108+
102109
// This implementation comes from logrus (https://github.com/sirupsen/logrus/blob/master/terminal_check_notappengine.go),
103110
// unfortunately logrus doesn't expose a public interface we can use to call it.
104111
func isTerminal(w io.Writer) bool {
105112
if _, ok := w.(ColoredWriteCloser); ok {
106113
return true
107114
}
115+
if _, ok := w.(ColoredWriter); ok {
116+
return true
117+
}
108118

109119
switch v := w.(type) {
110120
case *os.File:

pkg/skaffold/config/options.go

+8
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ limitations under the License.
1717
package config
1818

1919
import (
20+
"io"
2021
"strings"
2122
)
2223

24+
// Output defines which zones on the screen to print to
25+
type Output struct {
26+
Main io.Writer
27+
Logs io.Writer
28+
}
29+
2330
// SkaffoldOptions are options that are set by command line arguments not included
2431
// in the config file itself
2532
type SkaffoldOptions struct {
@@ -30,6 +37,7 @@ type SkaffoldOptions struct {
3037
TailDev bool
3138
PortForward bool
3239
SkipTests bool
40+
ExperimentalGUI bool
3341
Profiles []string
3442
CustomTag string
3543
Namespace string

pkg/skaffold/kubernetes/log.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func (a *LogAggregator) streamRequest(ctx context.Context, headerColor color.Col
196196
if _, err := headerColor.Fprintf(a.output, "%s ", header); err != nil {
197197
return errors.Wrap(err, "writing pod prefix header to out")
198198
}
199-
if _, err := fmt.Fprint(a.output, string(line)); err != nil {
199+
if _, err := color.White.Fprint(a.output, string(line)); err != nil {
200200
return errors.Wrap(err, "writing pod log to out")
201201
}
202202
}

pkg/skaffold/runner/dev.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ package runner
1818

1919
import (
2020
"context"
21-
"io"
2221

2322
"github.com/pkg/errors"
2423
"github.com/sirupsen/logrus"
2524

2625
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
2726
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
27+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2828
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
2929
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
3030
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
@@ -36,11 +36,11 @@ var ErrorConfigurationChanged = errors.New("configuration changed")
3636

3737
// Dev watches for changes and runs the skaffold build and deploy
3838
// pipeline until interrupted by the user.
39-
func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*latest.Artifact) error {
40-
logger := r.newLogger(out, artifacts)
39+
func (r *SkaffoldRunner) Dev(ctx context.Context, output *config.Output, artifacts []*latest.Artifact) error {
40+
logger := r.newLogger(output.Logs, artifacts)
4141
defer logger.Stop()
4242

43-
portForwarder := kubernetes.NewPortForwarder(out, r.imageList, r.namespaces)
43+
portForwarder := kubernetes.NewPortForwarder(output.Main, r.imageList, r.namespaces)
4444
defer portForwarder.Stop()
4545

4646
// Create watcher and register artifacts to build current state of files.
@@ -67,20 +67,20 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
6767
return ErrorConfigurationChanged
6868
case len(changed.needsResync) > 0:
6969
for _, s := range changed.needsResync {
70-
color.Default.Fprintf(out, "Syncing %d files for %s\n", len(s.Copy)+len(s.Delete), s.Image)
70+
color.Default.Fprintf(output.Main, "Syncing %d files for %s\n", len(s.Copy)+len(s.Delete), s.Image)
7171

7272
if err := r.Syncer.Sync(ctx, s); err != nil {
7373
logrus.Warnln("Skipping deploy due to sync error:", err)
7474
return nil
7575
}
7676
}
7777
case len(changed.needsRebuild) > 0:
78-
if err := r.buildTestDeploy(ctx, out, changed.needsRebuild); err != nil {
78+
if err := r.buildTestDeploy(ctx, output.Main, changed.needsRebuild); err != nil {
7979
logrus.Warnln("Skipping deploy due to error:", err)
8080
return nil
8181
}
8282
case changed.needsRedeploy:
83-
if err := r.Deploy(ctx, out, r.builds); err != nil {
83+
if err := r.Deploy(ctx, output.Main, r.builds); err != nil {
8484
logrus.Warnln("Skipping deploy due to error:", err)
8585
return nil
8686
}
@@ -131,7 +131,7 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
131131
}
132132

133133
// First run
134-
if err := r.buildTestDeploy(ctx, out, artifacts); err != nil {
134+
if err := r.buildTestDeploy(ctx, output.Main, artifacts); err != nil {
135135
return errors.Wrap(err, "exiting dev mode because first run failed")
136136
}
137137

@@ -148,5 +148,5 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
148148
}
149149
}
150150

151-
return r.Watcher.Run(ctx, out, onChange)
151+
return r.Watcher.Run(ctx, output.Main, onChange)
152152
}

pkg/skaffold/runner/dev_test.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io/ioutil"
2424
"testing"
2525

26+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2627
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
2728
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/watch"
2829
"github.com/GoogleContainerTools/skaffold/testutil"
@@ -82,6 +83,13 @@ func (t *TestWatcher) Run(ctx context.Context, out io.Writer, onChange func() er
8283
return nil
8384
}
8485

86+
func discardOutput() *config.Output {
87+
return &config.Output{
88+
Main: ioutil.Discard,
89+
Logs: ioutil.Discard,
90+
}
91+
}
92+
8593
func TestDevFailFirstCycle(t *testing.T) {
8694
var tests = []struct {
8795
description string
@@ -129,7 +137,7 @@ func TestDevFailFirstCycle(t *testing.T) {
129137
runner := createRunner(t, test.testBench)
130138
runner.Watcher = test.watcher
131139

132-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{{
140+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{{
133141
ImageName: "img",
134142
}})
135143

@@ -260,7 +268,7 @@ func TestDev(t *testing.T) {
260268
testBench: test.testBench,
261269
}
262270

263-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
271+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
264272
{ImageName: "img1"},
265273
{ImageName: "img2"},
266274
})
@@ -325,7 +333,7 @@ func TestDevSync(t *testing.T) {
325333
testBench: test.testBench,
326334
}
327335

328-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
336+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
329337
{
330338
ImageName: "img1",
331339
Sync: map[string]string{

0 commit comments

Comments
 (0)