Skip to content

Commit

Permalink
Refactor OutputWriterSpinner component and Add SetText and `Start…
Browse files Browse the repository at this point in the history
…Spinner` APIs (vmware-tanzu#157)

* Eliminate the use of parameters and use options to create a spinner
* Implements SetText, StartSpinner APIs
* Update docs
  • Loading branch information
anujc25 authored Feb 6, 2024
1 parent d0ea232 commit 9aac1de
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 84 deletions.
87 changes: 62 additions & 25 deletions component/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,51 +179,88 @@ This will output:

## OutputWriterSpinner Component

`OutputWriterSpinner` provides an interface to `OutputWriter` augmented with a spinner. It allows for rendering output with a spinner while also providing the ability to stop the spinner and render the final output.
`OutputWriterSpinner` provides an interface to `OutputWriter` augmented with a spinner. It allows for displaying output of multiple types (Text, Table, Yaml, Json) with a spinner while also providing the ability to stop the spinner, display the final text and render the output.

### Usage

To use `OutputWriterSpinner`, you can import the package in your Go code and create an instance of the `OutputWriterSpinner` interface using the `NewOutputWriterSpinnerWithOptions` function.
To use `OutputWriterSpinner`, you can import the package in your Go code and create an instance of the `OutputWriterSpinner` interface using the `NewOutputWriterSpinner` function.

Note that NewOutputWriterWithSpinner has been deprecated in favor of NewOutputWriterSpinnerWithOptions
One difference in the default rendering behavior for YAML/JSON output between
the writer created with NewOutputWriterWithSpinner vs that created with NewOutputWriterSpinnerWithOptions
is that row fields no longer gets auto stringified in the latter.
Note that `NewOutputWriterWithSpinner` and `NewOutputWriterSpinnerWithOptions` has been deprecated in favor of `NewOutputWriterSpinner`.
The main difference is how the parameters are provided when creating a `OutputWriterSpinner` component.

To retain the stringify behavior (unlikely what if needed except for backward compatibility
reasons), create the output writer by including WithAutoStringify() in the
options list .
#### Using Spinner to display Table/JSON/YAML output

``` go
import "github.com/vmware-tanzu/tanzu-plugin-runtime/component"


// create new OutputWriterSpinner
outputWriterSpinner, err := component.NewOutputWriterSpinnerWithOptions(os.Stdout, "json", "Loading...", true, []OutputWriterOption{})
// Create new OutputWriterSpinner component. If `WithOutputStream` option is not provided, it will use os.Stdout as default output stream
owSpinner, err := component.NewOutputWriterSpinner(
component.WithOutputFormat(component.TableOutputType), // For JSON use JSONOutputType and for YAML use YAMLOutputType
component.WithSpinnerText("Fetching data..."),
component.WithSpinnerStarted(),
component.WithHeaders("Namespace", "Name", "Ready"))
if err != nil {
fmt.Println("Error creating OutputWriterSpinner:", err)
return
}

// Render output with spinner
outputWriterSpinner.RenderWithSpinner()
// Do some processing to fetch the data from server and fill rows for table
owSpinner.AddRow("default", "pod1", "False")
owSpinner.AddRow("default", "pod2", "True")

// Stop the running spinner instance, displays FinalText if set, then renders the tabular output
owSpinner.Render()

// Stop spinner and render final output
outputWriterSpinner.StopSpinner()
```

The `NewOutputWriterWithSpinner` function takes in the following parameters:
#### Using Spinner to display plain text

``` go
import "github.com/vmware-tanzu/tanzu-plugin-runtime/component"

// Create new OutputWriterSpinner component
spinner, err := component.NewOutputWriterSpinner(component.WithOutputStream(os.Stderr))
if err != nil {
fmt.Println("Error creating spinner:", err)
return
}

- `output io.Writer`: The output writer for the spinner and final output.
- `outputFormat string`: The output format for the final output. It can be either "json" or "yaml".
- `spinnerText string`: The text to display next to the spinner.
- `startSpinner bool`: Whether to start the spinner immediately or not.
- `headers ...string`: Optional headers for the final output.
spinner.SetText("Installing plugin 'apps'")
spinner.StartSpinner()

The created `OutputWriterSpinner` instance provides two methods:
// Do some processing to fetch and install the required plugin
err := InstallPlugin("apps")

if err != nil {
spinner.SetFinalText("Error while installing the plugin 'apps'", log.LogTypeERROR)
} else {
spinner.SetFinalText("Successfully installed the plugin 'apps'", log.LogTypeSuccess)
}

// Stop the spinner and displays FinalText
spinner.StopSpinner()

```

- `RenderWithSpinner()`: Renders the output with a spinner.
- `StopSpinner()`: Stops the spinner and renders the final output.
The `NewOutputWriterSpinner` function takes in the following optional options:

- `WithOutputStream(writer io.Writer)`: sets the output stream for the OutputWriterSpinner component.
- `WithOutputFormat(outputFormat OutputType)`: sets output format for the OutputWriterSpinner component.
- `WithOutputWriterOptions(opts ...OutputWriterOption)`: configures OutputWriterOptions to the OutputWriterSpinner component.
- `WithSpinnerText(text string)`: sets the spinner text
- `WithSpinnerFinalText(finalText string, prefix log.LogType)`: sets the spinner final text and prefix log indicator
- `WithSpinnerStarted()`: starts the spinner when the OutputWriterSpinner component gets created
- `WithHeaders(headers ...string)`: Sets the headers for the OutputWriterOptions component

The created `OutputWriterSpinner` instance provides following methods:

- `SetKeys(headerKeys ...string)`: sets the headers for the OutputWriter component
- `AddRow(items ...interface{})`: adds rows to the OutputWriter component
- `SetText(text string)`: sets the spinner text
- `SetFinalText(finalText string, prefix log.LogType)`: sets the spinner final text and prefix log indicator
- `StartSpinner()`: starts the spinner instance, showing the spinnerText
- `StopSpinner()`: stops the running spinner instance, displays FinalText if set
- `Render()`: stops the running spinner instance, displays FinalText if set, then renders the output

## Prompt Component

Expand Down
149 changes: 105 additions & 44 deletions component/output_spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import (
// OutputWriterSpinner is OutputWriter augmented with a spinner.
type OutputWriterSpinner interface {
OutputWriter
// RenderWithSpinner will stop spinner and render the output
// Deprecated: RenderWithSpinner is being deprecated in favor of Render.
RenderWithSpinner()
// StartSpinner starts the spinner instance, showing the spinnerText
StartSpinner()
// StopSpinner stops the running spinner instance, displays FinalText if set
StopSpinner()
// SetText sets the spinner text
SetText(text string)
// SetFinalText sets the spinner final text and prefix
// log indicator (log.LogTypeOUTPUT can be used for no prefix)
SetFinalText(finalText string, prefix log.LogType)
Expand All @@ -28,32 +35,69 @@ type OutputWriterSpinner interface {
// outputwriterspinner is our internal implementation.
type outputwriterspinner struct {
outputwriter
spinnerText string
spinnerFinalText string
spinner *spinner.Spinner
spinnerText string
spinnerFinalText string
startSpinnerOnInit bool
spinner *spinner.Spinner
}

type OutputWriterSpinnerOptions struct {
OutputWriterOptions []OutputWriterOption
SpinnerOptions []OutputWriterSpinnerOption
}

// OutputWriterSpinnerOption is an option for outputwriterspinner
// OutputWriterSpinnerOption is an option to configure outputwriterspinner
type OutputWriterSpinnerOption func(*outputwriterspinner)

// WithSpinnerFinalText sets the spinner final text and prefix log indicator
// (log.LogTypeOUTPUT can be used for no prefix)
func WithSpinnerFinalText(finalText string, prefix log.LogType) OutputWriterSpinnerOption {
finalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText)
return func(ows *outputwriterspinner) {
ows.spinnerFinalText = finalText
ows.spinnerFinalText = fmt.Sprintf("%s%s", log.GetLogTypeIndicator(prefix), finalText)
}
}

// WithOutputWriterOptions configures OutputWriterOptions to the spinner
func WithOutputWriterOptions(opts ...OutputWriterOption) OutputWriterSpinnerOption {
return func(ow *outputwriterspinner) {
ow.applyOptions(opts)
}
}

// WithSpinnerText sets the spinner text
func WithSpinnerText(text string) OutputWriterSpinnerOption {
return func(ows *outputwriterspinner) {
ows.spinnerText = text
}
}

// WithSpinnerStarted starts the spinner
func WithSpinnerStarted() OutputWriterSpinnerOption {
return func(ows *outputwriterspinner) {
ows.startSpinnerOnInit = true
}
}

// WithHeaders sets key headers
func WithHeaders(headers ...string) OutputWriterSpinnerOption {
return func(ows *outputwriterspinner) {
ows.keys = headers
}
}

// WithOutputFormat sets output format for the OutputWriterSpinner component
func WithOutputFormat(outputFormat OutputType) OutputWriterSpinnerOption {
return func(ows *outputwriterspinner) {
ows.outputFormat = outputFormat
}
}

// WithOutputStream sets the output stream for the OutputWriterSpinner component
func WithOutputStream(writer io.Writer) OutputWriterSpinnerOption {
return func(ows *outputwriterspinner) {
ows.out = writer
}
}

// NewOutputWriterWithSpinner returns implementation of OutputWriterSpinner.
//
// Deprecated: NewOutputWriterWithSpinner is being deprecated in favor of
// NewOutputWriterSpinnerWithSpinnerOptions.
// NewOutputWriterSpinner.
// Until it is removed, it will retain the existing behavior of converting
// incoming row values to their golang string representation for backward
// compatibility reasons
Expand All @@ -65,60 +109,69 @@ func NewOutputWriterWithSpinner(output io.Writer, outputFormat, spinnerText stri
// NewOutputWriterSpinnerWithOptions returns implementation of OutputWriterSpinner.
//
// Deprecated: NewOutputWriterSpinnerWithOptions is being deprecated in favor of
// NewOutputWriterSpinnerWithSpinnerOptions.
// NewOutputWriterSpinner.
func NewOutputWriterSpinnerWithOptions(output io.Writer, outputFormat, spinnerText string, startSpinner bool, opts []OutputWriterOption, headers ...string) (OutputWriterSpinner, error) {
ows := &outputwriterspinner{}
ows.out = output
ows.outputFormat = OutputType(outputFormat)
ows.keys = headers
ows.applyOptions(opts)

return setAndInitializeSpinner(ows, spinnerText, startSpinner)
ows.spinnerText = spinnerText
ows.startSpinnerOnInit = startSpinner
return initializeSpinner(ows), nil
}

// NewOutputWriterSpinnerWithSpinnerOptions returns implementation of OutputWriterSpinner.
func NewOutputWriterSpinnerWithSpinnerOptions(output io.Writer, outputFormat OutputType, spinnerText string, startSpinner bool, opts OutputWriterSpinnerOptions, headers ...string) (OutputWriterSpinner, error) {
// NewOutputWriterSpinner returns implementation of OutputWriterSpinner
func NewOutputWriterSpinner(opts ...OutputWriterSpinnerOption) (OutputWriterSpinner, error) {
ows := &outputwriterspinner{}
ows.out = output
ows.outputFormat = outputFormat
ows.keys = headers
ows.applyOptions(opts.OutputWriterOptions)
ows.applyOutputWriterSpinnerOptions(opts.SpinnerOptions)
return setAndInitializeSpinner(ows, spinnerText, startSpinner)
ows.out = os.Stdout
ows.applySpinnerOptions(opts)
// Note: We are returning the 'nil' error always to protect against the possible API
// enhancement which might require throwing an error in future
return initializeSpinner(ows), nil
}

// setAndInitializeSpinner sets the spinner text and initializes the spinner
func setAndInitializeSpinner(ows *outputwriterspinner, spinnerText string, startSpinner bool) (OutputWriterSpinner, error) {
// initializeSpinner initializes the spinner
func initializeSpinner(ows *outputwriterspinner) OutputWriterSpinner {
if ows.outputFormat != JSONOutputType && ows.outputFormat != YAMLOutputType {
ows.spinnerText = spinnerText
ows.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond)
if err := ows.spinner.Color("bgBlack", "bold", "fgWhite"); err != nil {
return nil, err
}
ows.spinner.Suffix = fmt.Sprintf(" %s", spinnerText)
if ows.spinnerFinalText != "" {
spinner.WithFinalMSG(ows.spinnerFinalText)(ows.spinner)
}
ows.spinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond,
spinner.WithWriter(ows.out),
spinner.WithFinalMSG(ows.spinnerFinalText),
spinner.WithSuffix(fmt.Sprintf(" %s", ows.spinnerText)),
spinner.WithColor("bold"),
)

// Start the spinner only if attached to terminal
attachedToTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
if startSpinner && attachedToTerminal {
if ows.startSpinnerOnInit && attachedToTerminal {
ows.spinner.Start()
}
}
return ows, nil
return ows
}

// RenderWithSpinner will stop spinner and render the output
// RenderWithSpinner stops the running spinner instance, displays FinalText if set, then renders the output
//
// Deprecated: RenderWithSpinner is being deprecated in favor of Render.
func (ows *outputwriterspinner) RenderWithSpinner() {
if ows.spinner != nil && ows.spinner.Active() {
ows.spinner.Stop()
fmt.Fprintln(ows.out)
}
ows.Render()
}

// stop spinner
// Render stops the running spinner instance, displays FinalText if set, then renders the output
func (ows *outputwriterspinner) Render() {
ows.StopSpinner()
ows.outputwriter.Render()
}

// StartSpinner starts the spinner instance, showing the spinnerText
func (ows *outputwriterspinner) StartSpinner() {
attachedToTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
if ows.spinner != nil && !ows.spinner.Active() && attachedToTerminal {
ows.spinner.Start()
}
}

// StopSpinner stops the running spinner instance, displays FinalText if set
func (ows *outputwriterspinner) StopSpinner() {
if ows.spinner != nil && ows.spinner.Active() {
ows.spinner.Stop()
Expand All @@ -135,8 +188,16 @@ func (ows *outputwriterspinner) SetFinalText(finalText string, prefix log.LogTyp
}
}

// applyOutputWriterSpinnerOptions applies the options to the outputwriterspinner
func (ows *outputwriterspinner) applyOutputWriterSpinnerOptions(spinnerOpts []OutputWriterSpinnerOption) {
// SetText sets the spinner text
func (ows *outputwriterspinner) SetText(text string) {
if ows.spinner != nil {
ows.spinnerText = text
ows.spinner.Suffix = fmt.Sprintf(" %s", text)
}
}

// applySpinnerOptions applies the options to the outputwriterspinner
func (ows *outputwriterspinner) applySpinnerOptions(spinnerOpts []OutputWriterSpinnerOption) {
for i := range spinnerOpts {
spinnerOpts[i](ows)
}
Expand Down
33 changes: 18 additions & 15 deletions component/output_spinner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,34 @@ func TestNewOutputWriterSpinnerWithOptions(t *testing.T) {
assert.NotNil(t, ows)
}

func TestNewOutputWriterSpinnerWithSpinnerOptions(t *testing.T) {
func TestNewOutputWriterSpinner(t *testing.T) {
output := bytes.Buffer{}
spinnerText := loading
headers := []string{"Name", "Age"}

// Test creating an OutputWriterSpinner with spinner options and a spinner
opts := OutputWriterSpinnerOptions{
OutputWriterOptions: []OutputWriterOption{WithAutoStringify()},
SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)},
}
ows, err := NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, true, opts, headers...)
ows, err := NewOutputWriterSpinner(WithOutputStream(&output),
WithOutputFormat(TableOutputType),
WithSpinnerText(spinnerText),
WithSpinnerStarted(),
WithOutputWriterOptions(WithAutoStringify()),
WithHeaders(headers...),
WithSpinnerFinalText("Done!", log.LogTypeSUCCESS))
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with spinner options without a spinner
opts = OutputWriterSpinnerOptions{
SpinnerOptions: []OutputWriterSpinnerOption{WithSpinnerFinalText("Done!", log.LogTypeSUCCESS)},
}
ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "table", spinnerText, false, opts, headers...)
ows, err = NewOutputWriterSpinner(WithOutputStream(&output),
WithOutputFormat(TableOutputType),
WithSpinnerText(spinnerText),
WithHeaders(headers...),
WithSpinnerFinalText("Done!", log.LogTypeSUCCESS))
assert.NoError(t, err)
assert.NotNil(t, ows)

// Test creating an OutputWriterSpinner with unsupported output format
opts = OutputWriterSpinnerOptions{}
ows, err = NewOutputWriterSpinnerWithSpinnerOptions(&output, "unsupported", spinnerText, true, opts, headers...)
ows, err = NewOutputWriterSpinner(WithOutputStream(&output),
WithOutputFormat("unsupported"),
WithSpinnerText(spinnerText),
WithSpinnerStarted(),
WithHeaders(headers...))
assert.NoError(t, err)
assert.NotNil(t, ows)
}
Expand Down

0 comments on commit 9aac1de

Please sign in to comment.