2018-05-22 19:43:36 +00:00
|
|
|
// Copyright 2016-2018, Pulumi Corporation.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2018-09-04 22:40:15 +00:00
|
|
|
package display
|
2018-04-19 22:55:24 +00:00
|
|
|
|
|
|
|
// forked from: https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go
|
|
|
|
// so we can customize parts of the display of our progress messages
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2023-12-12 12:19:42 +00:00
|
|
|
"strconv"
|
2022-10-31 14:59:14 +00:00
|
|
|
"unicode/utf8"
|
2018-04-19 22:55:24 +00:00
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/backend/display/internal/terminal"
|
2022-10-31 14:59:14 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/engine"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
2018-04-19 22:55:24 +00:00
|
|
|
)
|
|
|
|
|
2022-10-31 14:59:14 +00:00
|
|
|
// Progress describes a message we want to show in the display. There are two types of messages,
|
|
|
|
// simple 'Messages' which just get printed out as a single uninterpreted line, and 'Actions' which
|
|
|
|
// are placed and updated in the progress-grid based on their ID. Messages do not need an ID, while
|
|
|
|
// Actions must have an ID.
|
|
|
|
type Progress struct {
|
|
|
|
ID string
|
|
|
|
Message string
|
|
|
|
Action string
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeMessageProgress(message string) Progress {
|
|
|
|
return Progress{Message: message}
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeActionProgress(id string, action string) Progress {
|
|
|
|
contract.Assertf(id != "", "id must be non empty for action %s", action)
|
|
|
|
contract.Assertf(action != "", "action must be non empty")
|
|
|
|
|
|
|
|
return Progress{ID: id, Action: action}
|
|
|
|
}
|
|
|
|
|
2018-04-19 22:55:24 +00:00
|
|
|
// Display displays the Progress to `out`. `termInfo` is non-nil if `out` is a terminal.
|
2022-10-31 15:40:22 +00:00
|
|
|
func (jm *Progress) Display(out io.Writer, termInfo terminal.Info) {
|
2018-04-19 22:55:24 +00:00
|
|
|
var endl string
|
|
|
|
if termInfo != nil && /*jm.Stream == "" &&*/ jm.Action != "" {
|
2022-10-31 15:40:22 +00:00
|
|
|
termInfo.ClearLine(out)
|
2018-04-19 22:55:24 +00:00
|
|
|
endl = "\r"
|
2019-04-01 14:27:21 +00:00
|
|
|
fmt.Fprint(out, endl)
|
2018-04-19 22:55:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if jm.Action != "" && termInfo != nil {
|
|
|
|
fmt.Fprintf(out, "%s%s", jm.Action, endl)
|
|
|
|
} else {
|
|
|
|
var msg string
|
|
|
|
if jm.Action != "" {
|
|
|
|
msg = jm.Action
|
|
|
|
} else {
|
|
|
|
msg = jm.Message
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintf(out, "%s%s\n", msg, endl)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-31 14:59:14 +00:00
|
|
|
type messageRenderer struct {
|
2022-10-31 15:40:22 +00:00
|
|
|
opts Options
|
|
|
|
isInteractive bool
|
2022-10-31 14:59:14 +00:00
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
terminal terminal.Terminal
|
2022-10-31 14:59:14 +00:00
|
|
|
terminalWidth int
|
|
|
|
terminalHeight int
|
|
|
|
|
|
|
|
// A spinner to use to show that we're still doing work even when no output has been
|
|
|
|
// printed to the console in a while.
|
|
|
|
nonInteractiveSpinner cmdutil.Spinner
|
|
|
|
|
|
|
|
progressOutput chan<- Progress
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
closed <-chan bool
|
2022-10-31 14:59:14 +00:00
|
|
|
|
|
|
|
// Cache of lines we've already printed. We don't print a progress message again if it hasn't
|
|
|
|
// changed between the last time we printed and now.
|
|
|
|
printedProgressCache map[string]Progress
|
|
|
|
}
|
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
func newInteractiveMessageRenderer(term terminal.Terminal, opts Options) progressRenderer {
|
|
|
|
r := newMessageRenderer(term, opts, true)
|
|
|
|
r.terminal = term
|
2022-11-08 02:50:08 +00:00
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
var err error
|
|
|
|
r.terminalWidth, r.terminalHeight, err = term.Size()
|
|
|
|
contract.IgnoreError(err)
|
2022-11-08 02:50:08 +00:00
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
return r
|
2022-11-08 02:50:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newNonInteractiveRenderer(stdout io.Writer, op string, opts Options) progressRenderer {
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
spinner, ticker := cmdutil.NewSpinnerAndTicker(
|
|
|
|
fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), op),
|
2024-02-05 11:48:10 +00:00
|
|
|
nil, opts.Color, 1 /*timesPerSecond*/, opts.SuppressProgress)
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
ticker.Stop()
|
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
r := newMessageRenderer(stdout, opts, false)
|
|
|
|
r.nonInteractiveSpinner = spinner
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func newMessageRenderer(out io.Writer, opts Options, isInteractive bool) *messageRenderer {
|
|
|
|
progressOutput, closed := make(chan Progress), make(chan bool)
|
|
|
|
go func() {
|
|
|
|
ShowProgressOutput(progressOutput, out, isInteractive)
|
|
|
|
close(closed)
|
|
|
|
}()
|
|
|
|
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
return &messageRenderer{
|
2022-10-31 15:40:22 +00:00
|
|
|
opts: opts,
|
|
|
|
isInteractive: isInteractive,
|
|
|
|
progressOutput: progressOutput,
|
|
|
|
closed: closed,
|
|
|
|
printedProgressCache: make(map[string]Progress),
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-31 14:59:14 +00:00
|
|
|
func (r *messageRenderer) Close() error {
|
|
|
|
close(r.progressOutput)
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
<-r.closed
|
2022-10-31 14:59:14 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts the colorization tags in a progress message and then actually writes the progress
|
|
|
|
// message to the output stream. This should be the only place in this file where we actually
|
|
|
|
// process colorization tags.
|
|
|
|
func (r *messageRenderer) colorizeAndWriteProgress(progress Progress) {
|
|
|
|
if progress.Message != "" {
|
|
|
|
progress.Message = r.opts.Color.Colorize(progress.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
if progress.Action != "" {
|
|
|
|
progress.Action = r.opts.Color.Colorize(progress.Action)
|
|
|
|
}
|
|
|
|
|
|
|
|
if progress.ID != "" {
|
|
|
|
// don't repeat the same output if there is no difference between the last time we
|
|
|
|
// printed it and now.
|
|
|
|
lastProgress, has := r.printedProgressCache[progress.ID]
|
|
|
|
if has && lastProgress.Message == progress.Message && lastProgress.Action == progress.Action {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
r.printedProgressCache[progress.ID] = progress
|
|
|
|
}
|
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
if !r.isInteractive {
|
2022-10-31 14:59:14 +00:00
|
|
|
// We're about to display something. Reset our spinner so that it will go on the next line.
|
|
|
|
r.nonInteractiveSpinner.Reset()
|
|
|
|
}
|
|
|
|
|
|
|
|
r.progressOutput <- progress
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) writeSimpleMessage(msg string) {
|
|
|
|
r.colorizeAndWriteProgress(makeMessageProgress(msg))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) println(display *ProgressDisplay, line string) {
|
|
|
|
r.writeSimpleMessage(line)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) tick(display *ProgressDisplay) {
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
r.render(display, false)
|
2022-10-31 14:59:14 +00:00
|
|
|
} else {
|
|
|
|
// Update the spinner to let the user know that that work is still happening.
|
|
|
|
r.nonInteractiveSpinner.Tick()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) renderRow(display *ProgressDisplay,
|
2023-03-03 16:36:39 +00:00
|
|
|
id string, colorizedColumns []string, maxColumnLengths []int,
|
|
|
|
) {
|
2022-10-31 16:00:20 +00:00
|
|
|
row := renderRow(colorizedColumns, maxColumnLengths)
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
2022-10-31 14:59:14 +00:00
|
|
|
// Ensure we don't go past the end of the terminal. Note: this is made complex due to
|
|
|
|
// msgWithColors having the color code information embedded with it. So we need to get
|
|
|
|
// the right substring of it, assuming that embedded colors are just markup and do not
|
|
|
|
// actually contribute to the length
|
|
|
|
maxRowLength := r.terminalWidth - 1
|
|
|
|
if maxRowLength < 0 {
|
|
|
|
maxRowLength = 0
|
|
|
|
}
|
|
|
|
row = colors.TrimColorizedString(row, maxRowLength)
|
|
|
|
}
|
|
|
|
|
|
|
|
if row != "" {
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
2022-10-31 14:59:14 +00:00
|
|
|
r.colorizeAndWriteProgress(makeActionProgress(id, row))
|
|
|
|
} else {
|
|
|
|
r.writeSimpleMessage(row)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) rowUpdated(display *ProgressDisplay, row Row) {
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
2022-10-31 14:59:14 +00:00
|
|
|
// if we're in a terminal, then refresh everything so that all our columns line up
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
r.render(display, false)
|
2022-10-31 14:59:14 +00:00
|
|
|
} else {
|
|
|
|
// otherwise, just print out this single row.
|
|
|
|
colorizedColumns := row.ColorizedColumns()
|
|
|
|
colorizedColumns[display.suffixColumn] += row.ColorizedSuffix()
|
|
|
|
r.renderRow(display, "", colorizedColumns, nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) systemMessage(display *ProgressDisplay, payload engine.StdoutEventPayload) {
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
2022-10-31 14:59:14 +00:00
|
|
|
// if we're in a terminal, then refresh everything. The system events will come after
|
|
|
|
// all the normal rows
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
r.render(display, false)
|
2022-10-31 14:59:14 +00:00
|
|
|
} else {
|
|
|
|
// otherwise, in a non-terminal, just print out the actual event.
|
|
|
|
r.writeSimpleMessage(renderStdoutColorEvent(payload, display.opts))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *messageRenderer) done(display *ProgressDisplay) {
|
2022-10-31 15:40:22 +00:00
|
|
|
if r.isInteractive {
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
r.render(display, false)
|
|
|
|
}
|
2022-10-31 14:59:14 +00:00
|
|
|
}
|
|
|
|
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
func (r *messageRenderer) render(display *ProgressDisplay, done bool) {
|
2022-10-31 15:40:22 +00:00
|
|
|
if !r.isInteractive || display.headerRow == nil {
|
2022-10-31 14:59:14 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure our stored dimension info is up to date
|
|
|
|
r.updateTerminalDimensions()
|
|
|
|
|
|
|
|
rootNodes := display.generateTreeNodes()
|
|
|
|
rootNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(rootNodes)
|
|
|
|
sortNodes(rootNodes)
|
|
|
|
display.addIndentations(rootNodes, true /*isRoot*/, "")
|
|
|
|
|
|
|
|
maxSuffixLength := 0
|
|
|
|
for _, v := range display.suffixesArray {
|
|
|
|
runeCount := utf8.RuneCountInString(v)
|
|
|
|
if runeCount > maxSuffixLength {
|
|
|
|
maxSuffixLength = runeCount
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var rows [][]string
|
|
|
|
var maxColumnLengths []int
|
|
|
|
display.convertNodesToRows(rootNodes, maxSuffixLength, &rows, &maxColumnLengths)
|
|
|
|
|
|
|
|
removeInfoColumnIfUnneeded(rows)
|
|
|
|
|
|
|
|
for i, row := range rows {
|
2023-12-12 12:19:42 +00:00
|
|
|
r.renderRow(display, strconv.Itoa(i), row, maxColumnLengths)
|
2022-10-31 14:59:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
systemID := len(rows)
|
|
|
|
|
|
|
|
printedHeader := false
|
|
|
|
for _, payload := range display.systemEventPayloads {
|
|
|
|
msg := payload.Color.Colorize(payload.Message)
|
|
|
|
lines := splitIntoDisplayableLines(msg)
|
|
|
|
|
|
|
|
if len(lines) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if !printedHeader {
|
|
|
|
printedHeader = true
|
|
|
|
r.colorizeAndWriteProgress(makeActionProgress(
|
2023-12-12 12:19:42 +00:00
|
|
|
strconv.Itoa(systemID), " "))
|
2022-10-31 14:59:14 +00:00
|
|
|
systemID++
|
|
|
|
|
|
|
|
r.colorizeAndWriteProgress(makeActionProgress(
|
2023-12-12 12:19:42 +00:00
|
|
|
strconv.Itoa(systemID),
|
2022-10-31 14:59:14 +00:00
|
|
|
colors.Yellow+"System Messages"+colors.Reset))
|
|
|
|
systemID++
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, line := range lines {
|
|
|
|
r.colorizeAndWriteProgress(makeActionProgress(
|
2023-12-12 12:19:42 +00:00
|
|
|
strconv.Itoa(systemID), " "+line))
|
2022-10-31 14:59:14 +00:00
|
|
|
systemID++
|
|
|
|
}
|
|
|
|
}
|
[cli] Reimplement the interactive renderer
The display pipleline looks like this:
╭──────╮
│Engine│
╰──────╯
⬇ engine events
╭────────────────╮
│Progress Display│
╰────────────────╯
⬇ display events: ticks, resource updates, system messages
╭─────────────────╮
│Progress Renderer│
╰─────────────────╯
⬇ text
╭────────╮
│Terminal│
╰────────╯
The existing implementation of the interactive Progress Renderer is broken
into two parts, the display renderer and the message renderer. The display
renderer converts display events into progress messages, each of which
generally represents a single line of text at a particular position in
the output. The message renderer converts progress messages into screen
updates by identifying whether or not the contents of a particular
message have changed and if so, re-rendering its output line. In
somewhat greater detail:
╭────────────────╮
│Display Renderer│
╰────────────────╯
⬇ convert resource rows into a tree table
⬇ convert the tree table and system messages into lines
⬇ convert each line into a progress message with an index
╭────────────────╮
│Message Renderer│
╰────────────────╯
⬇ if the line identified in a progress message has changed,
⬇ go to that line on the terminal, clear it, and update it
╭────────╮
│Terminal│
╰────────╯
This separation of concerns is unnecessary and makes it difficult to
understand where and when the terminal is updated. This approach also
makes it somewhat challenging to change the way in which the display
interacts with the terminal, as both the display renderer and the
message renderer need to e.g. understand terminal dimensions, movement,
etc.
These changes reimplement the interactive Progress Renderer using a
frame-oriented approach. The display is updated at 60 frame per second.
If nothing has happened to invalidate the display's contents (i.e. no
changes to the terminal geometry or the displayable contents have occurred),
then the frame is not redrawn. Otherwise, the contents of the display
are re-rendered and redrawn.
An advantage of this approach is that it made it relatively simple to
fix a long-standing issue with the interactive display: when the number
of rows in the output exceed the height of the terminal, the new
renderer clamps the output and allows the user to scroll the tree table
using the up and down arrow keys.
2022-10-31 14:59:14 +00:00
|
|
|
|
|
|
|
if done {
|
|
|
|
r.println(display, "")
|
|
|
|
}
|
2022-10-31 14:59:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure our stored dimension info is up to date.
|
|
|
|
func (r *messageRenderer) updateTerminalDimensions() {
|
2022-10-31 15:40:22 +00:00
|
|
|
currentTerminalWidth, currentTerminalHeight, err := r.terminal.Size()
|
2022-10-31 14:59:14 +00:00
|
|
|
contract.IgnoreError(err)
|
|
|
|
|
|
|
|
if currentTerminalWidth != r.terminalWidth ||
|
|
|
|
currentTerminalHeight != r.terminalHeight {
|
|
|
|
r.terminalWidth = currentTerminalWidth
|
|
|
|
r.terminalHeight = currentTerminalHeight
|
|
|
|
|
|
|
|
// also clear our display cache as we want to reprint all lines.
|
|
|
|
r.printedProgressCache = make(map[string]Progress)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
// ShowProgressOutput displays a progress stream from `in` to `out`, `isInteractive` describes if
|
2018-04-19 22:55:24 +00:00
|
|
|
// `out` is a terminal. If this is the case, it will print `\n` at the end of each line and move the
|
|
|
|
// cursor while displaying.
|
2022-10-31 15:40:22 +00:00
|
|
|
func ShowProgressOutput(in <-chan Progress, out io.Writer, isInteractive bool) {
|
2023-03-03 16:36:39 +00:00
|
|
|
ids := make(map[string]int)
|
2018-04-19 22:55:24 +00:00
|
|
|
|
2022-10-31 15:40:22 +00:00
|
|
|
var info terminal.Info
|
|
|
|
if isInteractive {
|
|
|
|
term := os.Getenv("TERM")
|
|
|
|
if term == "" {
|
|
|
|
term = "vt102"
|
|
|
|
}
|
|
|
|
info = terminal.OpenInfo(term)
|
|
|
|
}
|
|
|
|
|
2018-04-19 22:55:24 +00:00
|
|
|
for jm := range in {
|
|
|
|
diff := 0
|
|
|
|
|
|
|
|
if jm.Action != "" {
|
|
|
|
if jm.ID == "" {
|
|
|
|
contract.Failf("Must have an ID if we have an action! %s", jm.Action)
|
|
|
|
}
|
|
|
|
|
|
|
|
line, ok := ids[jm.ID]
|
|
|
|
if !ok {
|
|
|
|
// NOTE: This approach of using len(id) to
|
|
|
|
// figure out the number of lines of history
|
|
|
|
// only works as long as we clear the history
|
|
|
|
// when we output something that's not
|
|
|
|
// accounted for in the map, such as a line
|
|
|
|
// with no ID.
|
|
|
|
line = len(ids)
|
|
|
|
ids[jm.ID] = line
|
|
|
|
if info != nil {
|
|
|
|
fmt.Fprintf(out, "\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
diff = len(ids) - line
|
|
|
|
if info != nil {
|
2022-10-31 15:40:22 +00:00
|
|
|
info.CursorUp(out, diff)
|
2018-04-19 22:55:24 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// When outputting something that isn't progress
|
|
|
|
// output, clear the history of previous lines. We
|
|
|
|
// don't want progress entries from some previous
|
|
|
|
// operation to be updated (for example, pull -a
|
|
|
|
// with multiple tags).
|
|
|
|
ids = make(map[string]int)
|
|
|
|
}
|
|
|
|
jm.Display(out, info)
|
|
|
|
if jm.Action != "" && info != nil {
|
2022-10-31 15:40:22 +00:00
|
|
|
info.CursorDown(out, diff)
|
2018-04-19 22:55:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|