// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package progress import ( "context" "io" "time" "github.com/aws/copilot-cli/internal/pkg/term/cursor" ) // Renderer is the interface to print a component to a writer. // It returns the number of lines printed and the error if any. type Renderer interface { Render(out io.Writer) (numLines int, err error) } // DynamicRenderer is a Renderer that can notify that its internal states are Done updating. // DynamicRenderer is implemented by components that listen to events from a streamer and update their state. type DynamicRenderer interface { Renderer Done() <-chan struct{} } // RenderOptions holds optional style configuration for renderers. type RenderOptions struct { Padding int // Leading spaces before rendering the component. } // NestedRenderOptions takes a RenderOptions and returns the same RenderOptions but with additional padding. func NestedRenderOptions(opts RenderOptions) RenderOptions { return RenderOptions{ Padding: opts.Padding + nestedComponentPadding, } } // Render renders r periodically to out and returns the last number of lines written to out. // Render stops when there the ctx is canceled or r is done listening to new events. // While Render is executing, the terminal cursor is hidden and updates are written in-place. func Render(ctx context.Context, out FileWriteFlusher, r DynamicRenderer) (int, error) { defer out.Flush() // Make sure every buffered text in out is written before exiting. cursor := cursor.NewWithWriter(out) cursor.Hide() defer cursor.Show() var writtenLines int for { select { case <-ctx.Done(): return writtenLines, ctx.Err() case <-r.Done(): return EraseAndRender(out, r, writtenLines) case <-time.After(renderInterval): nl, err := EraseAndRender(out, r, writtenLines) if err != nil { return nl, err } writtenLines = nl } } } // EraseAndRender erases prevNumLines from out and then renders r. func EraseAndRender(out FileWriteFlusher, r Renderer, prevNumLines int) (int, error) { cursor.EraseLinesAbove(out, prevNumLines) if err := out.Flush(); err != nil { return 0, err } nl, err := r.Render(out) if err != nil { return 0, err } if err := out.Flush(); err != nil { return 0, err } return nl, err } // MultiRenderer returns a Renderer that's the concatenation of the input renderers. // The renderers are rendered sequentially, and the MultiRenderer is only Done once all renderers are Done. func MultiRenderer(renderers ...DynamicRenderer) DynamicRenderer { mr := &multiRenderer{ renderers: renderers, done: make(chan struct{}), } go mr.listen() return mr } type multiRenderer struct { renderers []DynamicRenderer done chan struct{} } // Render sequentially renders the renderers to out and returns the sum of the number of lines written. func (mr *multiRenderer) Render(out io.Writer) (int, error) { var sum int for _, r := range mr.renderers { nl, err := r.Render(out) if err != nil { return 0, err } sum += nl } return sum, nil } // Done returns a channel that's closed when there are no more events to Listen. func (mr *multiRenderer) Done() <-chan struct{} { return mr.done } func (mr *multiRenderer) listen() { for _, r := range mr.renderers { <-r.Done() } close(mr.done) }